mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 08:28:36 +00:00
fix(trigger): require names for trigger creation
This commit is contained in:
parent
7178ea3f13
commit
b690a48336
@ -16,7 +16,7 @@ These commands work inside chat channels and interactive agent sessions:
|
|||||||
| `/dream-restore` | List recent Dream memory versions |
|
| `/dream-restore` | List recent Dream memory versions |
|
||||||
| `/dream-restore <sha>` | Restore memory to the state before a specific change |
|
| `/dream-restore <sha>` | Restore memory to the state before a specific change |
|
||||||
| `/skill` | List enabled skills and their descriptions |
|
| `/skill` | List enabled skills and their descriptions |
|
||||||
| `/trigger` | Create a local external trigger for the current chat/session |
|
| `/trigger` | Show external trigger usage |
|
||||||
| `/trigger <name>` | Create a named local external trigger for the current chat/session |
|
| `/trigger <name>` | Create a named local external trigger for the current chat/session |
|
||||||
| `/pairing` | List pending pairing requests |
|
| `/pairing` | List pending pairing requests |
|
||||||
| `/pairing approve <code>` | Approve a pairing code |
|
| `/pairing approve <code>` | Approve a pairing code |
|
||||||
@ -59,8 +59,9 @@ Preset names come from the top-level `modelPresets` config. Switching is runtime
|
|||||||
|
|
||||||
## External Triggers
|
## External Triggers
|
||||||
|
|
||||||
Use `/trigger` when a local script or another service should be able to send a
|
Use `/trigger <name>` when a local script or another service should be able to
|
||||||
message into the current chat/session later.
|
send a message into the current chat/session later. A name is required; plain
|
||||||
|
`/trigger` only shows the usage hint.
|
||||||
|
|
||||||
Create the trigger from the chat where future messages should arrive:
|
Create the trigger from the chat where future messages should arrive:
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ Use this page when you know what you want to run and need the command shape. For
|
|||||||
| Send one test message | `nanobot agent -m "Hello!"` | First proof that install, config, provider, model, and workspace all work |
|
| Send one test message | `nanobot agent -m "Hello!"` | First proof that install, config, provider, model, and workspace all work |
|
||||||
| Chat in the terminal | `nanobot agent` | Interactive local chat; exit with `exit`, `/exit`, `:q`, or `Ctrl+D` |
|
| Chat in the terminal | `nanobot agent` | Interactive local chat; exit with `exit`, `/exit`, `:q`, or `Ctrl+D` |
|
||||||
| Use WebUI or chat apps | `nanobot gateway` | Keep this terminal running, or use `nanobot gateway --background` |
|
| Use WebUI or chat apps | `nanobot gateway` | Keep this terminal running, or use `nanobot gateway --background` |
|
||||||
| Deliver a local external trigger | `nanobot trigger <id> "message"` | Created first with `/trigger` in the target chat/session |
|
| Deliver a local external trigger | `nanobot trigger <id> "message"` | Created first with `/trigger <name>` in the target chat/session |
|
||||||
| Serve an OpenAI-compatible API | `nanobot serve` | Starts `/v1/chat/completions`, `/v1/models`, and `/health` |
|
| Serve an OpenAI-compatible API | `nanobot serve` | Starts `/v1/chat/completions`, `/v1/models`, and `/health` |
|
||||||
| Check chat channel setup | `nanobot channels status` | Useful before starting `nanobot gateway` |
|
| Check chat channel setup | `nanobot channels status` | Useful before starting `nanobot gateway` |
|
||||||
| Log in to QR/OAuth-style channels | `nanobot channels login <channel>` | Used by channels such as WhatsApp and WeChat |
|
| Log in to QR/OAuth-style channels | `nanobot channels login <channel>` | Used by channels such as WhatsApp and WeChat |
|
||||||
@ -126,7 +126,7 @@ The bundled WebUI is served by the WebSocket channel, usually on port `8765`, no
|
|||||||
## Local Triggers
|
## Local Triggers
|
||||||
|
|
||||||
`nanobot trigger` delivers one local message to a trigger that was created from
|
`nanobot trigger` delivers one local message to a trigger that was created from
|
||||||
a chat/session with `/trigger [name]`.
|
a chat/session with `/trigger <name>`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nanobot trigger trg_8K4P2Q9X "Review PR #4502"
|
nanobot trigger trg_8K4P2Q9X "Review PR #4502"
|
||||||
|
|||||||
@ -144,7 +144,7 @@ protected heartbeat system job. They run as scheduled turns in their origin
|
|||||||
chat/session and normally deliver the result back to that channel.
|
chat/session and normally deliver the result back to that channel.
|
||||||
|
|
||||||
External triggers are also session-bound, but they do not have their own
|
External triggers are also session-bound, but they do not have their own
|
||||||
schedule. Create one from the target chat with `/trigger [name]`, then call
|
schedule. Create one from the target chat with `/trigger <name>`, then call
|
||||||
`nanobot trigger <id> "<message>"` when a local script or external service wants
|
`nanobot trigger <id> "<message>"` when a local script or external service wants
|
||||||
nanobot to respond in that session. Webhook servers, third-party auth, and
|
nanobot to respond in that session. Webhook servers, third-party auth, and
|
||||||
event-to-message formatting stay outside nanobot.
|
event-to-message formatting stay outside nanobot.
|
||||||
|
|||||||
@ -125,7 +125,7 @@ There are two user-facing automation types:
|
|||||||
|
|
||||||
- Scheduled automations, created by the agent's cron tool, run at a time,
|
- Scheduled automations, created by the agent's cron tool, run at a time,
|
||||||
interval, or cron expression.
|
interval, or cron expression.
|
||||||
- External triggers, created with `/trigger [name]`, run when you call a local
|
- External triggers, created with `/trigger <name>`, run when you call a local
|
||||||
command such as `nanobot trigger trg_8K4P2Q9X "Review PR #4502"`.
|
command such as `nanobot trigger trg_8K4P2Q9X "Review PR #4502"`.
|
||||||
|
|
||||||
If a GitHub webhook, CI system, or another service should wake nanobot up, keep
|
If a GitHub webhook, CI system, or another service should wake nanobot up, keep
|
||||||
|
|||||||
@ -218,13 +218,13 @@ if DISCORD_AVAILABLE:
|
|||||||
command_text = f"/model {preset}" if preset else "/model"
|
command_text = f"/model {preset}" if preset else "/model"
|
||||||
await self._forward_slash_command(interaction, command_text)
|
await self._forward_slash_command(interaction, command_text)
|
||||||
|
|
||||||
@self.tree.command(name="trigger", description="Create a local trigger for this chat")
|
@self.tree.command(name="trigger", description="Create a named local trigger for this chat")
|
||||||
@app_commands.describe(name="Optional trigger name")
|
@app_commands.describe(name="Trigger name")
|
||||||
async def trigger_command(
|
async def trigger_command(
|
||||||
interaction: discord.Interaction,
|
interaction: discord.Interaction,
|
||||||
name: str | None = None,
|
name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
name = (name or "").strip()
|
name = name.strip()
|
||||||
command_text = f"/trigger {name}" if name else "/trigger"
|
command_text = f"/trigger {name}" if name else "/trigger"
|
||||||
await self._forward_slash_command(interaction, command_text)
|
await self._forward_slash_command(interaction, command_text)
|
||||||
|
|
||||||
|
|||||||
@ -411,7 +411,7 @@ class TelegramChannel(BaseChannel):
|
|||||||
BotCommand("status", "Show bot status"),
|
BotCommand("status", "Show bot status"),
|
||||||
BotCommand("history", "Show recent conversation messages"),
|
BotCommand("history", "Show recent conversation messages"),
|
||||||
BotCommand("goal", "Start a sustained objective (long-running task)"),
|
BotCommand("goal", "Start a sustained objective (long-running task)"),
|
||||||
BotCommand("trigger", "Create a local trigger for this chat"),
|
BotCommand("trigger", "Create a named local trigger"),
|
||||||
BotCommand("pairing", "Manage DM pairing (approve/deny/list)"),
|
BotCommand("pairing", "Manage DM pairing (approve/deny/list)"),
|
||||||
BotCommand("model", "Switch runtime model preset"),
|
BotCommand("model", "Switch runtime model preset"),
|
||||||
BotCommand("skill", "List enabled skills"),
|
BotCommand("skill", "List enabled skills"),
|
||||||
|
|||||||
@ -83,10 +83,10 @@ BUILTIN_COMMAND_SPECS: tuple[BuiltinCommandSpec, ...] = (
|
|||||||
),
|
),
|
||||||
BuiltinCommandSpec(
|
BuiltinCommandSpec(
|
||||||
"/trigger",
|
"/trigger",
|
||||||
"Create local trigger",
|
"Create named local trigger",
|
||||||
"Create a CLI trigger bound to this chat session.",
|
"Create a named CLI trigger bound to this chat session.",
|
||||||
"zap",
|
"zap",
|
||||||
"[name]",
|
"<name>",
|
||||||
),
|
),
|
||||||
BuiltinCommandSpec(
|
BuiltinCommandSpec(
|
||||||
"/dream",
|
"/dream",
|
||||||
@ -728,6 +728,18 @@ async def cmd_skill(ctx: CommandContext) -> OutboundMessage:
|
|||||||
|
|
||||||
async def cmd_trigger(ctx: CommandContext) -> OutboundMessage:
|
async def cmd_trigger(ctx: CommandContext) -> OutboundMessage:
|
||||||
"""Create a local trigger bound to the current session."""
|
"""Create a local trigger bound to the current session."""
|
||||||
|
name = ctx.args.strip()
|
||||||
|
if not name:
|
||||||
|
return OutboundMessage(
|
||||||
|
channel=ctx.msg.channel,
|
||||||
|
chat_id=ctx.msg.chat_id,
|
||||||
|
content=(
|
||||||
|
"Usage: /trigger <name>\n\n"
|
||||||
|
"Create a named local trigger bound to this chat session."
|
||||||
|
),
|
||||||
|
metadata={**dict(ctx.msg.metadata or {}), "render_as": "text"},
|
||||||
|
)
|
||||||
|
|
||||||
from nanobot.triggers.store import ExternalTriggerStore
|
from nanobot.triggers.store import ExternalTriggerStore
|
||||||
|
|
||||||
loop = ctx.loop
|
loop = ctx.loop
|
||||||
@ -741,7 +753,6 @@ async def cmd_trigger(ctx: CommandContext) -> OutboundMessage:
|
|||||||
if store is None:
|
if store is None:
|
||||||
store = ExternalTriggerStore(workspace)
|
store = ExternalTriggerStore(workspace)
|
||||||
|
|
||||||
name = ctx.args.strip() or "External trigger"
|
|
||||||
trigger = store.create(
|
trigger = store.create(
|
||||||
name=name,
|
name=name,
|
||||||
channel=ctx.msg.channel,
|
channel=ctx.msg.channel,
|
||||||
|
|||||||
@ -867,7 +867,7 @@ async def test_slash_new_is_blocked_for_disallowed_user() -> None:
|
|||||||
assert handled == []
|
assert handled == []
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("slash_name", ["stop", "restart", "status", "history", "model", "trigger"])
|
@pytest.mark.parametrize("slash_name", ["stop", "restart", "status", "history", "model"])
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_slash_commands_forward_via_handle_message(slash_name: str) -> None:
|
async def test_slash_commands_forward_via_handle_message(slash_name: str) -> None:
|
||||||
channel = DiscordChannel(DiscordConfig(enabled=True, allow_from=["*"]), MessageBus())
|
channel = DiscordChannel(DiscordConfig(enabled=True, allow_from=["*"]), MessageBus())
|
||||||
@ -919,7 +919,7 @@ async def test_slash_model_forwards_optional_preset() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_slash_trigger_forwards_optional_name() -> None:
|
async def test_slash_trigger_forwards_required_name() -> None:
|
||||||
channel = DiscordChannel(DiscordConfig(enabled=True, allow_from=["*"]), MessageBus())
|
channel = DiscordChannel(DiscordConfig(enabled=True, allow_from=["*"]), MessageBus())
|
||||||
handled: list[dict] = []
|
handled: list[dict] = []
|
||||||
|
|
||||||
|
|||||||
@ -1577,6 +1577,7 @@ def test_telegram_bus_slash_command_regex_matches_agent_loop_commands() -> None:
|
|||||||
assert pat.fullmatch("/history")
|
assert pat.fullmatch("/history")
|
||||||
assert pat.fullmatch("/history 5")
|
assert pat.fullmatch("/history 5")
|
||||||
assert pat.fullmatch("/goal ship the feature")
|
assert pat.fullmatch("/goal ship the feature")
|
||||||
|
assert pat.fullmatch("/trigger")
|
||||||
assert pat.fullmatch("/trigger PR review")
|
assert pat.fullmatch("/trigger PR review")
|
||||||
assert pat.fullmatch("/pairing list")
|
assert pat.fullmatch("/pairing list")
|
||||||
assert pat.fullmatch("/model fast")
|
assert pat.fullmatch("/model fast")
|
||||||
|
|||||||
@ -45,5 +45,33 @@ async def test_trigger_command_creates_session_bound_local_trigger(tmp_path: Pat
|
|||||||
assert f"nanobot trigger {trigger.id} \"message\"" in response.content
|
assert f"nanobot trigger {trigger.id} \"message\"" in response.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_trigger_command_without_name_returns_usage_only(tmp_path: Path) -> None:
|
||||||
|
router = CommandRouter()
|
||||||
|
register_builtin_commands(router)
|
||||||
|
store = ExternalTriggerStore(tmp_path)
|
||||||
|
loop = SimpleNamespace(workspace=tmp_path, external_trigger_store=store)
|
||||||
|
msg = InboundMessage(
|
||||||
|
channel="websocket",
|
||||||
|
sender_id="user",
|
||||||
|
chat_id="chat-1",
|
||||||
|
content="/trigger@nanobot_bot",
|
||||||
|
metadata={"webui": True},
|
||||||
|
)
|
||||||
|
ctx = CommandContext(
|
||||||
|
msg=msg,
|
||||||
|
session=None,
|
||||||
|
key="websocket:chat-1",
|
||||||
|
raw="/trigger@nanobot_bot",
|
||||||
|
loop=loop,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await router.dispatch(ctx)
|
||||||
|
|
||||||
|
assert response is not None
|
||||||
|
assert "Usage: /trigger <name>" in response.content
|
||||||
|
assert store.list_for_session("websocket:chat-1") == []
|
||||||
|
|
||||||
|
|
||||||
def test_trigger_command_is_in_help_text() -> None:
|
def test_trigger_command_is_in_help_text() -> None:
|
||||||
assert "/trigger [name]" in build_help_text()
|
assert "/trigger <name>" in build_help_text()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user