mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
bluebubbles: forward per-group systemPrompt into GroupSystemPrompt (#69198)
Forward per-group systemPrompt config into inbound context GroupSystemPrompt so configured group-specific behavioral instructions (for example threaded-reply and tapback conventions) are injected on every turn. Supports "*" wildcard fallback matching the existing requireMention pattern. Closes #60665. Co-authored-by: Omar Shahine <omarshahine@users.noreply.github.com>
This commit is contained in:
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/compaction: send opt-in start and completion notices during context compaction. (#67830) Thanks @feniix.
|
||||
- Moonshot/Kimi: default bundled Moonshot setup, web search, and media-understanding surfaces to `kimi-k2.6` while keeping `kimi-k2.5` available for compatibility. (#69477) Thanks @scoootscooob.
|
||||
- Moonshot/Kimi: allow `thinking.keep = "all"` on `moonshot/kimi-k2.6`, and strip it for other Moonshot models or requests where pinned `tool_choice` disables thinking. (#68816) Thanks @aniaan.
|
||||
- BlueBubbles/groups: forward per-group `systemPrompt` config into inbound context `GroupSystemPrompt` so configured group-specific behavioral instructions (for example threaded-reply and tapback conventions) are injected on every turn. Supports `"*"` wildcard fallback matching the existing `requireMention` pattern. Closes #60665. (#69198) Thanks @omarshahine.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ab40431597e9f7c09a9f010f267bab250c7f9c570c4a100776de98869f589a92 config-baseline.json
|
||||
580abc79677d84fa66cb55e42ea093bfa9681655861166c02dfaa5a313d44310 config-baseline.json
|
||||
04a82c2208bf69e0a195e7712e3a518a8255c1bb002c31f712cb95003325635b config-baseline.core.json
|
||||
e239cc20f20f8d0172812bc0ad3ee6df52da88e2e2702e3d03a47e01561132ae config-baseline.channel.json
|
||||
b695cb31b4c0cf1d31f842f2892e99cc3ff8d84263ae72b72977cae844b81d6e config-baseline.plugin.json
|
||||
8fb3a1cf5fe56ab8fc2cb46341c3403aed32b0d1f0aaeac0e96cd3599db4f06e config-baseline.plugin.json
|
||||
|
||||
@@ -217,6 +217,54 @@ Per-group configuration:
|
||||
- Uses `allowFrom` and `groupAllowFrom` to determine command authorization.
|
||||
- Authorized senders can run control commands even without mentioning in groups.
|
||||
|
||||
### Per-group system prompt
|
||||
|
||||
Each entry under `channels.bluebubbles.groups.*` accepts an optional `systemPrompt` string. The value is injected into the agent's system prompt on every turn that handles a message in that group, so you can set per-group persona or behavioral rules without editing agent prompts:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
groups: {
|
||||
"iMessage;-;chat123": {
|
||||
systemPrompt: "Keep responses under 3 sentences. Mirror the group's casual tone.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The key matches whatever BlueBubbles reports as `chatGuid` / `chatIdentifier` / numeric `chatId` for the group, and a `"*"` wildcard entry provides a default for every group without an exact match (same pattern used by `requireMention` and per-group tool policies). Exact matches always win over the wildcard. DMs ignore this field; use agent-level or account-level prompt customization instead.
|
||||
|
||||
#### Worked example: threaded replies and tapback reactions (Private API)
|
||||
|
||||
With the BlueBubbles Private API enabled, inbound messages arrive with short message IDs (for example `[[reply_to:5]]`) and the agent can call `action=reply` to thread into a specific message or `action=react` to drop a tapback. A per-group `systemPrompt` is a reliable way to keep the agent choosing the right tool:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
groups: {
|
||||
"iMessage;+;chat-family": {
|
||||
systemPrompt: [
|
||||
"When replying in this group, always call action=reply with the",
|
||||
"[[reply_to:N]] messageId from context so your response threads",
|
||||
"under the triggering message. Never send a new unlinked message.",
|
||||
"",
|
||||
"For short acknowledgements ('ok', 'got it', 'on it'), use",
|
||||
"action=react with an appropriate tapback emoji (❤️, 👍, 😂, ‼️, ❓)",
|
||||
"instead of sending a text reply.",
|
||||
].join(" "),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Tapback reactions and threaded replies both require the BlueBubbles Private API; see [Advanced actions](#advanced-actions) and [Message IDs](#message-ids-short-vs-full) for the underlying mechanics.
|
||||
|
||||
## ACP conversation bindings
|
||||
|
||||
BlueBubbles chats can be turned into durable ACP workspaces without changing the transport layer.
|
||||
|
||||
@@ -30,6 +30,12 @@ const bluebubblesActionSchema = z
|
||||
const bluebubblesGroupConfigSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
/**
|
||||
* Free-form directive appended to the system prompt for every turn that
|
||||
* handles a message in this group. Use it for per-group persona tweaks or
|
||||
* behavioral rules (reply-threading, tapback conventions, etc.).
|
||||
*/
|
||||
systemPrompt: z.string().optional(),
|
||||
});
|
||||
|
||||
const bluebubblesNetworkSchema = z
|
||||
|
||||
@@ -1537,6 +1537,14 @@ async function processMessageAfterDedupe(
|
||||
OriginatingTo: `bluebubbles:${outboundTarget}`,
|
||||
WasMentioned: effectiveWasMentioned,
|
||||
CommandAuthorized: commandAuthorized,
|
||||
// Exact group match wins over the "*" wildcard fallback, matching the
|
||||
// pattern used by resolveChannelGroupRequireMention/toolsPolicy.
|
||||
GroupSystemPrompt: isGroup
|
||||
? normalizeOptionalString(
|
||||
account.config.groups?.[peerId]?.systemPrompt ??
|
||||
account.config.groups?.["*"]?.systemPrompt,
|
||||
)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
let sentMessage = false;
|
||||
|
||||
@@ -593,6 +593,100 @@ describe("BlueBubbles webhook monitor", () => {
|
||||
expect(callArgs.ctx.GroupMembers).toBe("Alice (+15551234567), Bob (+15557654321)");
|
||||
});
|
||||
|
||||
it("threads per-group systemPrompt into ctx for group messages", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({
|
||||
groups: {
|
||||
"iMessage;+;chat123456": {
|
||||
systemPrompt: "Reply in thread with action=reply; ack via action=react.",
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hello group",
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
chatName: "Family",
|
||||
participants: [{ address: "+15551234567", displayName: "Alice" }],
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
|
||||
const callArgs = getFirstDispatchCall();
|
||||
expect(callArgs.ctx.GroupSystemPrompt).toBe(
|
||||
"Reply in thread with action=reply; ack via action=react.",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the '*' wildcard systemPrompt when no exact group match", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({
|
||||
groups: {
|
||||
"*": { systemPrompt: "Default group rule: keep it short." },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hi group",
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat-unmapped",
|
||||
chatName: "Family",
|
||||
participants: [{ address: "+15551234567", displayName: "Alice" }],
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
|
||||
const callArgs = getFirstDispatchCall();
|
||||
expect(callArgs.ctx.GroupSystemPrompt).toBe("Default group rule: keep it short.");
|
||||
});
|
||||
|
||||
it("prefers an exact group systemPrompt over the '*' wildcard", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({
|
||||
groups: {
|
||||
"*": { systemPrompt: "wildcard value" },
|
||||
"iMessage;+;chat123456": { systemPrompt: "exact value" },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hi group",
|
||||
isGroup: true,
|
||||
chatGuid: "iMessage;+;chat123456",
|
||||
chatName: "Family",
|
||||
participants: [{ address: "+15551234567", displayName: "Alice" }],
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
|
||||
const callArgs = getFirstDispatchCall();
|
||||
expect(callArgs.ctx.GroupSystemPrompt).toBe("exact value");
|
||||
});
|
||||
|
||||
it("omits GroupSystemPrompt for DMs even when the group config would match", async () => {
|
||||
setupWebhookTarget({
|
||||
account: createMockAccount({
|
||||
groups: {
|
||||
"+15551234567": { systemPrompt: "unused in DM" },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = createTimestampedNewMessagePayloadForTest({
|
||||
text: "hi",
|
||||
isGroup: false,
|
||||
});
|
||||
|
||||
await dispatchWebhookPayload(payload);
|
||||
|
||||
const callArgs = getFirstDispatchCall();
|
||||
expect(callArgs.ctx.GroupSystemPrompt).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not enrich group participants when the config flag is disabled", async () => {
|
||||
const resolvePhoneNames = vi.fn(async () => new Map([["5551234567", "Alice Contact"]]));
|
||||
setupWebhookTarget({
|
||||
|
||||
@@ -10,6 +10,11 @@ export type BlueBubblesGroupConfig = {
|
||||
requireMention?: boolean;
|
||||
/** Optional tool policy overrides for this group. */
|
||||
tools?: { allow?: string[]; deny?: string[] };
|
||||
/**
|
||||
* Free-form directive appended to the system prompt on every turn that
|
||||
* handles a message in this group.
|
||||
*/
|
||||
systemPrompt?: string;
|
||||
};
|
||||
|
||||
export type BlueBubblesActionConfig = {
|
||||
|
||||
@@ -15,7 +15,7 @@ const sourceRoots = ["src/channels", "src/routing", "src/line", "extensions"];
|
||||
// code should be rejected and migrated to fetchWithSsrFGuard/shared channel helpers.
|
||||
const allowedRawFetchCallsites = new Set([
|
||||
bundledPluginCallsite("bluebubbles", "src/test-harness.ts", 132),
|
||||
bundledPluginCallsite("bluebubbles", "src/types.ts", 189),
|
||||
bundledPluginCallsite("bluebubbles", "src/types.ts", 194),
|
||||
bundledPluginCallsite("browser", "src/browser/cdp.helpers.ts", 268),
|
||||
bundledPluginCallsite("browser", "src/browser/client-fetch.ts", 192),
|
||||
bundledPluginCallsite("browser", "src/browser/test-fetch.ts", 24),
|
||||
|
||||
Reference in New Issue
Block a user