mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-05 07:12:02 +08:00
Add 'docs/' from commit '51dddcb3f891d60bc38645e88b2dbd515344da81'
git-subtree-dir: docs git-subtree-mainline:0582d1b3d4git-subtree-split:51dddcb3f8
This commit is contained in:
@@ -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) |
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
@@ -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.
|
||||
@@ -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))
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||

|
||||
```
|
||||
|
||||
**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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user