mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-28 19:35:27 +08:00
168 lines
5.7 KiB
Python
168 lines
5.7 KiB
Python
"""Execute the published tutorial with the matching 4.11 SDK installed.
|
|
|
|
Run: python scripts/verify-runner-tutorial.py
|
|
The tool API is mocked; real Host/runtime checks are a separate validation layer.
|
|
"""
|
|
|
|
import asyncio
|
|
import re
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
import yaml
|
|
from langbot_plugin.api.definition.components.runner import RunnerContext
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def load_tutorial():
|
|
examples = []
|
|
for locale in ("zh", "en", "ja"):
|
|
path = ROOT / locale / "plugin/dev/components/runner.mdx"
|
|
source = path.read_text()
|
|
python = re.findall(r"^```python\n(.*?)^```", source, re.M | re.S)
|
|
manifests = [
|
|
block
|
|
for block in re.findall(r"^```yaml\n(.*?)^```", source, re.M | re.S)
|
|
if "kind: Runner" in block
|
|
]
|
|
python = [block for block in python if "class Welcome(Runner):" in block]
|
|
assert len(python) == len(manifests) == 1, path
|
|
examples.append((python[0], manifests[0]))
|
|
assert examples[0] == examples[1] == examples[2], "Locale examples differ"
|
|
code, manifest = examples[0]
|
|
namespace = {}
|
|
exec(compile(code, "runner.mdx", "exec"), namespace)
|
|
manifest = yaml.safe_load(manifest)
|
|
assert manifest["spec"]["events"] == ["group.member_joined", "message.received"]
|
|
assert manifest["spec"]["capabilities"]["tool_calling"] is True
|
|
assert manifest["spec"]["permissions"]["tools"] == ["detail", "call"]
|
|
defaults = {field["name"]: field["default"] for field in manifest["spec"]["config"]}
|
|
return namespace[manifest["execution"]["python"]["attr"]], defaults
|
|
|
|
|
|
async def main():
|
|
component_class, defaults = load_tutorial()
|
|
component = component_class()
|
|
await component.initialize()
|
|
cases = [
|
|
(
|
|
"join",
|
|
{
|
|
"type": "group.member_joined",
|
|
"member": {"id": "alice", "nickname": "Alice"},
|
|
},
|
|
{},
|
|
"Alice, Welcome aboard!",
|
|
),
|
|
(
|
|
"nickname-fallback",
|
|
{"type": "group.member_joined", "member": {"id": "alice"}},
|
|
{},
|
|
"alice, Welcome aboard!",
|
|
),
|
|
(
|
|
"join-disabled",
|
|
{"type": "group.member_joined"},
|
|
{"reply_enabled": False},
|
|
None,
|
|
),
|
|
(
|
|
"command-disabled",
|
|
{
|
|
"type": "message.received",
|
|
"message_chain": [{"type": "Plain", "text": "/hello"}],
|
|
},
|
|
{"reply_enabled": False},
|
|
None,
|
|
),
|
|
(
|
|
"ordinary-text",
|
|
{
|
|
"type": "message.received",
|
|
"message_chain": [{"type": "Plain", "text": "hello"}],
|
|
},
|
|
{},
|
|
None,
|
|
),
|
|
("empty-message", {"type": "message.received", "message_chain": []}, {}, None),
|
|
(
|
|
"image-only",
|
|
{
|
|
"type": "message.received",
|
|
"message_chain": [
|
|
{"type": "Image", "url": "https://example.com/image.png"}
|
|
],
|
|
},
|
|
{},
|
|
None,
|
|
),
|
|
]
|
|
for chat_type in ("private", "group"):
|
|
cases.append(
|
|
(
|
|
chat_type,
|
|
{
|
|
"type": "message.received",
|
|
"chat_type": chat_type,
|
|
"message_chain": [{"type": "Plain", "text": " /hello "}],
|
|
},
|
|
{"greeting": "你好 🧩"},
|
|
"你好 🧩",
|
|
)
|
|
)
|
|
|
|
async def execute(name, data, config, reply, *, failure=False):
|
|
api = SimpleNamespace(
|
|
call_tool=AsyncMock(
|
|
side_effect=RuntimeError("Delivery failed") if failure else None,
|
|
return_value={"mock": True},
|
|
)
|
|
)
|
|
component.get_run_api = Mock(return_value=api)
|
|
context = RunnerContext.model_validate({
|
|
"run_id": name, "trigger": {"type": data["type"]},
|
|
"event": {"event_id": name, "event_type": data["type"], "source": "test", "data": data},
|
|
"config": {**defaults, **config}, "input": {}, "delivery": {"surface": "test"}, "resources": {}, "runtime": {},
|
|
})
|
|
results = []
|
|
try:
|
|
async for result in component.invoke(context):
|
|
results.append(result)
|
|
except RuntimeError as exc:
|
|
assert failure and str(exc) == "Delivery failed"
|
|
else:
|
|
assert not failure, "Delivery failure was swallowed"
|
|
assert sum(r.type == "run.completed" for r in results) == int(not failure)
|
|
assert all(r.run_id == name for r in results)
|
|
if reply is None:
|
|
api.call_tool.assert_not_called()
|
|
else:
|
|
api.call_tool.assert_awaited_once_with("event_reply", {"text": reply})
|
|
started = [
|
|
r.data["tool_call_id"] for r in results if r.type == "tool.call.started"
|
|
]
|
|
completed = [
|
|
r.data["tool_call_id"]
|
|
for r in results
|
|
if r.type == "tool.call.completed"
|
|
]
|
|
assert len(started) == 1 and started == completed
|
|
if failure:
|
|
assert any(r.type == "processor.log" for r in results)
|
|
assert any(r.data.get("error") == "Delivery failed" for r in results)
|
|
print(f"PASS {name}")
|
|
|
|
for case in cases:
|
|
await execute(*case)
|
|
await execute("delivery-failure", cases[0][1], {}, cases[0][3], failure=True)
|
|
print(
|
|
"All 10 tutorial cases passed; Chinese, English and Japanese code is identical."
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|