feat(channels): add systemPrompt support for WhatsApp and Signal
Add per-group and per-DM system prompt configuration for WhatsApp and Signal channels, mirroring the existing Telegram implementation. - Add systemPrompt to DmConfig type and schema - Add systemPrompt to WhatsApp group config (both root and account) - Add SignalGroupConfig type with systemPrompt and groups config - Add resolveChannelGroupSystemPrompt() and resolveChannelDmSystemPrompt() helpers in group-policy.ts - Pass GroupSystemPrompt to inbound context in WhatsApp and Signal handlers
This commit is contained in:
parent
6859e1e6a6
commit
4e515ceb89
@ -8,6 +8,8 @@ export type GroupPolicyChannel = ChannelId;
|
||||
export type ChannelGroupConfig = {
|
||||
requireMention?: boolean;
|
||||
tools?: GroupToolPolicyConfig;
|
||||
/** Custom system prompt for this group. */
|
||||
systemPrompt?: string;
|
||||
};
|
||||
|
||||
export type ChannelGroupPolicy = {
|
||||
@ -105,3 +107,67 @@ export function resolveChannelGroupToolsPolicy(params: {
|
||||
if (defaultConfig?.tools) return defaultConfig.tools;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve group-specific system prompt from channel config.
|
||||
* Falls back to wildcard (`*`) config if specific group not found.
|
||||
*/
|
||||
export function resolveChannelGroupSystemPrompt(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
channel: GroupPolicyChannel;
|
||||
groupId?: string | null;
|
||||
accountId?: string | null;
|
||||
}): string | undefined {
|
||||
const { groupConfig, defaultConfig } = resolveChannelGroupPolicy(params);
|
||||
if (groupConfig?.systemPrompt?.trim()) return groupConfig.systemPrompt.trim();
|
||||
if (defaultConfig?.systemPrompt?.trim()) return defaultConfig.systemPrompt.trim();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type ChannelDmsConfig = Record<
|
||||
string,
|
||||
{ historyLimit?: number; systemPrompt?: string } | undefined
|
||||
>;
|
||||
|
||||
function resolveChannelDms(
|
||||
cfg: ClawdbotConfig,
|
||||
channel: GroupPolicyChannel,
|
||||
accountId?: string | null,
|
||||
): ChannelDmsConfig | undefined {
|
||||
const normalizedAccountId = normalizeAccountId(accountId);
|
||||
const channelConfig = cfg.channels?.[channel] as
|
||||
| {
|
||||
accounts?: Record<string, { dms?: ChannelDmsConfig }>;
|
||||
dms?: ChannelDmsConfig;
|
||||
}
|
||||
| undefined;
|
||||
if (!channelConfig) return undefined;
|
||||
const accountDms =
|
||||
channelConfig.accounts?.[normalizedAccountId]?.dms ??
|
||||
channelConfig.accounts?.[
|
||||
Object.keys(channelConfig.accounts ?? {}).find(
|
||||
(key) => key.toLowerCase() === normalizedAccountId.toLowerCase(),
|
||||
) ?? ""
|
||||
]?.dms;
|
||||
return accountDms ?? channelConfig.dms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve DM-specific system prompt from channel config.
|
||||
* Falls back to wildcard (`*`) config if specific DM not found.
|
||||
*/
|
||||
export function resolveChannelDmSystemPrompt(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
channel: GroupPolicyChannel;
|
||||
dmId?: string | null;
|
||||
accountId?: string | null;
|
||||
}): string | undefined {
|
||||
const dms = resolveChannelDms(params.cfg, params.channel, params.accountId);
|
||||
if (!dms) return undefined;
|
||||
const normalizedId = params.dmId?.trim();
|
||||
const dmConfig = normalizedId ? dms[normalizedId] : undefined;
|
||||
const defaultConfig = dms["*"];
|
||||
if (dmConfig?.systemPrompt?.trim()) return dmConfig.systemPrompt.trim();
|
||||
if (defaultConfig?.systemPrompt?.trim()) return defaultConfig.systemPrompt.trim();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -8,6 +8,8 @@ export type GroupChatConfig = {
|
||||
|
||||
export type DmConfig = {
|
||||
historyLimit?: number;
|
||||
/** Custom system prompt for this DM conversation. */
|
||||
systemPrompt?: string;
|
||||
};
|
||||
|
||||
export type QueueConfig = {
|
||||
|
||||
@ -6,10 +6,20 @@ import type {
|
||||
} from "./types.base.js";
|
||||
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
|
||||
import type { DmConfig } from "./types.messages.js";
|
||||
import type { GroupToolPolicyConfig } from "./types.tools.js";
|
||||
|
||||
export type SignalReactionNotificationMode = "off" | "own" | "all" | "allowlist";
|
||||
export type SignalReactionLevel = "off" | "ack" | "minimal" | "extensive";
|
||||
|
||||
export type SignalGroupConfig = {
|
||||
/** Require explicit bot mention in group messages (default: true). */
|
||||
requireMention?: boolean;
|
||||
/** Tool policy overrides for this group. */
|
||||
tools?: GroupToolPolicyConfig;
|
||||
/** Custom system prompt for this group. */
|
||||
systemPrompt?: string;
|
||||
};
|
||||
|
||||
export type SignalAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
name?: string;
|
||||
@ -57,6 +67,8 @@ export type SignalAccountConfig = {
|
||||
dmHistoryLimit?: number;
|
||||
/** Per-DM config overrides keyed by user ID. */
|
||||
dms?: Record<string, DmConfig>;
|
||||
/** Per-group config overrides keyed by group ID. */
|
||||
groups?: Record<string, SignalGroupConfig>;
|
||||
/** Outbound text chunk size (chars). Default: 4000. */
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
||||
|
||||
@ -70,6 +70,8 @@ export type WhatsAppConfig = {
|
||||
{
|
||||
requireMention?: boolean;
|
||||
tools?: GroupToolPolicyConfig;
|
||||
/** Custom system prompt for this group. */
|
||||
systemPrompt?: string;
|
||||
}
|
||||
>;
|
||||
/** Acknowledgment reaction sent immediately upon message receipt. */
|
||||
@ -135,6 +137,8 @@ export type WhatsAppAccountConfig = {
|
||||
{
|
||||
requireMention?: boolean;
|
||||
tools?: GroupToolPolicyConfig;
|
||||
/** Custom system prompt for this group. */
|
||||
systemPrompt?: string;
|
||||
}
|
||||
>;
|
||||
/** Acknowledgment reaction sent immediately upon message receipt. */
|
||||
|
||||
@ -92,6 +92,7 @@ export const GroupChatSchema = z
|
||||
export const DmConfigSchema = z
|
||||
.object({
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
systemPrompt: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
@ -474,6 +474,14 @@ export const SlackConfigSchema = SlackAccountSchema.extend({
|
||||
}
|
||||
});
|
||||
|
||||
export const SignalGroupSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
systemPrompt: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const SignalAccountSchemaBase = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
@ -499,6 +507,7 @@ export const SignalAccountSchemaBase = z
|
||||
historyLimit: z.number().int().min(0).optional(),
|
||||
dmHistoryLimit: z.number().int().min(0).optional(),
|
||||
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
||||
groups: z.record(z.string(), SignalGroupSchema.optional()).optional(),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||
blockStreaming: z.boolean().optional(),
|
||||
|
||||
@ -41,6 +41,7 @@ export const WhatsAppAccountSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
systemPrompt: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
@ -105,6 +106,7 @@ export const WhatsAppConfigSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
systemPrompt: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
|
||||
@ -20,6 +20,10 @@ import { logInboundDrop, logTypingFailure } from "../../channels/logging.js";
|
||||
import { createReplyPrefixContext } from "../../channels/reply-prefix.js";
|
||||
import { recordInboundSession } from "../../channels/session.js";
|
||||
import { createTypingCallbacks } from "../../channels/typing.js";
|
||||
import {
|
||||
resolveChannelDmSystemPrompt,
|
||||
resolveChannelGroupSystemPrompt,
|
||||
} from "../../config/group-policy.js";
|
||||
import { readSessionUpdatedAt, resolveStorePath } from "../../config/sessions.js";
|
||||
import { danger, logVerbose, shouldLogVerbose } from "../../globals.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
@ -123,6 +127,22 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
});
|
||||
}
|
||||
const signalTo = entry.isGroup ? `group:${entry.groupId}` : `signal:${entry.senderRecipient}`;
|
||||
|
||||
// Resolve system prompt from group or DM config
|
||||
const groupSystemPrompt = entry.isGroup
|
||||
? resolveChannelGroupSystemPrompt({
|
||||
cfg: deps.cfg,
|
||||
channel: "signal",
|
||||
groupId: entry.groupId,
|
||||
accountId: deps.accountId,
|
||||
})
|
||||
: resolveChannelDmSystemPrompt({
|
||||
cfg: deps.cfg,
|
||||
channel: "signal",
|
||||
dmId: entry.senderPeerId,
|
||||
accountId: deps.accountId,
|
||||
});
|
||||
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: combinedBody,
|
||||
RawBody: entry.bodyText,
|
||||
@ -148,6 +168,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
CommandAuthorized: entry.commandAuthorized,
|
||||
OriginatingChannel: "signal" as const,
|
||||
OriginatingTo: signalTo,
|
||||
GroupSystemPrompt: groupSystemPrompt,
|
||||
});
|
||||
|
||||
await recordInboundSession({
|
||||
|
||||
@ -16,6 +16,10 @@ import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-contex
|
||||
import { toLocationContext } from "../../../channels/location.js";
|
||||
import { createReplyPrefixContext } from "../../../channels/reply-prefix.js";
|
||||
import type { loadConfig } from "../../../config/config.js";
|
||||
import {
|
||||
resolveChannelDmSystemPrompt,
|
||||
resolveChannelGroupSystemPrompt,
|
||||
} from "../../../config/group-policy.js";
|
||||
import {
|
||||
readSessionUpdatedAt,
|
||||
recordSessionMetaFromInbound,
|
||||
@ -255,6 +259,22 @@ export async function processMessage(params: {
|
||||
? (resolveIdentityNamePrefix(params.cfg, params.route.agentId) ?? "[clawdbot]")
|
||||
: undefined);
|
||||
|
||||
// Resolve system prompt from group or DM config
|
||||
const groupSystemPrompt =
|
||||
params.msg.chatType === "group"
|
||||
? resolveChannelGroupSystemPrompt({
|
||||
cfg: params.cfg,
|
||||
channel: "whatsapp",
|
||||
groupId: conversationId,
|
||||
accountId: params.route.accountId,
|
||||
})
|
||||
: resolveChannelDmSystemPrompt({
|
||||
cfg: params.cfg,
|
||||
channel: "whatsapp",
|
||||
dmId: dmRouteTarget,
|
||||
accountId: params.route.accountId,
|
||||
});
|
||||
|
||||
const ctxPayload = finalizeInboundContext({
|
||||
Body: combinedBody,
|
||||
RawBody: params.msg.body,
|
||||
@ -288,6 +308,7 @@ export async function processMessage(params: {
|
||||
Surface: "whatsapp",
|
||||
OriginatingChannel: "whatsapp",
|
||||
OriginatingTo: params.msg.from,
|
||||
GroupSystemPrompt: groupSystemPrompt,
|
||||
});
|
||||
|
||||
if (dmRouteTarget) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user