Merge pull request #62 from langbot-app/feat/new-plugin

Feat/new plugin
This commit is contained in:
Junyan Qin (Chin)
2025-09-16 12:35:23 +08:00
committed by GitHub
parent 61eca6f343
commit 5a3cd5fcb1
59 changed files with 3246 additions and 1390 deletions
-164
View File
@@ -1,164 +0,0 @@
# API Reference
:::info Table of Contents
[[toc]]
:::
## Event API
The following APIs are only available in event handler functions.
### Reply to Message
```python
await ctx.reply(message_chain: MessageChain)
```
Reply to the conversation that initiated this event.
- `message_chain`: [Message Chain object](./messages.md), the program can automatically convert it to the target message platform's message chain
### Send Active Message
> Due to poor support for active messages in QQ Official API, if users are using QQ Official API, sending active messages may fail
```python
await ctx.send_message(target_type: str, target_id: str, message_chain: MessageChain)
```
Send an active message to the target.
- `target_type`: Target type, possible values: `"person"`, `"group"`
- `target_id`: Target ID (QQ number or group number)
- `message_chain`: [Message Chain object](./messages.md), the program can automatically convert it to the target message platform's message chain
### Prevent Default Event Behavior
```python
ctx.prevent_default()
```
Prevent the default behavior of this event, i.e., stop the subsequent behaviors in the processing flow (such as getting a reply from the interface when receiving a private chat message, getting a reply from the interface when receiving a group message, etc.).
### Prevent Subsequent Plugin Execution
```python
ctx.prevent_postorder()
```
Prevent the execution of subsequent plugins for this event. To modify the execution order of plugins, please change the priority through the methods described in the "Plugin Management" section of the "Plugin Introduction".
### Add Return Value
```python
ctx.add_return(name: str, value: Any)
```
Add a return value. Event return values are all **optional**. For return values supported by each event, please check the comments of each event in `pkg.plugin.events`.
## Request API
A `Request (Query)` refers to the program context when a user sends a question to LangBot and LangBot processes that question. The APIs in this section are used to interact with these contexts.
- In event handler functions, the Query object is usually located at `ctx.event.query`
### Set Request Variable
```python
ctx.event.query.set_variable(key: str, value: typing.Any)
```
Request variables are a `dict` bound to this Query, which contains some program context information. If you are using Dify or Alibaba Cloud Bailian or other LLMOps runners, [these variables will be passed to the corresponding platform's API as variables](/en/deploy/pipelines/readme.html#conversation-variables).
If you need to set variables using plugins, it is recommended to handle them during the `PromptPreProcessing` event.
- `key`: The name of the variable
- `value`: The value of the variable
### Get Request Variable
```python
ctx.event.query.get_variable(key: str)
```
Get a request variable.
- `key`: Variable name
- Return value: `typing.Any`
### Get All Request Variables
```python
ctx.event.query.get_variables()
```
Get all variable values set for this Query object.
- Return value: `dict[str, typing.Any]`
## LangBot API
The following APIs are provided directly by LangBot for plugins to call and can be called outside of event handler functions.
- `host` represents an object of the `pkg.plugin.context.APIHost` class, which is included in each plugin class.
### Get Enabled Message Platform Adapters List
```python
host.get_platform_adapters()
```
Get a list of enabled message platform adapters.
- Return value: `list[pkg.platform.adapter.MessageSourceAdapter]`
### Send Active Message
```python
await host.send_active_message(adapter: pkg.platform.adapter.MessageSourceAdapter, target_type: str, target_id: str, message_chain: MessageChain)
```
Send an active message to the target.
- `adapter`: Message platform adapter, obtained through `host.get_platform_adapters()` and selecting one of them
- `target_type`: Target type, possible values: `"person"`, `"group"`
- `target_id`: Target ID (QQ number or group number)
- `message_chain`: [Message Chain object](./messages.md), the program can automatically convert it to the target message platform's message chain
:::info
- Some message platforms may not support active messages or have strict limitations
- Some message platform adapters (such as `aiocqhttp`) act as servers waiting for message platforms to connect and push messages. Before a successful connection, sending active messages will fail
:::
Example:
```python
import asyncio
import pkg.platform.types as platform_types
...
# initialize function of a plugin
# This code will attempt to send a message "hello, world!" to QQ number 1010553892 10 seconds after LangBot starts
async def initialize(self):
print(self.host.get_platform_adapters())
async def send_message():
print("send message start waiting")
await asyncio.sleep(10)
try:
await self.host.send_active_message(
adapter=self.host.get_platform_adapters()[0],
target_type="person",
target_id="1010553892",
message=platform_types.MessageChain([
platform_types.Plain("hello, world!")
])
)
except Exception as e:
traceback.print_exc()
print("send message end")
asyncio.get_running_loop().create_task(send_message())
```
+300
View File
@@ -0,0 +1,300 @@
# Plugin Common APIs
LangBot provides a series of APIs for plugins to operate various LangBot modules and control message contexts.
:::info Table of Contents
[[toc]]
:::
## Request API
Operations for the currently processing user request (message). Only available in `EventListener` and `Command` components. Access methods:
- In event handler methods of `EventListener`: Internal methods of `event_context: context.EventContext` object
- In subcommand handler methods of `Command`: Internal methods of `context: ExecuteContext` object
### Direct Reply Message
Directly reply with a message chain to the session where the current request is located.
For message chain construction methods, please refer to [Message Platform Entities](/en/plugin/dev/apis/messages).
```python
async def reply(
self, message_chain: platform_message.MessageChain, quote_origin: bool = False
):
"""Reply to the message request
Args:
message_chain (platform.types.MessageChain): LangBot message chain
quote_origin (bool): Whether to quote the original message
"""
# Usage example
await event_context.reply(
platform_message.MessageChain([
platform_message.Plain(text="Hello, world!"),
]),
)
```
### Get Bot UUID
Get the UUID of the bot that originated the current request.
```python
async def get_bot_uuid(self) -> str:
"""Get the bot uuid"""
# Usage example
bot_uuid = await event_context.get_bot_uuid()
```
### Set Request Variables
Some information in a single request is stored in request variables. When using external LLMOps platforms like Dify, [these variables are explicitly passed to LLMOps platforms](/en/deploy/pipelines/readme.html#request-variables).
```python
async def set_query_var(self, key: str, value: Any):
"""Set a query variable"""
# Usage example
await event_context.set_query_var("key", "value")
```
### Get Request Variables
Get a single request variable.
```python
async def get_query_var(self, key: str) -> Any:
"""Get a query variable"""
# Usage example
query_var = await event_context.get_query_var("key")
```
### Get All Request Variables
```python
async def get_query_vars(self) -> dict[str, Any]:
"""Get all query variables"""
# Usage example
query_vars = await event_context.get_query_vars()
```
## LangBot API
These APIs can be called in any plugin component. Access methods:
- In the plugin root directory `main.py`: Internal methods of the `self` object, these APIs are all provided by the plugin class parent class `BasePlugin`.
- In any plugin component class: Internal methods of the `self.plugin` object.
### 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] = {},
) -> 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={},
)
```
### 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")
```
+96
View File
@@ -0,0 +1,96 @@
# Message Platform Entities
:::info Table of Contents
[[toc]]
:::
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:
![](/assets/image/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.platform.message`.
### Constructing Message Chains
Please first import the `langbot_plugin.api.entities.platform.message` package to use the message components within it.
```python
from langbot_plugin.api.entities.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.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.platform.entities`.
## Accessing Underlying Platform APIs
:::warning Note
Not applicable for 4.x.
:::
To abstract away platform differences, LangBot provides an abstraction layer above messaging platform APIs, which includes the content described above and various platform adapters in the `pkg/platform/sources` directory.
However, due to significant platform differences, the entity and API encapsulation in the abstraction layer is not complete. If your plugin needs to access platform-specific functionality, you can access underlying APIs according to the following instructions.
:::info
The `adapter` here is the instantiated object of each adapter, corresponding to the `xxxAdapter(adapter.MessagePlatformAdapter)` classes in files under `pkg/platform/sources/`. It can be obtained from `query.adapter` in plugin events, for example:
```python
# For example in a plugin
@handler(GroupMessageReceived)
async def _(self, ctx: EventContext):
# Get adapter object
adapter = ctx.query.adapter
```
:::
| Platform | Adapter | Access Method | Description |
| --- | --- | --- | --- |
| OneBot v11| aiocqhttp | adapter.bot | bot object corresponds to [aiocqhttp](https://github.com/nonebot/aiocqhttp) CQHttp object, please refer to aiocqhttp documentation for detailed usage |
| QQ Official API | qqofficial | adapter.bot | bot object is the SDK object under libs/qq_official_api, for detailed usage please refer to the `send_group_text_msg` method in `QQOfficialClient` in libs/qq_official_api/api.py for HTTP request methods, please refer to QQ Official API documentation for specific interface documentation |
| WeCom (Enterprise WeChat) | wecom | adapter.bot | Refer to the `send_image` method in `WecomClient` in libs/wecom_api/api.py for HTTP request methods, please refer to WeCom API documentation for specific interface documentation |
| WeChat Official Account | officialaccount | - | Please refer to WeChat Official Account API documentation for interface documentation, credential information can be obtained from adapter.bot |
| Lark | lark | adapter.api_client | Lark SDK's API Client object, please refer to [oapi-sdk-python](https://github.com/larksuite/oapi-sdk-python) for details |
| DingTalk | dingtalk | - | Please refer to DingTalk API documentation, credential information can be obtained from adapter.bot |
| Discord | discord | adapter.bot | bot object is [Rapptz/discord.py](https://github.com/Rapptz/discord.py) Discord object, please refer to documentation for detailed usage |
| Telegram | telegram | adapter.bot | bot object is [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) Telegram object, please refer to documentation for detailed usage |
| Slack | slack | adapter.bot | bot object in libs/slack_api/api.py, please refer to its code and Slack SDK for detailed usage |
+218
View File
@@ -0,0 +1,218 @@
# Pipeline Events
:::info Table of Contents
[[toc]]
:::
LangBot plugins can register and handle pipeline events. For usage instructions, see [Component: Event Listener](/en/plugin/dev/components/event-listener).
## *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"""
message_chain: platform_message.MessageChain = pydantic.Field(
serialization_alias="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_chain: platform_message.MessageChain = pydantic.Field(
serialization_alias="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_id: typing.Union[int, str]
sender_id: typing.Union[int, str]
text_message: str
"""Message text"""
alter: typing.Optional[str] = None
"""Modified message text"""
reply: typing.Optional[list] = None
"""Reply message component list"""
class GroupNormalMessageReceived(BaseEventModel):
"""Triggered when a group chat normal message that should be processed is determined"""
event_name: str = "GroupNormalMessageReceived"
launcher_type: str
launcher_id: typing.Union[int, str]
sender_id: typing.Union[int, str]
text_message: str
"""Message text"""
alter: typing.Optional[str] = None
"""Modified message text"""
reply: typing.Optional[list] = None
"""Reply message component list"""
```
## *CommandSent
:::warning Note
No longer recommended for use, please use [Component: Command](/en/plugin/dev/components/command) instead.
:::
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"""
alter: typing.Optional[str] = None
"""Modified complete command text"""
reply: typing.Optional[list] = None
"""Reply message component list"""
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"""
alter: typing.Optional[str] = None
"""Modified complete command text"""
reply: typing.Optional[list] = None
"""Reply message component list"""
```
## 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"""
reply: typing.Optional[list] = None
"""Reply message component list"""
```
## 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
default_prompt: list[provider_message.Message]
"""Scenario preset for this conversation, can be modified"""
prompt: list[provider_message.Message]
"""Existing message records for this conversation, can be modified"""
```
+11
View File
@@ -0,0 +1,11 @@
# Plugin Technical Details
## Architecture
<img width="600" src="/assets/image/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.
+157
View File
@@ -0,0 +1,157 @@
# Completing Plugin 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
```
Plugin multilingual support follows the [RFC 4646](https://datatracker.ietf.org/doc/html/rfc4646) standard. Currently supported languages include:
- `en_US` English
- `zh_Hans` Simplified Chinese
- `zh_Hant` Traditional Chinese
- `ja_JP` Japanese
## 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, 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.
- name: mode
type: select
...
```
The supported types and fields for configuration items are as follows:
### type: string
String type.
```yaml
- name: api_key
type: string
...
```
### 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.
```yaml
- name: mode
type: select
options: # Dropdown options, supports multilingual.
- name: mode1 # Value
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
...
```
### 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: 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
...
```
## 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.
+71
View File
@@ -0,0 +1,71 @@
# Adding Components
Plugins consist of one or more components that provide different functionalities to LangBot. Currently supported component types include:
- 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.
## 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
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.
:::
## Component Types
For detailed usage of each component, please refer to:
- [EventListener](./event-listener.md)
- [Command](./command.md)
- [Tool](./tool.md)
+211
View File
@@ -0,0 +1,211 @@
# 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="/assets/image/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://docs.langbot.app/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
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="/assets/image/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)
## What's Next
You have learned the basic information about command registration and command execution. Next, you can:
- Check out [Plugin Common APIs](/en/plugin/dev/apis/common)
@@ -0,0 +1,101 @@
# Component: Event Listener
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://docs.langbot.app/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`.
## What's Next
You have learned the basic information about event listeners. Next, you can:
- Check the `List of Monitorable Events` and `Event Context APIs` in the documentation [Pipeline Events](/en/plugin/dev/apis/pipeline-events)
- Check out [Plugin Common APIs](/en/plugin/dev/apis/common)
+172
View File
@@ -0,0 +1,172 @@
# 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://docs.langbot.app/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
class GetWeatherAlerts(Tool):
async def call(self, params: dict[str, Any]) -> dict[str, Any]:
"""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).
:::
```python
from __future__ import annotations
from typing import Any
from langbot_plugin.api.definition.components.tool.tool import Tool
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]) -> dict[str, Any]:
"""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="/assets/image/zh/plugin/dev/components/tool_use_in_chat.png" />
## What's Next
You have learned the basic usage of tools. Next, you can:
- Check out [Plugin Common APIs](/en/plugin/dev/apis/common)
-112
View File
@@ -1,112 +0,0 @@
# Message Platform Entities
:::info Table of Contents
[[toc]]
:::
LangBot supports multiple messaging platforms, but the `message entity` format of each platform is different. To shield these differences, LangBot has a unified standard. Plugin developers only need to master and use the message entities described on this page in their plugins, and LangBot's internal message processing logic can automatically complete the parsing and conversion of messages.
:::info
LangBot's message platform entities are modified based on the implementation of [YiriMirai](https://github.com/YiriMiraiProject/YiriMirai).
:::
## Message Chain
The `message` of a message platform is different from the message used for AI interaction. The message of a message platform is described in the form of a `message chain`. Each independent message displayed on QQ is a message chain, which can contain multiple `message chain components` such as `text`, `image`, `@ component`, for example:
![](/assets/image/zh/plugin/dev/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 chain and message chain components are located in `pkg/platform/types/messages.py`.
### Constructing Message Chain
Please import the `pkg.platform.types` package first to use the message components.
```python
from pkg.platform.types import *
# Build a message containing the text Hello LangBot and an image (from URL)
msg_chain = MessageChain([
Plain("Hello LangBot"),
Image(url='https://qchatgpt.rockchin.top/langbot-logo.png')
])
# Build a message containing @all and the text Hello LangBot
msg_chain = MessageChain([
AtAll(),
Plain("Hello LangBot")
])
# Build a message containing @specific member and the text Hello LangBot
msg_chain = MessageChain([
At(123456),
Plain("Hello LangBot")
])
```
Currently supported message chain components:
- `Source` Source message chain information, if it is a message received from a message platform, this component will be included at the beginning of the message chain, recording message information
- `Plain` Plain text message
- `Quote` Quote message
- `Image` Image message
- `AtAll` @all members message
- `At` @specific member message
- `Voice` Voice message
- Need to check message platform support
- `Forward` Forward message
- Not supported on many platforms, not recommended to use
- `File` File message
For specific usage, please check the definitions in the source code `pkg/platform/types/messages.py`.
## Message Platform Events
These events are different from LangBot plugin events, they are an independent set of event systems for the message platform system. These events are the source events of LangBot messages, which can be obtained from each Query.message_event.
```python
# For example in a plugin
@handler(GroupMessageReceived)
async def _(self, ctx: EventContext):
# Get the message platform event
message_event = ctx.query.message_event
```
Message platform events are defined in `pkg/platform/types/events.py`.
## Other Entities
In addition to the above entities, there are also entities such as `Friend` and `Group`, which are defined in `pkg/platform/types/entities.py`.
They are generally included in the above event objects, from which you can get some information not included in the plugin events.
## Accessing Message Platform Low-Level API
In order to smooth out the differences between various platforms, LangBot provides an abstraction layer on top of the message platform APIs, namely the content described above and the platform adapters in the `pkg/platform/sources` directory.
However, due to the large differences between platforms, the entities and API encapsulation in the abstraction layer are not complete. If your plugin needs to access specific platform functions, you can access the low-level API according to the following instructions.
:::info
The `adapter` here is the instantiated object of each adapter, corresponding to the `xxxAdapter(adapter.MessagePlatformAdapter)` class in each file under `pkg/platform/sources/`. It can be obtained from the plugin event's `query.adapter`, for example:
```python
# For example in a plugin
@handler(GroupMessageReceived)
async def _(self, ctx: EventContext):
# Get the adapter object
adapter = ctx.query.adapter
```
:::
| Platform | Adapter | Access Method | Description |
| --- | --- | --- | --- |
| OneBot v11| aiocqhttp | adapter.bot | The bot object corresponds to the CQHttp object of [aiocqhttp](https://github.com/nonebot/aiocqhttp), please refer to the aiocqhttp documentation for detailed usage |
| QQ Official API | qqofficial | adapter.bot | The bot object is the SDK object under libs/qq_official_api, for detailed usage, you can refer to the way of initiating HTTP requests in the `send_group_text_msg` method in `QQOfficialClient` in libs/qq_official_api/api.py, please check the QQ official API documentation for specific interface documentation|
| WeCom | wecom | adapter.bot | Refer to the way of initiating HTTP requests in the `send_image` method in `WecomClient` in libs/wecom_api/api.py, please check the WeCom API documentation for specific interface documentation |
| WeChat Official Account | officialaccount | - | Please refer to the interface documentation information in the WeChat Official Account API documentation, the relevant credential information can be obtained from adapter.bot |
| Feishu | lark | adapter.api_client | Feishu SDK's API Client object, please refer to [oapi-sdk-python](https://github.com/larksuite/oapi-sdk-python) |
| DingTalk | dingtalk | - | Please refer to the DingTalk API documentation, the relevant credential information can be obtained from adapter.bot |
| Discord | discord | adapter.bot | The bot object is the Discord object of [Rapptz/discord.py](https://github.com/Rapptz/discord.py), please refer to the documentation for detailed usage |
| Telegram | telegram | adapter.bot | The bot object is the Telegram object of [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot), please refer to the documentation for detailed usage |
| Slack | slack | adapter.bot | The bot object is the object in libs/slack_api/api.py, please refer to its code and Slack SDK for detailed usage |
+27
View File
@@ -0,0 +1,27 @@
# 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
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)
:::
- 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
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)
:::
- 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.
+65
View File
@@ -0,0 +1,65 @@
# Publish Plugin
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="/assets/image/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
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="/assets/image/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, 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="/assets/image/zh/plugin/dev/plugin_submission.png" />
If you have any questions, you can send an email to `[email protected]` or contact us through [Community](/en/insight/community).
+94 -269
View File
@@ -1,308 +1,133 @@
# Plugin Development Tutorial
> Please read the [Plugin Introduction page](/en/plugin/plugin-intro) first
> It is recommended to read the source code of this project first to understand the project architecture
## Architecture Foundation
> For questions and requirements, please open an issue in the repository
> **Try to solve problems on your own before asking**
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.
:::info Table of Contents
[[toc]]
<img width="600" src="/assets/image/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
└── requirements.txt # Plugin dependencies file
```
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
- 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.
:::
## 💬 Introduction
## Installing CLI
This page explains the steps for developing a regular plugin. Plugins have two methods: event listeners and content functions.
Event listeners can respond to certain events when triggered and change behavior. Content functions can be called by large language models, please check the plugin introduction page for details.
Please ensure you have Python 3.10 or higher installed, and have installed the [uv package manager](https://docs.astral.sh/uv/).
## 💻 Quick Start
Use [HelloPlugin](https://github.com/langbot-app/HelloPlugin) as a template to generate a plugin code repository, then clone the repository code to the `plugins` directory.
Modify the parts that need to be changed in the plugin's README.md file.
Edit `main.py` and enter the following content:
```Python
from pkg.plugin.context import register, handler, llm_func, BasePlugin, APIHost, EventContext
from pkg.plugin.events import * # Import event classes
"""
When receiving a private chat or group chat message "hello", reply with "hello, <sender id>!" or "hello, everyone!"
"""
class HelloPlugin(BasePlugin):
# Triggered when the plugin is loaded
def __init__(self, host: APIHost):
pass
# Asynchronous initialization
async def initialize(self):
pass
# Triggered when a personal message is received
@handler(PersonNormalMessageReceived)
async def person_normal_message_received(self, ctx: EventContext):
msg = ctx.event.text_message # Here event is the PersonNormalMessageReceived object
if msg == "hello": # If the message is hello
# Output debug information
self.ap.logger.debug("hello, {}".format(ctx.event.sender_id))
# Reply with message "hello, <sender id>!"
ctx.add_return("reply", ["hello, {}!".format(ctx.event.sender_id)])
# Prevent the default behavior of this event (getting a reply from the API)
ctx.prevent_default()
# Triggered when a group message is received
@handler(GroupNormalMessageReceived)
async def group_normal_message_received(self, ctx: EventContext):
msg = ctx.event.text_message # Here event is the GroupNormalMessageReceived object
if msg == "hello": # If the message is hello
# Output debug information
self.ap.logger.debug("hello, {}".format(ctx.event.sender_id))
# Reply with message "hello, everyone!"
ctx.add_return("reply", ["hello, everyone!"])
# Prevent the default behavior of this event (getting a reply from the API)
ctx.prevent_default()
# Triggered when the plugin is unloaded
def __del__(self):
pass
Execute the command in any empty directory to install LangBot CLI and SDK:
```bash
pip install langbot_plugin==0.1.1b5
```
## 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
You can also use the `lbp init HelloPlugin` command to initialize the plugin in the subdirectory `HelloPlugin`.
Interpreting this plugin program
- `import pkg.plugin.context` imports `register (used to register plugin classes)`, `handler (used to register event listeners)`, `llm_func (used to register content functions)`, `BasePlugin (plugin base class)`, `APIHost (API host)`, `EventContext (event context class)` and other content
- `import pkg.plugin.events` imports all supported event classes
- Declare class `HelloPlugin` inheriting from `BasePlugin`, this class can be named arbitrarily, the plugin name is only related to the parameter when calling `register`
- Declare the `__init__` method of this class, this method is optional, the code in it will be executed when the main program loads the plugin at startup
- The plugin class also supports adding an asynchronous method `async def initialize(self)` for asynchronous initialization
- `@handler` marks the method `person_normal_message_received` as an event listener, handling the `PersonNormalMessageReceived` (triggered when a private chat message is received and before getting an OpenAI reply) event, this method can be named arbitrarily, the bound event is only related to the parameter in `handler`, more supported events can be found in the `pkg.plugin.events` file or check the `API` section below
- Output debug information through the `self.ap.logger` logger, the `ap` object in the plugin class parent class is the context object of the entire program, you can access all objects in the program through this object
- The event listener method gets a second parameter `ctx` which is the context of this event, the `event` in it is the event object of this event, you can extract related parameters from it, for specific parameters that can be obtained from each event, please check the comments of each event class in the `pkg.plugin.events` file
- The event listener method extracts the `text_message` parameter from the parameters, checks if it is `hello`, if so, sets the return value `reply` to `["hello, {}!".format(ctx.event.sender_id)]`, then calls the `ctx.prevent_default()` method to prevent the original program's default behavior
- For return values supported by each event, please check the comments of each event in `pkg.plugin.events`
- Register the `GroupNormalMessageReceived` event with similar code to handle group messages
After writing and saving, restart the main program, go to the WebUI plugin page to see the plugin
<img width="600" src="/assets/image/zh/plugin/dev/create_plugin.png" />
:::
This plugin will implement: replying with `hello, <sender QQ number>!` when receiving a `hello` message in private chat, and replying with `hello, everyone!` when receiving a `hello` message in group chat.
### Writing the Manifest File
Starting from LangBot 4.0, manifest files will be used to register plugins.
Please create a new file `manifest.yaml` in the same directory as main.py. If it already exists, please modify it according to the format below.
```yaml
apiVersion: v1 # Do not change
kind: Plugin # Do not change
metadata:
# author and name uniquely identify a plugin
author: langbot # Plugin author, change to your name
name: Hello # Plugin name, change to your plugin name
repository: 'https://github.com/langbot-app/HelloPlugin' # Plugin repository address, change to your plugin GitHub repository address
version: 0.1.0 # Plugin version, change to your plugin version
description: # Plugin description, change to your plugin description, supports multiple languages. Language codes follow the RFC 4646 standard.
en_US: Plugin for sending hello
zh_Hans: 示例插件
label: # Plugin display name, supports multiple languages. The WebUI will display the label in the corresponding language. Language codes follow the RFC 4646 standard.
en_US: Hello
zh_Hans: Hello
spec:
# Plugin configuration (optional), can configure multiple items
config:
- name: github_token # Configuration item name
label: # Configuration item display name, supports multiple languages. Language codes follow the RFC 4646 standard.
en_US: Github Token
zh_Hans: Github Token
description: # Configuration item description, supports multiple languages. Optional.
en_US: Image downloading requires a Github token
zh_Hans: 如果未填写,图片下载可能会失败
type: string # Configuration item type, supports string, integer, float, boolean, etc.
default: '' # Configuration item default value
required: false # Whether the configuration item is required
execution:
python:
path: main.py # Plugin main program path, must be the same as the file name of the plugin entry code above
attr: HelloPlugin # Plugin class name, must be the same as the class name declared in the code above
```
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
Configuration item supported types:
If you get a "command not found" error for `lbp`, it may be because you haven't properly set the `PATH` environment variable.
You can use `python -m langbot_plugin.cli.__init__` instead of the `lbp` command.
- string: String
- integer: Integer
- float: Floating point number
- boolean: Boolean value
- array[string]: String array
- select: Dropdown box, can select one option from multiple options
- Need to configure the options option, indicating the options in the dropdown box
For example:
```yaml
- name: mode
label:
en_US: Mode
zh_Hans: 模式
type: select
options:
- name: mode1
label:
en_US: Mode 1
zh_Hans: 模式 1
- name: mode2
label:
en_US: Mode 2
zh_Hans: 模式 2
```
- prompt-editor: Prompt editor.
- llm-model-selector: LLM model selector.
After the user modifies the configuration items through the WebUI, the plugin can get the values of the configuration items in self.config.
```bash
python -m langbot_plugin.cli.__init__ init HelloPlugin
cd HelloPlugin
```
:::
## Starting Debug Mode
## ❗ Standards (Important)
You need to first deploy and start LangBot, ensuring that Plugin Runtime is running and listening on port `5401`.
- Please keep each plugin in a separate directory for management, it is recommended to create a repository on Github to store a single plugin for easy retrieval and updates
- Naming conventions (important):
- Plugin author: Use English, case insensitive, such as `RockChinQ`
- Plugin name: Use `PascalCase`, such as `Hello`, `ExamplePlugin`, `ChineseCommands`, etc.
- Plugin repository: Please use PascalCase, it is recommended to be the same as the plugin name, such as `HelloPlugin`
- Plugin description: It is recommended to include the phrase `LangBot Plugin`
- Multiple Python program files can be stored in one directory to separate the various functions of the plugin for easy management by developers, but it is not recommended to register multiple plugins in one directory
- Please specify the dependencies required by the plugin in the `requirements.txt` file in the plugin directory, the program will automatically install the dependencies when obtaining this plugin from the repository
::: info
Runtime has two startup modes:
## 🪝 Content Functions
- `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.
`Content functions` implemented through [GPT's Function Calling capability](https://platform.openai.com/docs/guides/gpt/function-calling), which is a function embedded in the conversation and automatically called by GPT.
- `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`.
- 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).
> Your plugin does not necessarily have to include content functions, please check the content function page to understand this feature first
Whether in `stdio` or `websocket` mode, Plugin Runtime will listen on port `5401` of its host as a debug port for plugin debugging connections.
### Content Function Writing Steps
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.
:::
1️⃣ Please first follow the steps above to write your plugin's basic structure, now please delete (of course you can also keep them, just for simplicity) the various class functions decorated by `@handler` in the above plugin content
Copy the `.env.example` file in the plugin directory to `.env`, and check or modify `DEBUG_RUNTIME_WS_URL` to your Plugin Runtime's WebSocket address.
<details>
<summary>Class structure after deletion</summary>
```python
class HelloPlugin(Plugin):
# Triggered when the plugin is loaded
def __init__(self, plugin_host: APIHost):
pass
# Triggered when the plugin is unloaded
def __del__(self):
pass
```bash
cp .env.example .env
```
</details>
2️⃣ Now we add the following function to the position of the functions we just deleted (as a class method)
```Python
# Function to add
@llm_func(name="access_the_web") # Set function name
async def access_web(self, query, url: str):
"""Call this function to search about the question before you answer any questions.
- Do not search through baidu.com at any time.
- If you need to search somthing, visit https://www.google.com/search?q=xxx.
- If user ask you to open a url (start with http:// or https://), visit it directly.
- Summary the plain content result by yourself, DO NOT directly output anything in the result you got.
Args:
url(str): url to visit
Returns:
str: plain text content of the web page
"""
import requests
from bs4 import BeautifulSoup
# You need to first use
# pip install beautifulsoup4
# to install dependencies
r = requests.get(
url,
timeout=10,
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.183"
}
)
soup = BeautifulSoup(r.text, 'html.parser')
s = soup.get_text()
# Remove extra blank lines or lines with only \t and spaces
s = re.sub(r'\n\s*\n', '\n', s)
if len(s) >= 512: # Truncate the first 512 characters of the plain text content of the web page
return s[:512]
return s
Start plugin debugging, and you will see the plugin output:
```bash
lbp run
```
#### Please note:
<img width="600" src="/assets/image/zh/plugin/dev/launch_debug_plugin.png" />
- The function's comments must be strictly written according to the required format, please check [this document](https://github.com/RockChinQ/CallingGPT/wiki/1.-Function-Format#function-format) for the specific format
- Content functions and `event listeners decorated with @handler` can coexist in the same plugin and are both controlled by the plugin switch
- Make sure that the model you are using supports the function calling feature
And you can see this plugin has been loaded in LangBot's WebUI.
3️⃣ Now your program has web access capability, restart the program, ask the bot about online content or directly send article links requesting summaries.
<img width="600" src="/assets/image/zh/plugin/dev/debug_plugin_loaded.png" />
- This is just an example, for more efficient web access capability support plugin, please check [WebwlkrPlugin](https://github.com/RockChinQ/WebwlkrPlugin)
## What's Next
## 📦 Publishing to the Plugin Market
This tutorial will guide you through step-by-step completion of plugin functionality.
When your plugin development is complete, please upload it to GitHub and [open an issue in the LangBot repository](https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6). After review, it will be added to the plugin market and will be visible on the LangBot WebUI plugin page's plugin market in about half an hour.
![](/assets/image/zh/plugin/dev/plugin_dev_tutor_01.png)
## 📄 Glossary
### Description
The `ctx: EventContext` object obtained by the event listener method can be viewed in the `pkg.plugin.context` module for its structure. The `event` attribute of the `ctx` object is the event object of this event, from which you can extract related parameters. For specific parameters that can be obtained from each event, please check the comments of each event class in the `pkg.plugin.events` file.
Event return values are all **optional**, you can submit return values by calling `ctx.add_return(key: str, ret)`
### Events
If an event is an event in a request (user sending a message), its event object will contain a `query` object, which includes the context data in the processing of this request (i.e., the process of handling a user sending a message, a request process may trigger multiple events).
For all supported events, please check the comments of each event class in the `pkg.plugin.events` file.
For `message chain components`, please check [Message Platform Entities](./messages)
### BasePlugin Structure
#### self.ap
Accessing `self.ap` in the plugin's methods is an object of the `pkg.core.app.Application` class, which contains the context object of the entire program, you can access all objects in the program through this object.
#### self.host
Accessing `self.host` in the plugin's methods is an object of the `pkg.plugin.context.APIHost` class, which provides some APIs of the main program, please check its source code for details.
### API Reference
For APIs that plugins can call, please check [API Reference](./api-ref)
### Plugin Development Group
Plugin development, publishing, and discussion
- LangBot Plugin Development: 1032327138 ([Link](https://qm.qq.com/q/G7ENGTjeou))
- 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.