Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions plugins/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@
},
{
"id": "langbot-team/EssentialCommands",
"version": "0.1.11",
"version": "0.1.12",
"category": "misc",
"path": "misc/EssentialCommands",
"repository": "https://github.com/langbot-app/LangBot/tree/master/plugins/misc/EssentialCommands"
Expand Down Expand Up @@ -211,7 +211,7 @@
},
{
"id": "RockChinQ/HelloPlugin",
"version": "0.1.5",
"version": "0.1.6",
"category": "misc",
"path": "misc/HelloPlugin",
"repository": "https://github.com/langbot-app/LangBot/tree/master/plugins/misc/HelloPlugin"
Expand Down
30 changes: 25 additions & 5 deletions plugins/misc/EssentialCommands/components/commands/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,19 @@ async def _(self: Cmd, context: ExecuteContext) -> AsyncGenerator[CommandReturn,
command_list_str = ''

for command in commands:
command_manifest = command['manifest']
command_list_str += f"{assume_command_prefix}{command_manifest['metadata']['name']} - {command_manifest['metadata']['description'][language]}\n"
if not isinstance(command, dict):
continue
command_manifest = command.get('manifest', command)
metadata = command_manifest.get('metadata', {}) if isinstance(command_manifest, dict) else {}
name = metadata.get('name', '')
desc = metadata.get('description', {})
if isinstance(desc, dict):
description = desc.get(language) or desc.get('en_US') or (next(iter(desc.values())) if desc else '')
elif isinstance(desc, str):
description = desc
else:
description = ''
command_list_str += f"{assume_command_prefix}{name} - {description}\n"

title = get_text(language, "cmd.title")
tip = get_text(language, "cmd.tip", command_prefix=assume_command_prefix)
Expand All @@ -57,9 +68,18 @@ async def man(self: Cmd, context: ExecuteContext) -> AsyncGenerator[CommandRetur

command_manual = get_text(language, "cmd.man_not_found", command_name=command_name)
for command in commands:
command_manifest = command['manifest']
if command_manifest['metadata']['name'] == command_name:
description = command_manifest['metadata']['description'][language]
if not isinstance(command, dict):
continue
command_manifest = command.get('manifest', command)
metadata = command_manifest.get('metadata', {}) if isinstance(command_manifest, dict) else {}
if metadata.get('name') == command_name:
desc = metadata.get('description', {})
if isinstance(desc, dict):
description = desc.get(language) or desc.get('en_US') or (next(iter(desc.values())) if desc else '')
elif isinstance(desc, str):
description = desc
else:
description = ''
command_manual = get_text(language, "cmd.man_title", command_name=command_name, description=description)
break

Expand Down
26 changes: 22 additions & 4 deletions plugins/misc/EssentialCommands/components/commands/func.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,28 @@ async def _(self: Func, context: ExecuteContext) -> AsyncGenerator[CommandReturn
tool_list_str = ''

for tool in tools:
tool_manifest = tool['manifest']
name = tool_manifest['metadata']['name']
description = tool_manifest['metadata']['description'][language]
prompt = tool_manifest['spec']['llm_prompt']
if not isinstance(tool, dict):
continue

manifest = tool.get('manifest') if isinstance(tool.get('manifest'), dict) else {}
metadata = manifest.get('metadata') if isinstance(manifest.get('metadata'), dict) else {}

name = tool.get('name') or metadata.get('name') or ''

raw_description = tool.get('description') if tool.get('description') is not None else metadata.get('description')
if isinstance(raw_description, dict):
description = raw_description.get(language) or raw_description.get('en_US') or (next(iter(raw_description.values())) if raw_description else '')
elif isinstance(raw_description, str):
description = raw_description
else:
description = tool.get('human_desc') or ''

spec = tool.get('spec') if tool.get('spec') is not None else manifest.get('spec')
if isinstance(spec, dict):
prompt = spec.get('llm_prompt') or ''
else:
prompt = ''

tool_list_str += get_text(language, "func.tool_item", name=name, description=description, prompt=prompt) + "\n"

title = get_text(language, "func.title")
Expand Down
15 changes: 12 additions & 3 deletions plugins/misc/EssentialCommands/components/commands/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,18 @@ async def _(self: Plugin, context: ExecuteContext) -> AsyncGenerator[CommandRetu
plugin_list_str = ''

for plugin in plugins:
plugin_manifest = plugin['manifest']
name = plugin_manifest['metadata']['name']
description = plugin_manifest['metadata']['description'][language]
if not isinstance(plugin, dict):
continue
plugin_manifest = plugin.get('manifest', plugin)
metadata = plugin_manifest.get('metadata', {}) if isinstance(plugin_manifest, dict) else {}
name = metadata.get('name', '')
desc = metadata.get('description', {})
if isinstance(desc, dict):
description = desc.get(language) or desc.get('en_US') or (next(iter(desc.values())) if desc else '')
elif isinstance(desc, str):
description = desc
else:
description = ''
plugin_list_str += get_text(language, "plugin.item", name=name, description=description) + "\n\n"

title = get_text(language, "plugin.title")
Expand Down
2 changes: 1 addition & 1 deletion plugins/misc/EssentialCommands/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ metadata:
author: langbot-team
name: EssentialCommands
repository: https://github.com/langbot-app/LangBot/tree/master/plugins/misc/EssentialCommands
version: 0.1.11
version: 0.1.12
description:
en_US: Essential commands, previously included in LangBot
zh_Hans: 实用命令, 以前包含在 LangBot 中
Expand Down
159 changes: 159 additions & 0 deletions plugins/misc/EssentialCommands/tests/test_shared_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@
if str(PLUGIN_ROOT) not in sys.path:
sys.path.insert(0, str(PLUGIN_ROOT))

from components.commands.cmd import Cmd # noqa: E402
from components.commands.func import Func # noqa: E402
from components.commands.help import Help # noqa: E402
from components.commands.plugin import Plugin # noqa: E402
from components.commands.reset import Reset # noqa: E402
from main import EssentialCommands # noqa: E402

Expand Down Expand Up @@ -172,5 +175,161 @@ def test_dedicated_worker_config_is_still_honoured_without_a_binding():
assert plugin.get_language() == "en_US"


def test_func_command_supports_flat_tools_and_mcp_builtin_tools():
handler = SimpleNamespace()
plugin = _shared_plugin(handler)
asyncio.run(plugin.initialize())

func = Func()
func.plugin = plugin
asyncio.run(func.initialize())

mock_tools = [
# 1. Plugin tool with flat structure
{
"name": "get_weather_alerts",
"label": {"en_US": "GetWeatherAlerts", "zh_Hans": "GetWeatherAlerts"},
"description": {
"en_US": "Get weather alerts for a US state.",
"zh_Hans": "获取美国某个州的天气预警",
},
"icon": None,
"spec": {
"parameters": {"type": "object"},
"llm_prompt": "Get weather alerts for a US state.",
},
},
# 2. Builtin / sandbox tool
{
"name": "exec",
"description": "Run shell commands in an isolated execution environment.",
"human_desc": "Execute a command in an isolated environment",
"parameters": {},
"source": "builtin",
"source_name": "LangBot",
},
# 3. MCP tool with only name / source / source_id
{
"name": "mcp_custom_tool",
"source": "mcp",
"source_id": "mcp-server-uuid",
},
# 4. Legacy nested manifest tool
{
"manifest": {
"metadata": {
"name": "legacy_tool",
"description": {
"en_US": "Legacy tool description",
"zh_Hans": "旧工具描述",
},
},
"spec": {
"llm_prompt": "Legacy prompt",
},
}
},
]

async def mock_list_tools():
return mock_tools

plugin.list_tools = mock_list_tools

# Test under zh_Hans
with bind_invocation(handler, config={"language": "zh_Hans"}, binding=_binding("b")):
output_zh = asyncio.run(_render(func, "func"))[0]
assert "可用的 LLM 工具:" in output_zh
assert "get_weather_alerts - 获取美国某个州的天气预警" in output_zh
assert "提示词: Get weather alerts for a US state." in output_zh
assert "exec - Run shell commands in an isolated execution environment." in output_zh
assert "mcp_custom_tool -" in output_zh
assert "legacy_tool - 旧工具描述" in output_zh
assert "提示词: Legacy prompt" in output_zh

# Test under en_US
with bind_invocation(handler, config={"language": "en_US"}, binding=_binding("a")):
output_en = asyncio.run(_render(func, "func"))[0]
assert "Available LLM Tools:" in output_en
assert "get_weather_alerts - Get weather alerts for a US state." in output_en
assert "Prompt: Get weather alerts for a US state." in output_en
assert "exec - Run shell commands in an isolated execution environment." in output_en
assert "mcp_custom_tool -" in output_en
assert "legacy_tool - Legacy tool description" in output_en
assert "Prompt: Legacy prompt" in output_en


def test_cmd_and_plugin_commands():
handler = SimpleNamespace()
plugin = _shared_plugin(handler)
asyncio.run(plugin.initialize())

cmd = Cmd()
cmd.plugin = plugin
asyncio.run(cmd.initialize())

plugin_cmd = Plugin()
plugin_cmd.plugin = plugin
asyncio.run(plugin_cmd.initialize())

mock_commands = [
{
"manifest": {
"metadata": {
"name": "test_cmd",
"description": {
"en_US": "Test command description",
"zh_Hans": "测试命令描述",
},
}
}
}
]

async def mock_list_commands():
return mock_commands

plugin.list_commands = mock_list_commands

mock_plugins = [
{
"manifest": {
"metadata": {
"name": "TestPlugin",
"description": {
"en_US": "Test plugin description",
"zh_Hans": "测试插件描述",
},
}
}
}
]

async def mock_list_plugins():
return mock_plugins

plugin.list_plugins_manifest = mock_list_plugins

# Test cmd under zh_Hans
with bind_invocation(handler, config={"language": "zh_Hans"}, binding=_binding("b")):
output_cmd = asyncio.run(_render(cmd, "cmd"))[0]
assert "可用命令:" in output_cmd
assert "!test_cmd - 测试命令描述" in output_cmd

output_plugin = asyncio.run(_render(plugin_cmd, "plugin"))[0]
assert "已加载的插件:" in output_plugin
assert "TestPlugin - 测试插件描述" in output_plugin

# Test cmd man
async def run_man():
ctx_man = _context("cmd man test_cmd")
ctx_man.params = ["man", "test_cmd"]
ctx_man.crt_params = ["man", "test_cmd"]
return [ret.text async for ret in cmd._execute(ctx_man)]

output_man = (asyncio.run(run_man()))[0]
assert "命令 test_cmd 手册:\n测试命令描述" in output_man


if __name__ == "__main__":
raise SystemExit(pytest.main([__file__, "-q"]))
2 changes: 0 additions & 2 deletions plugins/misc/HelloPlugin/components/commands/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ async def send(self, context: ExecuteContext) -> AsyncGenerator[CommandReturn, 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,
)
Expand Down
2 changes: 1 addition & 1 deletion plugins/misc/HelloPlugin/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ metadata:
author: RockChinQ
name: HelloPlugin
repository: https://github.com/langbot-app/LangBot/tree/master/plugins/misc/HelloPlugin
version: 0.1.5
version: 0.1.6
icon: assets/icon.svg
description:
en_US: This plugin is an experimental plugin that provides demos on `EventListener`,
Expand Down
31 changes: 31 additions & 0 deletions plugins/misc/HelloPlugin/tests/test_shared_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
from types import SimpleNamespace

from langbot_plugin.api.entities import events
from langbot_plugin.api.entities.builtin.command.context import ExecuteContext
from langbot_plugin.api.entities.builtin.provider.session import LauncherTypes, Session
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.commands.info as info_mod # noqa: E402
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
Expand Down Expand Up @@ -105,6 +108,34 @@ async def dispatch(event_context: SimpleNamespace, binding: InstallationBinding)
assert set(vars(listener)) == {"registered_handlers", "plugin"}


def test_info_command_does_not_print_to_stdout(capsys):
_plugin, handler, info = _wire(info_mod.Info())
asyncio.run(info.initialize())

context = ExecuteContext(
query_id=1,
session=Session(launcher_type=LauncherTypes.PERSON, launcher_id="10001", sender_id="10001"),
command_text="info",
full_command_text="!info",
command="info",
crt_command="info",
params=[],
crt_params=[],
privilege=0,
)

async def run_info():
return [ret.text async for ret in info._execute(context)]

with bind_invocation(handler, config={}, binding=_binding("installation-a")):
results = asyncio.run(run_info())

assert len(results) == 1
assert "Query ID: 1" in results[0]
captured = capsys.readouterr()
assert captured.out == ""


if __name__ == "__main__":
import pytest

Expand Down
Loading