mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-03 22:32:27 +08:00
feat(plugins): consolidate official plugins with independent releases (LANG-180) (#2797)
Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
multica-agent
parent
d099033d79
commit
3a77306c3c
@@ -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
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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 中的类名一致
|
||||
@@ -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
|
||||
@@ -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"]))
|
||||
Reference in New Issue
Block a user