fix: inject reply context into body
parent
950432eac0
commit
67a3dda53a
|
|
@ -3,7 +3,8 @@
|
||||||
## Unreleased — 2025-12-23
|
## Unreleased — 2025-12-23
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Telegram/WhatsApp: native replies now target the original inbound message; reply context is captured in `ReplyTo*` fields for templates. (Thanks @joshp123 for the PR and follow-up question.)
|
- Telegram/WhatsApp: native replies now target the original inbound message; reply context is appended to `Body` and captured in `ReplyTo*` fields. (Thanks @joshp123 for the PR and follow-up question.)
|
||||||
|
- Embedded agent: custom model providers now load from `models.providers` (merged into `~/.clawdis/agent/models.json`), enabling proxy/base URL setups.
|
||||||
|
|
||||||
## 2.0.0-beta2 — 2025-12-21
|
## 2.0.0-beta2 — 2025-12-21
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ Updated: 2025-12-07
|
||||||
Goal: make replies deterministic per channel while keeping one shared context for direct chats.
|
Goal: make replies deterministic per channel while keeping one shared context for direct chats.
|
||||||
|
|
||||||
- **Surfaces** (channel labels): `whatsapp`, `webchat`, `telegram`, `voice`, etc. Add `Surface` to inbound `MsgContext` so templates/agents can log which channel a turn came from. Routing is fixed: replies go back to the origin surface; the model doesn’t choose.
|
- **Surfaces** (channel labels): `whatsapp`, `webchat`, `telegram`, `voice`, etc. Add `Surface` to inbound `MsgContext` so templates/agents can log which channel a turn came from. Routing is fixed: replies go back to the origin surface; the model doesn’t choose.
|
||||||
- **Reply context (optional):** inbound replies may include `ReplyToId`, `ReplyToBody`, and `ReplyToSender` so templates can surface the quoted context when needed.
|
- **Reply context:** inbound replies include `ReplyToId`, `ReplyToBody`, and `ReplyToSender`, and the quoted context is appended to `Body` as a `[Replying to ...]` block.
|
||||||
- **Canonical direct session:** All direct chats collapse into the single `main` session by default (no config needed). Groups stay `group:<jid>`, so they remain isolated.
|
- **Canonical direct session:** All direct chats collapse into the single `main` session by default (no config needed). Groups stay `group:<jid>`, so they remain isolated.
|
||||||
- **Session store:** Keys are resolved via `resolveSessionKey(scope, ctx, mainKey)`; the agent JSONL path lives under `~/.clawdis/sessions/<SessionId>.jsonl`.
|
- **Session store:** Keys are resolved via `resolveSessionKey(scope, ctx, mainKey)`; the agent JSONL path lives under `~/.clawdis/sessions/<SessionId>.jsonl`.
|
||||||
- **WebChat:** Always attaches to `main`, loads the full session transcript so desktop reflects cross-surface history, and writes new turns back to the same session.
|
- **WebChat:** Always attaches to `main`, loads the full session transcript so desktop reflects cross-surface history, and writes new turns back to the same session.
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ Status: ready for bot-mode use with grammY (long-polling by default; webhook sup
|
||||||
|
|
||||||
## Planned implementation details
|
## Planned implementation details
|
||||||
- Library: grammY is the only client for send + gateway (fetch fallback removed); grammY throttler is enabled by default to stay under Bot API limits.
|
- Library: grammY is the only client for send + gateway (fetch fallback removed); grammY throttler is enabled by default to stay under Bot API limits.
|
||||||
- Inbound normalization: maps Bot API updates to `MsgContext` with `Surface: "telegram"`, `ChatType: direct|group`, `SenderName`, `MediaPath`/`MediaType` when attachments arrive, `Timestamp`, and reply-to metadata (`ReplyToId`, `ReplyToBody`, `ReplyToSender`) when the user replies; groups require @bot mention by default.
|
- Inbound normalization: maps Bot API updates to `MsgContext` with `Surface: "telegram"`, `ChatType: direct|group`, `SenderName`, `MediaPath`/`MediaType` when attachments arrive, `Timestamp`, and reply-to metadata (`ReplyToId`, `ReplyToBody`, `ReplyToSender`) when the user replies; reply context is appended to `Body` as a `[Replying to ...]` block; groups require @bot mention by default.
|
||||||
- Outbound: text and media (photo/video/audio/document) with optional caption; chunked to limits. Typing cue sent best-effort.
|
- Outbound: text and media (photo/video/audio/document) with optional caption; chunked to limits. Typing cue sent best-effort.
|
||||||
- Config: `TELEGRAM_BOT_TOKEN` env or `telegram.botToken` required; `telegram.requireMention`, `telegram.allowFrom`, `telegram.mediaMaxMb`, `telegram.proxy`, `telegram.webhookSecret`, `telegram.webhookUrl`, `telegram.webhookPath` supported.
|
- Config: `TELEGRAM_BOT_TOKEN` env or `telegram.botToken` required; `telegram.requireMention`, `telegram.allowFrom`, `telegram.mediaMaxMb`, `telegram.proxy`, `telegram.webhookSecret`, `telegram.webhookUrl`, `telegram.webhookPath` supported.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,8 @@ describe("createTelegramBot", () => {
|
||||||
|
|
||||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
const payload = replySpy.mock.calls[0][0];
|
const payload = replySpy.mock.calls[0][0];
|
||||||
expect(payload.Body).not.toContain("Reply to Ada: Can you summarize this?");
|
expect(payload.Body).toContain("[Replying to Ada]");
|
||||||
|
expect(payload.Body).toContain("Can you summarize this?");
|
||||||
expect(payload.ReplyToId).toBe("9001");
|
expect(payload.ReplyToId).toBe("9001");
|
||||||
expect(payload.ReplyToBody).toBe("Can you summarize this?");
|
expect(payload.ReplyToBody).toBe("Can you summarize this?");
|
||||||
expect(payload.ReplyToSender).toBe("Ada");
|
expect(payload.ReplyToSender).toBe("Ada");
|
||||||
|
|
|
||||||
|
|
@ -124,13 +124,16 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||||
""
|
""
|
||||||
).trim();
|
).trim();
|
||||||
if (!rawBody) return;
|
if (!rawBody) return;
|
||||||
|
const replySuffix = replyTarget
|
||||||
|
? `\n\n[Replying to ${replyTarget.sender}]\n${replyTarget.body}\n[/Replying]`
|
||||||
|
: "";
|
||||||
const body = formatAgentEnvelope({
|
const body = formatAgentEnvelope({
|
||||||
surface: "Telegram",
|
surface: "Telegram",
|
||||||
from: isGroup
|
from: isGroup
|
||||||
? buildGroupLabel(msg, chatId)
|
? buildGroupLabel(msg, chatId)
|
||||||
: buildSenderLabel(msg, chatId),
|
: buildSenderLabel(msg, chatId),
|
||||||
timestamp: msg.date ? msg.date * 1000 : undefined,
|
timestamp: msg.date ? msg.date * 1000 : undefined,
|
||||||
body: rawBody,
|
body: `${rawBody}${replySuffix}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const ctxPayload = {
|
const ctxPayload = {
|
||||||
|
|
|
||||||
|
|
@ -1786,10 +1786,13 @@ describe("web auto-reply", () => {
|
||||||
ReplyToId?: string;
|
ReplyToId?: string;
|
||||||
ReplyToBody?: string;
|
ReplyToBody?: string;
|
||||||
ReplyToSender?: string;
|
ReplyToSender?: string;
|
||||||
|
Body?: string;
|
||||||
};
|
};
|
||||||
expect(callArg.ReplyToId).toBe("q1");
|
expect(callArg.ReplyToId).toBe("q1");
|
||||||
expect(callArg.ReplyToBody).toBe("original");
|
expect(callArg.ReplyToBody).toBe("original");
|
||||||
expect(callArg.ReplyToSender).toBe("+1999");
|
expect(callArg.ReplyToSender).toBe("+1999");
|
||||||
|
expect(callArg.Body).toContain("[Replying to +1999]");
|
||||||
|
expect(callArg.Body).toContain("original");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies responsePrefix to regular replies", async () => {
|
it("applies responsePrefix to regular replies", async () => {
|
||||||
|
|
|
||||||
|
|
@ -933,6 +933,12 @@ export async function monitorWebProvider(
|
||||||
|
|
||||||
const backgroundTasks = new Set<Promise<unknown>>();
|
const backgroundTasks = new Set<Promise<unknown>>();
|
||||||
|
|
||||||
|
const formatReplyContext = (msg: WebInboundMsg) => {
|
||||||
|
if (!msg.replyToBody) return null;
|
||||||
|
const sender = msg.replyToSender ?? "unknown sender";
|
||||||
|
return `[Replying to ${sender}]\n${msg.replyToBody}\n[/Replying]`;
|
||||||
|
};
|
||||||
|
|
||||||
const buildLine = (msg: WebInboundMsg) => {
|
const buildLine = (msg: WebInboundMsg) => {
|
||||||
// Build message prefix: explicit config > default based on allowFrom
|
// Build message prefix: explicit config > default based on allowFrom
|
||||||
let messagePrefix = cfg.inbound?.messagePrefix;
|
let messagePrefix = cfg.inbound?.messagePrefix;
|
||||||
|
|
@ -945,7 +951,10 @@ export async function monitorWebProvider(
|
||||||
msg.chatType === "group"
|
msg.chatType === "group"
|
||||||
? `${msg.senderName ?? msg.senderE164 ?? "Someone"}: `
|
? `${msg.senderName ?? msg.senderE164 ?? "Someone"}: `
|
||||||
: "";
|
: "";
|
||||||
const baseLine = `${prefixStr}${senderLabel}${msg.body}`;
|
const replyContext = formatReplyContext(msg);
|
||||||
|
const baseLine = `${prefixStr}${senderLabel}${msg.body}${
|
||||||
|
replyContext ? `\n\n${replyContext}` : ""
|
||||||
|
}`;
|
||||||
|
|
||||||
// Wrap with standardized envelope for the agent.
|
// Wrap with standardized envelope for the agent.
|
||||||
return formatAgentEnvelope({
|
return formatAgentEnvelope({
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue