feat(plugins): consolidate official plugins with independent releases (LANG-180) (#2797)

Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
RockChinQ
2026-10-28 17:32:24 +08:00
committed by GitHub
co-authored by multica-agent
parent d099033d79
commit 3a77306c3c
1144 changed files with 142049 additions and 130 deletions
+3
View File
@@ -0,0 +1,3 @@
# This is a .env file example, please copy it to .env file.
# Please refer to https://langbot.app/docs/en/plugin/dev/tutor.html for more details.
DEBUG_RUNTIME_WS_URL=ws://localhost:5401/debug/ws
+176
View File
@@ -0,0 +1,176 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
/data/
+7
View File
@@ -0,0 +1,7 @@
# HelloPlugin
This plugin is an experimental plugin that provides demos on `EventListener`, `Tool` and `Command` components.
- `components/event_listener/default.py`: simply reply a message when a person message is received.
- `components/tools/get_weather_alerts.py`: provide a tool that can be used to get the weather alerts of US states.
- `components/commands/info.py`: respond to the command `info` with the context of the command execution.
@@ -0,0 +1,43 @@
# Shared runtime source candidate (SDK >=0.7.4)
This source declares `sharedRuntime: shared-runtime-v1` and `componentModel: stateless-v1`.
It is source-reviewed for the multi-tenant contract; it is **not certified** and makes no
claim of live tenant acceptance.
- Configuration is read per invocation with `self.get_config()` /
`self.get_plugin_config()`. HelloPlugin declares no config items (`spec.config: []`), so
the shared worker never reads tenant settings; nothing from an installation is copied
onto the plugin, the components or module globals.
- `initialize()` is empty, so a shared worker is never initialized with another
installation's settings.
## Per invocation
- `GetWeatherAlerts.call` builds the NWS URL from its arguments and creates a fresh
`httpx.AsyncClient` for every request (`components/tools/get_weather_alerts.py`); no
client, response or parameter is cached on the shared tool object.
- `DefaultEventListener`'s handler only assigns `event_context.event.user_message_alter`
on the event it received; it keeps no state across events.
- `Info` builds its reply from the `ExecuteContext` of the current command invocation.
- No SDK process-global settings are modified.
## Identity
No upstream/tenant identity is derived: the plugin exposes no per-user vendor state and
calls no identity-scoped Host API.
## Tests
`tests/test_shared_runtime.py` drives one plugin/component object graph across two
distinct `InstallationBinding` values with the real SDK `bind_invocation(...)` and
`InstallationBinding`, and asserts (a) the shared tool resolves the invocation config and
produces installation-independent results while retaining no attributes, and (b) the
listener mutates only the per-event context. HTTP replies are simulated; no live network
or vendor account is used.
## Known limits
- `spec.config` is empty: there are no installation settings to resolve, and the shared
and dedicated paths behave identically.
- No claim of certification; independent review and two-Workspace invocation acceptance
are still required.
+8
View File
@@ -0,0 +1,8 @@
<svg
className="w-16 h-16 text-[#2288ee] color-[#2288ee]"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="#2288ee"
>
<path d="M8 4C8 2.34315 9.34315 1 11 1C12.6569 1 14 2.34315 14 4C14 4.35064 13.9398 4.68722 13.8293 5H18C18.5523 5 19 5.44772 19 6V10.1707C19.3128 10.0602 19.6494 10 20 10C21.6569 10 23 11.3431 23 13C23 14.6569 21.6569 16 20 16C19.6494 16 19.3128 15.9398 19 15.8293V20C19 20.5523 18.5523 21 18 21H4C3.44772 21 3 20.5523 3 20V6C3 5.44772 3.44772 5 4 5H8.17071C8.06015 4.68722 8 4.35064 8 4Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.
@@ -0,0 +1,51 @@
# 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):
def __init__(self):
super().__init__()
@self.subcommand(
name="", # empty string means the root command
help="Show information of the query", # command help message
usage="info", # command usage example, displayed in the command help message
aliases=["i"], # command aliases
)
async def send(self, context: ExecuteContext) -> AsyncGenerator[CommandReturn, None]:
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"
print(f"发起者ID{context.session.launcher_id}发送者ID{context.session.sender_id}消息类型{context.session.launcher_type}")
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]:
field_name = context.crt_params[0]
field_value = getattr(context, field_name)
yield CommandReturn(
text=f"{field_name}: {field_value}",
)
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Command
metadata:
name: info
label:
en_US: Info
zh_Hans: Info
description:
en_US: 'Show information of the query'
zh_Hans: 'Show information of the query'
spec:
execution:
python:
path: info.py
attr: Info
@@ -0,0 +1,90 @@
# 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
import base64
from langbot_plugin.api.definition.components.common.event_listener import EventListener
from langbot_plugin.api.entities import events, context
from langbot_plugin.api.entities.builtin.platform import message as platform_message
from langbot_plugin.api.entities.builtin.provider import message as provider_message
class DefaultEventListener(EventListener):
def __init__(self):
super().__init__()
@self.handler(events.GroupNormalMessageReceived)
@self.handler(events.PersonNormalMessageReceived)
async def handler(event_context: context.EventContext):
print("Hello LangBot Plugin!")
event_context.event.user_message_alter = provider_message.ContentElement.from_text("Hello from LangBot Plugin!")
# event_context.prevent_default()
# # event_context.event.reply_message_chain = platform_message.MessageChain([
# # platform_message.Plain(text=f"Hello from LangBot Plugin!"),
# # ])
# # Simple text response
# # await event_context.reply(
# # platform_message.MessageChain([
# # platform_message.Plain(text=f"Hello from LangBot Plugin! 1"),
# # ])
# # )
# # raw_message_event = event_context.event.message_event
# # print(raw_message_event)
# with open("assets/image.png", "rb") as f:
# image_data = f.read()
# image_base64 = base64.b64encode(image_data).decode("utf-8")
# # image = platform_message.Image(base64=f"data:image/png;base64,{image_base64}")
# image = platform_message.Image(url="https://langbot.app/docs/langbot-logo.png")
# with open("assets/rec.wav", "rb") as f:
# voice_data = f.read()
# voice_base64 = base64.b64encode(voice_data).decode("utf-8")
# voice = platform_message.Voice(base64=f"data:audio/wav;base64,{voice_base64}")
# # Comprehensive test message with various components
# await event_context.reply(
# platform_message.MessageChain([
# platform_message.Plain(text=f"Hello\n"),
# platform_message.Plain(text="This is a test message, containing various component types:\n"),
# platform_message.Plain(text="1. Text message "),
# # platform_message.At(target=event_context.event.sender_id, display="@you"),
# platform_message.Plain(text="\n2. At someone\n"),
# platform_message.Plain(text="3. Image (example)\n"),
# image,
# platform_message.Plain(text="\n4. Voice (example)\n"),
# voice,
# platform_message.Plain(text="\n5. File (example)\n"),
# platform_message.Plain(text="\n✅ All components tested successfully!"),
# ])
# )
# await event_context.reply(
# platform_message.MessageChain([
# platform_message.File(
# name=".mcp.json",
# size=1024000,
# url="https://raw.githubusercontent.com/langbot-app/LangBot/refs/heads/master/.mcp.json"
# ),
# ])
# )
# # Test message with Quote component
# await event_context.reply(
# platform_message.MessageChain([
# platform_message.Quote(
# id=event_context.event.message_chain.message_id,
# sender_id=event_context.event.sender_id,
# origin=[platform_message.Plain(text="Quote the original message")]
# ),
# platform_message.Plain(text="This is a quoted reply message"),
# ])
# )
@@ -0,0 +1,12 @@
apiVersion: v1
kind: EventListener
metadata:
name: default
label:
en_US: DefaultEventListener
zh_Hans: DefaultEventListener
spec:
execution:
python:
path: default.py
attr: DefaultEventListener
@@ -0,0 +1,56 @@
# 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
import httpx
# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
async def make_nws_request(url: str) -> dict[str, Any] | None:
"""Make a request to the NWS API with proper error handling."""
headers = {
"User-Agent": USER_AGENT,
"Accept": "application/geo+json"
}
async with httpx.AsyncClient() as client:
try:
response = await client.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None
def format_alert(feature: dict) -> str:
"""Format an alert feature into a readable string."""
props = feature["properties"]
return f"""
Event: {props.get('event', 'Unknown')}
Area: {props.get('areaDesc', 'Unknown')}
Severity: {props.get('severity', 'Unknown')}
Description: {props.get('description', 'No description available')}
Instructions: {props.get('instruction', 'No specific instructions provided')}
"""
class GetWeatherAlerts(Tool):
async def call(self, params: dict[str, Any]) -> dict[str, Any]:
"""Fill your tool code here"""
state = params.get("state", "CA")
url = f"{NWS_API_BASE}/alerts/active/area/{state}"
data = await make_nws_request(url)
if not data or "features" not in data:
return "Unable to fetch alerts or no alerts found."
if not data["features"]:
return "No active alerts for this state."
alerts = [format_alert(feature) for feature in data["features"]]
return "\n---\n".join(alerts)
@@ -0,0 +1,24 @@
apiVersion: v1 # 请勿修改
kind: Tool # 请勿修改
metadata:
name: get_weather_alerts # 工具名称,供 LLM 识别
label:
en_US: GetWeatherAlerts # 工具显示名称,用于显示在 LangBot 的 UI 上,支持多语言
zh_Hans: GetWeatherAlerts
description:
en_US: 'Get weather alerts for a US state.' # 工具描述,用于显示在 LangBot 的 UI 上,支持多语言。可选
zh_Hans: '获取美国某个州的天气预警'
spec:
parameters:
type: object # 请勿修改
properties: # 请勿修改,在此处添加参数
state:
type: string
description: 'Two-letter US state code (e.g. CA, NY)'
required: # 在此添加必填参数说明
- state
llm_prompt: 'Get weather alerts for a US state.' # 工具提示词,供 LLM 判断是否调用该工具
execution:
python:
path: get_weather_alerts.py # 工具处理程序,请勿修改
attr: GetWeatherAlerts # 工具处理程序的类名,与 get_weather_alerts.py 中的类名一致
+13
View File
@@ -0,0 +1,13 @@
# 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.plugin import BasePlugin
class HelloPlugin(BasePlugin):
def __init__(self):
super().__init__()
async def initialize(self) -> None:
pass
+46
View File
@@ -0,0 +1,46 @@
apiVersion: v1
kind: Plugin
metadata:
author: RockChinQ
name: HelloPlugin
repository: https://github.com/langbot-app/LangBot/tree/master/plugins/misc/HelloPlugin
version: 0.1.4
icon: assets/icon.svg
description:
en_US: This plugin is an experimental plugin that provides demos on `EventListener`,
`Tool` and `Command` components.
zh_Hans: 这是一个实验性的插件,提供了 `EventListener`、`Tool` 和 `Command` 组件的演示。
zh_Hant: 這是一個實驗性的插件,提供了 EventListener、Tool 和 Command 元件的演示。
ja_JP: EventListener、Tool、Commandコンポーネントのデモを提供する実験的なプラグインです。
vi_VN: Đây là plugin thử nghiệm, cung cấp bản demo cho các thành phần EventListener,
Tool và Command.
th_TH: ปลั๊กอินทดลองที่สาธิตการใช้งานส่วนประกอบ EventListener, Tool และ Command
es_ES: Plugin experimental que proporciona demostraciones de los componentes EventListener,
Tool y Command.
label:
en_US: HelloPlugin
zh_Hans: HelloPlugin
zh_Hant: HelloPlugin
ja_JP: HelloPlugin
vi_VN: HelloPlugin
th_TH: HelloPlugin
es_ES: HelloPlugin
icon: assets/icon.svg
spec:
config: []
components:
EventListener:
fromDirs:
- path: components/event_listener/
Command:
fromDirs:
- path: components/commands/
Tool:
fromDirs:
- path: components/tools/
execution:
python:
path: main.py
attr: HelloPlugin
sharedRuntime: shared-runtime-v1
componentModel: stateless-v1
@@ -0,0 +1,7 @@
# HolaPlugin
Este complemento es un complemento experimental que proporciona demostraciones sobre los componentes`EventListener`,`Tool`y`Command`.
-`components/event_listener/default.py`: simplemente responde un mensaje cuando se recibe un mensaje personal.
-`components/tools/get_weather_alerts.py`: proporciona una herramienta que se puede utilizar para obtener alertas meteorológicas de los estados de EE. UU.
-`components/commands/info.py`: responde al comando`info`con el contexto de ejecución del comando.
@@ -0,0 +1,7 @@
# HelloPlugin
このプラグインは、`EventListener`、`Tool`、`Command` コンポーネントのデモを提供する実験的なプラグインです。
- `components/event_listener/default.py`:個人メッセージを受信したときに、シンプルにメッセージを返信します。
- `components/tools/get_weather_alerts.py`:米国の州の気象警報を取得するために使用できるツールを提供します。
- `components/commands/info.py`:`info` コマンドに応答し、コマンド実行のコンテキストを返します。
@@ -0,0 +1,7 @@
# สวัสดีปลั๊กอิน
ปลั๊กอินนี้เป็นปลั๊กอินทดลองที่ให้การสาธิตเกี่ยวกับส่วนประกอบ`EventListener`,`Tool`และ`Command`
-`components/event_listener/default.py`: เพียงตอบกลับข้อความเมื่อได้รับข้อความถึงบุคคล
-`components/tools/get_weather_alerts.py`: จัดเตรียมเครื่องมือที่สามารถใช้เพื่อรับการแจ้งเตือนสภาพอากาศของรัฐในสหรัฐฯ
-`components/commands/info.py`: ตอบสนองต่อคำสั่ง`info`ด้วยบริบทของการดำเนินการคำสั่ง
@@ -0,0 +1,7 @@
#Xin chàoPlugin
Plugin này là một plugin thử nghiệm cung cấp các bản demo trên các thành phần`EventListener`,`Tool`và`Command`.
-`comComponents/event_listener/default.py`: chỉ cần trả lời tin nhắn khi nhận được tin nhắn của một người.
-`comComponents/tools/get_weather_alerts.py`: cung cấp một công cụ có thể được sử dụng để nhận thông báo thời tiết của các tiểu bang Hoa Kỳ.
-`comComponents/commands/info.py`: phản hồi lệnh`info`với ngữ cảnh thực thi lệnh.
@@ -0,0 +1,7 @@
# HelloPlugin
这是一个实验性插件,提供了 `EventListener`、`Tool` 和 `Command` 组件的演示示例。
- `components/event_listener/default.py`:当收到个人消息时简单地回复一条消息。
- `components/tools/get_weather_alerts.py`:提供一个可用于获取美国各州天气预警信息的工具。
- `components/commands/info.py`:响应 `info` 命令并返回命令执行的上下文信息。
@@ -0,0 +1,7 @@
# 你好插件
該插件是一個實驗性插件,提供了`EventListener`、`Tool`和`Command`元件的示範。
-`components/event_listener/default.py`:當收到個人訊息時簡單地回覆訊息。
-`components/tools/get_weather_alerts.py`:提供一個可用來取得美國各州天氣警報的工具。
-`components/commands/info.py`:使用指令執行的上下文回應指令`info`。
@@ -0,0 +1,2 @@
langbot-plugin
httpx
@@ -0,0 +1,111 @@
"""Two installations drive one HelloPlugin object graph under shared-runtime-v1.
HelloPlugin relies on the stateless component model: one ``HelloPlugin`` and one
object per declared component serve every installation of the artifact digest.
These tests instantiate the real plugin/component classes, attach the same
component object to a task-local invocation for two distinct
``InstallationBinding`` values, and assert that each call builds everything from
its arguments and the invocation, retaining no tenant state on the shared object.
"""
from __future__ import annotations
import asyncio
import sys
from pathlib import Path
from types import SimpleNamespace
from langbot_plugin.api.entities import events
from langbot_plugin.api.proxies.invocation import bind_invocation
from langbot_plugin.entities.io.context import InstallationBinding
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
if str(PLUGIN_ROOT) not in sys.path:
sys.path.insert(0, str(PLUGIN_ROOT))
import components.event_listener.default as default_mod # noqa: E402
import components.tools.get_weather_alerts as weather_mod # noqa: E402
from main import HelloPlugin # noqa: E402
def _binding(
installation: str,
*,
workspace: str = "workspace-1",
instance: str = "instance-1",
revision: int = 1,
) -> InstallationBinding:
return InstallationBinding(
instance_uuid=instance,
workspace_uuid=workspace,
installation_uuid=installation,
runtime_revision=revision,
artifact_digest="a" * 64,
)
def _wire(component):
"""Mimic the runtime object graph: one plugin and one shared component."""
plugin = HelloPlugin()
handler = SimpleNamespace()
plugin.plugin_runtime_handler = handler
component.plugin = plugin
return plugin, handler, component
def test_tool_is_stateless_and_resolves_config_per_invocation(monkeypatch):
_plugin, handler, tool = _wire(weather_mod.GetWeatherAlerts())
seen_urls: list[str] = []
async def fake_request(url: str):
seen_urls.append(url)
return {"features": [{"properties": {"event": url.rsplit("/", 1)[-1]}}]}
monkeypatch.setattr(weather_mod, "make_nws_request", fake_request)
async def call(state: str, binding: InstallationBinding, config: dict) -> str:
with bind_invocation(handler, config=config, binding=binding):
# The same shared object yields the config of the active installation.
assert tool.get_plugin_config() == config
return await tool.call({"state": state})
first = asyncio.run(call("CA", _binding("installation-a"), {"marker": "a"}))
second = asyncio.run(call("NY", _binding("installation-b"), {"marker": "b"}))
assert seen_urls == [
"https://api.weather.gov/alerts/active/area/CA",
"https://api.weather.gov/alerts/active/area/NY",
]
assert "CA" in first and "NY" in second
assert "NY" not in first and "CA" not in second
# No per-invocation or tenant value was copied onto the shared component.
assert set(vars(tool)) == {"plugin"}
def test_listener_handlers_mutate_only_the_per_event_context():
_plugin, handler, listener = _wire(default_mod.DefaultEventListener())
def make_context() -> SimpleNamespace:
return SimpleNamespace(event=SimpleNamespace(user_message_alter=None))
async def dispatch(event_context: SimpleNamespace, binding: InstallationBinding) -> None:
with bind_invocation(handler, config={}, binding=binding):
for registered in listener.registered_handlers[events.GroupNormalMessageReceived]:
await registered(event_context)
context_a = make_context()
context_b = make_context()
asyncio.run(dispatch(context_a, _binding("installation-a")))
asyncio.run(dispatch(context_b, _binding("installation-b")))
assert context_a.event.user_message_alter.text == "Hello from LangBot Plugin!"
assert context_b.event.user_message_alter.text == "Hello from LangBot Plugin!"
assert context_a.event.user_message_alter is not context_b.event.user_message_alter
# The listener holds only its handler registry and plugin reference.
assert set(vars(listener)) == {"registered_handlers", "plugin"}
if __name__ == "__main__":
import pytest
raise SystemExit(pytest.main([__file__, "-q"]))