Skip to main content

langbot-plugin-dev

Develop, debug, and test LangBot plugins. Use when creating new LangBot plugins, fixing plugin bugs, setting up a LangBot test environment, or testing plugins via WebSocket. Covers plugin component architecture (EventListener, Command, Tool), the plugin SDK API (invoke_llm, get_llm_models, send_message, plugin storage), common pitfalls, and automated WebSocket-based testing. Triggers on "langbot plugin", "lbp", "GroupChatSummary", "plugin debug", "langbot test".

来源信息

仓库
langbot-app/LangBot
最近来源活动
2026年7月19日 14:45
检测到的 SKILL.md 语言
英语
星标
18,104
分支
1,629

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
2 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
langbot-plugin-dev
description
Develop, debug, and test LangBot plugins. Use when creating new LangBot plugins, fixing plugin bugs, setting up a LangBot test environment, or testing plugins via WebSocket. Covers plugin component architecture (EventListener, Command, Tool), the plugin SDK API (invoke_llm, get_llm_models, send_message, plugin storage), common pitfalls, and automated WebSocket-based testing. Triggers on "langbot plugin", "lbp", "GroupChatSummary", "plugin debug", "langbot test".
# LangBot Plugin Development & Debugging ## Controlling a running instance via MCP Beyond writing code, you can **drive a live LangBot instance over MCP** — no raw HTTP needed. Two MCP servers exist (both reuse existing API keys; see `AGENTS.md`): - **LangBot instance** — `http://<host>:5300/mcp` (auth: web-UI `lbk_` key or the `api.global_api_key` from `config.yaml`). Manage bots, pipelines, models, knowledge bases, and skills. See the **`langbot-mcp-ops`** skill. - **LangBot Space marketplace** — `https://space.langbot.app/mcp` (auth: Personal Access Token). Search plugins / MCP servers / skills. See the **`langbot-space-ops`** skill. > Any change to an agent-accessible HTTP API endpoint must keep the matching MCP > tool and these skills in sync. ## Plugin Architecture A LangBot plugin consists of: ``` MyPlugin/ ├── manifest.yaml # Plugin metadata, config schema ├── main.py # BasePlugin subclass (entry point, shared state) ├── components/ │ ├── event_listener/ # Hook pipeline events │ │ ├── collector.yaml │ │ └── collector.py │ ├── commands/ # !command handlers │ │ ├── mycommand.yaml │ │ └── mycommand.py │ └── tools/ # LLM function-call tools │ ├── mytool.yaml │ └── mytool.py ``` Each component has a `.yaml` (metadata) and `.py` (implementation). ## README & i18n convention (enforced on the marketplace) A plugin published to LangBot Space serves a localized README on its detail page. The resolver (`langbot-space` `PluginService.GetPluginREADME`) works like this: - **Root `README.md` MUST be in English.** It is the default and the fallback — when no per-language README matches the viewer's locale, the page serves the root `README.md`. A non-English root README makes the English/default view show the wrong language. - **All other languages live under `readme/README_{lang}.md`** — e.g. `readme/README_zh_Hans.md`, `readme/README_ja_JP.md`. The 8 supported locales: `en_US, zh_Hans, zh_Hant, ja_JP, th_TH, vi_VN, es_ES, ru_RU`. - `manifest.yaml` `metadata.label` / `metadata.description` should carry the same 8-locale i18n set (`repository` must be a real, alive URL). ``` MyPlugin/ ├── manifest.yaml ├── README.md # English (default + fallback) — REQUIRED, must be English └── readme/ ├── README_zh_Hans.md ├── README_zh_Hant.md ├── README_ja_JP.md ├── README_th_TH.md ├── README_vi_VN.md ├── README_es_ES.md └── README_ru_RU.md ``` `manifest.yaml` (incl. `repository`) is the source of truth — the marketplace syncs from it, so edit the package and re-publish rather than patching live data. ## Critical SDK Pitfalls ### 1. MessageChain is a RootModel — iterate directly ```python # ❌ WRONG — MessageChain has no .components attribute for component in event.message_chain.components: # ✅ CORRECT — MessageChain is a Pydantic RootModel, iterate directly for component in event.message_chain: ``` ### 2. Message.content must be `list[ContentElement]` or `str`, not a single ContentElement ```python from langbot_plugin.api.entities.builtin.provider import message as provider_message # ❌ WRONG — single ContentElement Message(role="user", content=ContentElement.from_text("hello")) # ✅ CORRECT — list of ContentElement Message(role="user", content=[ContentElement.from_text("hello")]) # ✅ ALSO CORRECT — plain string Message(role="user", content="hello") ``` ### 3. invoke_llm does NOT accept timeout ```python # ❌ WRONG await self.invoke_llm(llm_model_uuid=uuid, messages=msgs, timeout=60) # ✅ CORRECT await self.invoke_llm(llm_model_uuid=uuid, messages=msgs) ``` ### 4. invoke_llm response.content can be str OR list ```python response = await self.invoke_llm(...) if response.content: if isinstance(response.content, str): return response.content elif isinstance(response.content, list): parts = [e.text for e in response.content if hasattr(e, "text") and e.text] return "\n".join(parts) ``` ### 5. get_llm_models() returns UUIDs ```python # Returns list[str] of model UUIDs models = await self.get_llm_models() model_uuid = models[0] # First available model UUID ``` **Known bug (v4.9.3):** The host handler may return `list[dict]` instead of `list[str]`. If you hit `TypeError: unhashable type: 'dict'` in `invoke_llm`, the fix is in `LangBot/src/langbot/pkg/plugin/handler.py` — change `'llm_models': llm_models` to `'llm_models': [m['uuid'] for m in llm_models]`. ### 6. invoke_llm parameter is `llm_model_uuid`, NOT `model_uuid` ```python # ❌ WRONG — will throw "got an unexpected keyword argument" await self.invoke_llm(messages=msgs, model_uuid=uuid) # ✅ CORRECT await self.invoke_llm(messages=msgs, llm_model_uuid=uuid) ``` ### 7. prevent_default() alone does NOT block LLM response To fully prevent the default LLM pipeline from responding when your EventListener handles the message, you must call **both**: ```python event_context.prevent_default() # Block default behavior event_context.prevent_postorder() # Block subsequent plugins/pipeline ``` Using only `prevent_default()` still allows the LLM to generate a response. ### 8. get_plugin_storage / set_plugin_storage may throw KeyError: 'owner' This is a version mismatch between the SDK and host. Wrap storage calls in try/except: ```python try: data = await self.get_plugin_storage("my_key") except Exception: data = None # Fallback gracefully ``` ### 9. Component YAML must have full structure, not just name/description ```yaml # ❌ WRONG — will silently fail to register the component name: translator description: en_US: 'Does stuff' # ✅ CORRECT — full component YAML apiVersion: v1 kind: EventListener metadata: name: translator label: en_US: Translator spec: execution: python: path: translator.py attr: Translator ``` ### 10. BasePlugin import path ```python # ❌ WRONG from langbot_plugin.api.definition.base_plugin import BasePlugin # ✅ CORRECT from langbot_plugin.api.definition.plugin import BasePlugin ``` ## Pipeline Events Events the EventListener can hook (from most general to most specific): | Event | When | |---|---| | `GroupMessageReceived` | **Any** group message arrives (before trigger rules) | | `PersonMessageReceived` | **Any** private message arrives | | `GroupNormalMessageReceived` | Group message passes trigger rules, going to LLM | | `PersonNormalMessageReceived` | Private message going to LLM | | `GroupCommandSent` | Group message matched as command | | `PersonCommandSent` | Private message matched as command | | `NormalMessageResponded` | LLM generated a response | | `PromptPreProcessing` | About to build LLM context | **Key insight:** `*MessageReceived` fires for ALL messages regardless of trigger rules. `*NormalMessageReceived` only fires for messages that match the pipeline's trigger rules (e.g., @bot, prefix, random%). Use `*MessageReceived` for message collection/logging. ## EventContext API ```python @self.handler(events.GroupMessageReceived) async def on_msg(event_context: context.EventContext): event = event_context.event event.launcher_id # Group ID event.sender_id # Sender ID event.message_chain # MessageChain (iterate directly) # Reply to the current conversation await event_context.reply(MessageChain([Plain(text="hello")])) # Block default pipeline behavior event_context.prevent_default() # Block subsequent plugins event_context.prevent_postorder() ``` ## Setting Up a Test Environment ### Deploy via Docker (GitOps + Portainer) See `references/test-env-setup.md` for full deployment steps. Quick summary: 1. Create `docker-compose.yaml` in `server-deploy` repo 2. Deploy via Portainer git repository method 3. Set up admin account via `/api/v1/user/init` POST 4. Configure LLM provider and model via API 5. Copy plugin to `data/plugins/` directory ### WebSocket Testing LangBot's WebUI chat uses WebSocket. Connect to test message flow: ``` ws://<host>:<port>/api/v1/pipelines/<pipeline_uuid>/ws/connect?session_type=group ``` - `session_type=group` for group chat simulation - `session_type=person` for private chat (always triggers pipeline) **Requires Origin header** to pass CORS: ```javascript const ws = new WebSocket(url, { headers: { Origin: 'https://your-langbot-domain' } }); ``` Send messages: ```json {"type": "message", "message": [{"type": "Plain", "text": "hello"}]} ``` Receive: - `{"type": "connected", ...}` — connection established - `{"type": "user_message", "data": {...}}` — echo of sent message - `{"type": "response", "data": {"content": "...", "is_final": true/false}}` — bot reply (streamed) ### Group Trigger Rules Group messages only enter the pipeline if trigger rules are met: ```json { "group-respond-rules": { "at": true, // Respond when @bot "prefix": ["ai"], // Respond to messages starting with "ai" "random": 0.0, // Probability of responding to any message (0.0-1.0) "regexp": [] // Regex patterns } } ``` For testing, set `random: 1.0` via PUT `/api/v1/pipelines/<uuid>` to respond to all messages. **Important:** EventListener hooks like `GroupMessageReceived` fire regardless of trigger rules. Only the LLM processing (`GroupNormalMessageReceived` and beyond) requires trigger rules. ### Plugin Hot-Reload There is **no hot-reload**. After changing plugin files: ```bash docker restart <runtime-container> # Wait ~5 seconds for plugin to re-mount ``` The main LangBot container does NOT need restart for plugin changes — only the runtime container. ## API Quick Reference ### Admin Setup ```bash # Initialize admin account (first time only) curl -X POST $BASE/api/v1/user/init \ -H "Content-Type: application/json" \ -d '{"user":"admin@test.com","password":"test123"}' # Login curl -X POST $BASE/api/v1/user/auth \ -H "Content-Type: application/json" \ -d '{"user":"admin@test.com","password":"test123"}' # Returns: {"data":{"token":"eyJ..."}} ``` ### Provider & Model Setup ```bash # Create provider curl -X POST $BASE/api/v1/provider/providers \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"MyProvider","requester":"new-api-chat-completions","base_url":"https://api.example.com/v1","api_keys":["sk-xxx"]}' # Create LLM model curl -X POST $BASE/api/v1/provider/models/llm \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"gpt-4o-mini","provider_uuid":"<uuid>","abilities":["chat","tool-use"]}' # List models curl $BASE/api/v1/provider/models/llm -H "Authorization: Bearer $TOKEN" ``` ### Pipeline Config ```bash # Get pipeline curl $BASE/api/v1/pipelines -H "Authorization: Bearer $TOKEN" # Update pipeline (e.g., set model, modify trigger rules) curl -X PUT $BASE/api/v1/pipelines/<uuid> \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '<full pipeline JSON>' ``` ## Plugin Config Types Supported `type` values in `manifest.yaml` `spec.config`: | Type | Description | Value | |---|---|---| | `string` | Text input | string | | `int` / `integer` | Number input | int | | `float` | Decimal input | float | | `bool` / `boolean` | Toggle | bool | | `select` | Dropdown (needs `options`) | string | | `prompt-editor` | Multi-line prompt editor | string | | `llm-model-selector` | LLM model picker UI | UUID string | | `bot-selector` | Bot picker UI | UUID string | Example — let users choose which model the plugin uses: ```yaml spec: config: - name: model type: llm-model-selector label: en_US: 'LLM Model' zh_Hans: 'LLM 模型' description: en_US: 'Select the LLM model. Falls back to first available if not set.' zh_Hans: '选择 LLM 模型。未设置时使用第一个可用模型。' required: false ``` Read config in plugin code: ```python model_uuid = self.get_config().get("model")
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看