Merge pull request #964 from bohdanpodvirnyi/feat/telegram-reactions
feat(telegram): add bidirectional emoji reactions support
This commit is contained in:
@@ -9,6 +9,7 @@ import { createAgentSession, SessionManager, SettingsManager } from "@mariozechn
|
||||
import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js";
|
||||
import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js";
|
||||
import { getMachineDisplayName } from "../../../infra/machine-name.js";
|
||||
import { resolveTelegramReactionLevel } from "../../../telegram/reaction-level.js";
|
||||
import { normalizeMessageChannel } from "../../../utils/message-channel.js";
|
||||
import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
|
||||
import { resolveUserPath } from "../../../utils.js";
|
||||
@@ -161,6 +162,17 @@ export async function runEmbeddedAttempt(
|
||||
accountId: params.agentAccountId,
|
||||
}) ?? [])
|
||||
: undefined;
|
||||
const reactionGuidance =
|
||||
runtimeChannel === "telegram" && params.config
|
||||
? (() => {
|
||||
const resolved = resolveTelegramReactionLevel({
|
||||
cfg: params.config,
|
||||
accountId: params.agentAccountId ?? undefined,
|
||||
});
|
||||
const level = resolved.agentReactionGuidance;
|
||||
return level ? { level, channel: "Telegram" } : undefined;
|
||||
})()
|
||||
: undefined;
|
||||
const runtimeInfo = {
|
||||
host: machineName,
|
||||
os: `${os.type()} ${os.release()}`,
|
||||
@@ -192,6 +204,7 @@ export async function runEmbeddedAttempt(
|
||||
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
||||
: undefined,
|
||||
skillsPrompt,
|
||||
reactionGuidance,
|
||||
runtimeInfo,
|
||||
sandboxInfo,
|
||||
tools,
|
||||
|
||||
@@ -14,6 +14,10 @@ export function buildEmbeddedSystemPrompt(params: {
|
||||
reasoningTagHint: boolean;
|
||||
heartbeatPrompt?: string;
|
||||
skillsPrompt?: string;
|
||||
reactionGuidance?: {
|
||||
level: "minimal" | "extensive";
|
||||
channel: string;
|
||||
};
|
||||
runtimeInfo: {
|
||||
host: string;
|
||||
os: string;
|
||||
@@ -40,6 +44,7 @@ export function buildEmbeddedSystemPrompt(params: {
|
||||
reasoningTagHint: params.reasoningTagHint,
|
||||
heartbeatPrompt: params.heartbeatPrompt,
|
||||
skillsPrompt: params.skillsPrompt,
|
||||
reactionGuidance: params.reactionGuidance,
|
||||
runtimeInfo: params.runtimeInfo,
|
||||
sandboxInfo: params.sandboxInfo,
|
||||
toolNames: params.tools.map((tool) => tool.name),
|
||||
|
||||
@@ -208,4 +208,17 @@ describe("buildAgentSystemPrompt", () => {
|
||||
expect(prompt).toContain("User can toggle with /elevated on|off.");
|
||||
expect(prompt).toContain("Current elevated level: on");
|
||||
});
|
||||
|
||||
it("includes reaction guidance when provided", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/clawd",
|
||||
reactionGuidance: {
|
||||
level: "minimal",
|
||||
channel: "Telegram",
|
||||
},
|
||||
});
|
||||
|
||||
expect(prompt).toContain("## Reactions");
|
||||
expect(prompt).toContain("Reactions are enabled for Telegram in MINIMAL mode.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,6 +43,11 @@ export function buildAgentSystemPrompt(params: {
|
||||
defaultLevel: "on" | "off";
|
||||
};
|
||||
};
|
||||
/** Reaction guidance for the agent (for Telegram minimal/extensive modes). */
|
||||
reactionGuidance?: {
|
||||
level: "minimal" | "extensive";
|
||||
channel: string;
|
||||
};
|
||||
}) {
|
||||
const coreToolSummaries: Record<string, string> = {
|
||||
read: "Read file contents",
|
||||
@@ -351,6 +356,29 @@ export function buildAgentSystemPrompt(params: {
|
||||
if (extraSystemPrompt) {
|
||||
lines.push("## Group Chat Context", extraSystemPrompt, "");
|
||||
}
|
||||
if (params.reactionGuidance) {
|
||||
const { level, channel } = params.reactionGuidance;
|
||||
const guidanceText =
|
||||
level === "minimal"
|
||||
? [
|
||||
`Reactions are enabled for ${channel} in MINIMAL mode.`,
|
||||
"React ONLY when truly relevant:",
|
||||
"- Acknowledge important user requests or confirmations",
|
||||
"- Express genuine sentiment (humor, appreciation) sparingly",
|
||||
"- Avoid reacting to routine messages or your own replies",
|
||||
"Guideline: at most 1 reaction per 5-10 exchanges.",
|
||||
].join("\n")
|
||||
: [
|
||||
`Reactions are enabled for ${channel} in EXTENSIVE mode.`,
|
||||
"Feel free to react liberally:",
|
||||
"- Acknowledge messages with appropriate emojis",
|
||||
"- Express sentiment and personality through reactions",
|
||||
"- React to interesting content, humor, or notable events",
|
||||
"- Use reactions to confirm understanding or agreement",
|
||||
"Guideline: react whenever it feels natural.",
|
||||
].join("\n");
|
||||
lines.push("## Reactions", guidanceText, "");
|
||||
}
|
||||
if (reasoningHint) {
|
||||
lines.push("## Reasoning Format", reasoningHint, "");
|
||||
}
|
||||
|
||||
@@ -33,9 +33,30 @@ describe("handleTelegramAction", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("adds reactions", async () => {
|
||||
it("adds reactions when reactionLevel is minimal", async () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok" } },
|
||||
channels: { telegram: { botToken: "tok", reactionLevel: "minimal" } },
|
||||
} as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
action: "react",
|
||||
chatId: "123",
|
||||
messageId: "456",
|
||||
emoji: "✅",
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
expect(reactMessageTelegram).toHaveBeenCalledWith(
|
||||
"123",
|
||||
456,
|
||||
"✅",
|
||||
expect.objectContaining({ token: "tok", remove: false }),
|
||||
);
|
||||
});
|
||||
|
||||
it("adds reactions when reactionLevel is extensive", async () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok", reactionLevel: "extensive" } },
|
||||
} as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
@@ -56,7 +77,7 @@ describe("handleTelegramAction", () => {
|
||||
|
||||
it("removes reactions on empty emoji", async () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok" } },
|
||||
channels: { telegram: { botToken: "tok", reactionLevel: "minimal" } },
|
||||
} as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
@@ -77,7 +98,7 @@ describe("handleTelegramAction", () => {
|
||||
|
||||
it("removes reactions when remove flag set", async () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok" } },
|
||||
channels: { telegram: { botToken: "tok", reactionLevel: "extensive" } },
|
||||
} as ClawdbotConfig;
|
||||
await handleTelegramAction(
|
||||
{
|
||||
@@ -97,10 +118,48 @@ describe("handleTelegramAction", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("respects reaction gating", async () => {
|
||||
it("blocks reactions when reactionLevel is off", async () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok", reactionLevel: "off" } },
|
||||
} as ClawdbotConfig;
|
||||
await expect(
|
||||
handleTelegramAction(
|
||||
{
|
||||
action: "react",
|
||||
chatId: "123",
|
||||
messageId: "456",
|
||||
emoji: "✅",
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
).rejects.toThrow(/Telegram agent reactions disabled.*reactionLevel="off"/);
|
||||
});
|
||||
|
||||
it("blocks reactions when reactionLevel is ack (default)", async () => {
|
||||
const cfg = {
|
||||
channels: { telegram: { botToken: "tok", reactionLevel: "ack" } },
|
||||
} as ClawdbotConfig;
|
||||
await expect(
|
||||
handleTelegramAction(
|
||||
{
|
||||
action: "react",
|
||||
chatId: "123",
|
||||
messageId: "456",
|
||||
emoji: "✅",
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
).rejects.toThrow(/Telegram agent reactions disabled.*reactionLevel="ack"/);
|
||||
});
|
||||
|
||||
it("also respects legacy actions.reactions gating", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: { botToken: "tok", actions: { reactions: false } },
|
||||
telegram: {
|
||||
botToken: "tok",
|
||||
reactionLevel: "minimal",
|
||||
actions: { reactions: false },
|
||||
},
|
||||
},
|
||||
} as ClawdbotConfig;
|
||||
await expect(
|
||||
@@ -113,7 +172,7 @@ describe("handleTelegramAction", () => {
|
||||
},
|
||||
cfg,
|
||||
),
|
||||
).rejects.toThrow(/Telegram reactions are disabled/);
|
||||
).rejects.toThrow(/Telegram reactions are disabled via actions.reactions/);
|
||||
});
|
||||
|
||||
it("sends a text message", async () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { resolveTelegramReactionLevel } from "../../telegram/reaction-level.js";
|
||||
import {
|
||||
deleteMessageTelegram,
|
||||
reactMessageTelegram,
|
||||
@@ -82,8 +83,20 @@ export async function handleTelegramAction(
|
||||
const isActionEnabled = createActionGate(cfg.channels?.telegram?.actions);
|
||||
|
||||
if (action === "react") {
|
||||
// Check reaction level first
|
||||
const reactionLevelInfo = resolveTelegramReactionLevel({
|
||||
cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
if (!reactionLevelInfo.agentReactionsEnabled) {
|
||||
throw new Error(
|
||||
`Telegram agent reactions disabled (reactionLevel="${reactionLevelInfo.level}"). ` +
|
||||
`Set channels.telegram.reactionLevel to "minimal" or "extensive" to enable.`,
|
||||
);
|
||||
}
|
||||
// Also check the existing action gate for backward compatibility
|
||||
if (!isActionEnabled("reactions")) {
|
||||
throw new Error("Telegram reactions are disabled.");
|
||||
throw new Error("Telegram reactions are disabled via actions.reactions.");
|
||||
}
|
||||
const chatId = readStringOrNumberParam(params, "chatId", {
|
||||
required: true,
|
||||
|
||||
Reference in New Issue
Block a user