Add buildReplyHeader helper with truncation and media descriptors
This commit is contained in:
@@ -3,7 +3,7 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import type { Transcriber } from "../stt/openai.js";
|
import type { Transcriber } from "../stt/openai.js";
|
||||||
import { type InboundDeps, normalizeMessage, saveToInbox } from "./inbound.js";
|
import { buildReplyHeader, type InboundDeps, normalizeMessage, saveToInbox } from "./inbound.js";
|
||||||
|
|
||||||
describe("normalizeMessage", () => {
|
describe("normalizeMessage", () => {
|
||||||
let dir: string;
|
let dir: string;
|
||||||
@@ -177,3 +177,112 @@ describe("saveToInbox", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("buildReplyHeader", () => {
|
||||||
|
const BOT_ID = 42;
|
||||||
|
|
||||||
|
it("returns undefined when there is no reply and no quote", () => {
|
||||||
|
expect(buildReplyHeader(undefined, undefined, BOT_ID)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("labels a plain reply to a bot message with 'your message'", () => {
|
||||||
|
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, text: "Which file?" }, undefined, BOT_ID);
|
||||||
|
expect(header).toBe('[Reply to your message: "Which file?"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("labels a plain reply to the owner's own earlier message", () => {
|
||||||
|
const header = buildReplyHeader({ from: { id: 99, is_bot: false }, text: "Buy milk" }, undefined, BOT_ID);
|
||||||
|
expect(header).toBe('[Reply to your own earlier message: "Buy milk"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to 'the earlier message' when `from` is absent", () => {
|
||||||
|
const header = buildReplyHeader({ text: "hello" }, undefined, BOT_ID);
|
||||||
|
expect(header).toBe('[Reply to the earlier message: "hello"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the quote text and 'quoting your message' wording for a reply-with-quote to the bot", () => {
|
||||||
|
const header = buildReplyHeader(
|
||||||
|
{ from: { id: BOT_ID, is_bot: true }, text: "The full assistant answer with lots of context." },
|
||||||
|
{ text: "assistant answer" },
|
||||||
|
BOT_ID,
|
||||||
|
);
|
||||||
|
expect(header).toBe('[Reply quoting your message: "assistant answer"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses 'quoting your own earlier message' when the source is the owner's own", () => {
|
||||||
|
const header = buildReplyHeader(
|
||||||
|
{ from: { id: 99, is_bot: false }, text: "shopping list" },
|
||||||
|
{ text: "shopping" },
|
||||||
|
BOT_ID,
|
||||||
|
);
|
||||||
|
expect(header).toBe('[Reply quoting your own earlier message: "shopping"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats an empty quote.text as if there were no quote (falls through to plain reply)", () => {
|
||||||
|
const header = buildReplyHeader(
|
||||||
|
{ from: { id: BOT_ID, is_bot: true }, text: "The answer" },
|
||||||
|
{ text: "" },
|
||||||
|
BOT_ID,
|
||||||
|
);
|
||||||
|
expect(header).toBe('[Reply to your message: "The answer"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("describes a photo-only replied-to message", () => {
|
||||||
|
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, photo: [{}] }, undefined, BOT_ID);
|
||||||
|
expect(header).toBe('[Reply to your message: "(photo)"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("describes a voice-only replied-to message", () => {
|
||||||
|
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, voice: {} }, undefined, BOT_ID);
|
||||||
|
expect(header).toBe('[Reply to your message: "(voice message)"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("describes a document with a filename", () => {
|
||||||
|
const header = buildReplyHeader(
|
||||||
|
{ from: { id: BOT_ID, is_bot: true }, document: { file_name: "notes.txt" } },
|
||||||
|
undefined,
|
||||||
|
BOT_ID,
|
||||||
|
);
|
||||||
|
expect(header).toBe('[Reply to your message: "(document: notes.txt)"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("describes an unnamed document", () => {
|
||||||
|
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, document: {} }, undefined, BOT_ID);
|
||||||
|
expect(header).toBe('[Reply to your message: "(document)"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the caption instead of the media descriptor when both are present", () => {
|
||||||
|
const header = buildReplyHeader(
|
||||||
|
{ from: { id: BOT_ID, is_bot: true }, photo: [{}], caption: "the sunset" },
|
||||||
|
undefined,
|
||||||
|
BOT_ID,
|
||||||
|
);
|
||||||
|
expect(header).toBe('[Reply to your message: "the sunset"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("truncates replied-to text longer than 500 characters with an ellipsis", () => {
|
||||||
|
const long = "a".repeat(600);
|
||||||
|
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, text: long }, undefined, BOT_ID);
|
||||||
|
expect(header).toBe(`[Reply to your message: "${"a".repeat(499)}…"]`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("truncates a very long quote in the same way", () => {
|
||||||
|
const long = "b".repeat(600);
|
||||||
|
const header = buildReplyHeader(
|
||||||
|
{ from: { id: BOT_ID, is_bot: true }, text: "irrelevant" },
|
||||||
|
{ text: long },
|
||||||
|
BOT_ID,
|
||||||
|
);
|
||||||
|
expect(header).toBe(`[Reply quoting your message: "${"b".repeat(499)}…"]`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("labels as 'your message' when botUserId is missing but is_bot is true", () => {
|
||||||
|
const header = buildReplyHeader({ from: { id: 0, is_bot: true }, text: "hi" }, undefined, undefined);
|
||||||
|
expect(header).toBe('[Reply to your message: "hi"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to '(message)' when the replied-to message has no usable fields", () => {
|
||||||
|
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true } }, undefined, BOT_ID);
|
||||||
|
expect(header).toBe('[Reply to your message: "(message)"]');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -164,3 +164,43 @@ export async function saveToInbox(workspaceDir: string, originalName: string, by
|
|||||||
function errorText(err: unknown): string {
|
function errorText(err: unknown): string {
|
||||||
return err instanceof Error ? err.message : String(err);
|
return err instanceof Error ? err.message : String(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Builds the `[Reply ...]` header line for the agent, or returns undefined when there is nothing to reply to. */
|
||||||
|
export function buildReplyHeader(
|
||||||
|
reply: RepliedMessage | undefined,
|
||||||
|
quote: Quote | undefined,
|
||||||
|
botUserId: number | undefined,
|
||||||
|
): string | undefined {
|
||||||
|
if (!reply) return undefined;
|
||||||
|
const isFromBot =
|
||||||
|
reply.from !== undefined &&
|
||||||
|
(botUserId !== undefined ? reply.from.id === botUserId : reply.from.is_bot === true);
|
||||||
|
const isFromOwner = reply.from !== undefined && !isFromBot;
|
||||||
|
const who = isFromBot
|
||||||
|
? "your message"
|
||||||
|
: isFromOwner
|
||||||
|
? "your own earlier message"
|
||||||
|
: "the earlier message";
|
||||||
|
const quoted = quote?.text ?? "";
|
||||||
|
if (quoted.length > 0) {
|
||||||
|
return `[Reply quoting ${who}: "${truncateReplyContext(quoted)}"]`;
|
||||||
|
}
|
||||||
|
const content = truncateReplyContext(replyContent(reply));
|
||||||
|
return `[Reply to ${who}: "${content}"]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replyContent(reply: RepliedMessage): string {
|
||||||
|
if (reply.text && reply.text.length > 0) return reply.text;
|
||||||
|
if (reply.caption && reply.caption.length > 0) return reply.caption;
|
||||||
|
if (reply.photo && reply.photo.length > 0) return "(photo)";
|
||||||
|
if (reply.voice) return "(voice message)";
|
||||||
|
if (reply.audio) return reply.audio.file_name ? `(audio: ${reply.audio.file_name})` : "(audio)";
|
||||||
|
if (reply.document) return reply.document.file_name ? `(document: ${reply.document.file_name})` : "(document)";
|
||||||
|
return "(message)";
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateReplyContext(text: string): string {
|
||||||
|
const chars = Array.from(text);
|
||||||
|
if (chars.length <= MAX_REPLY_CONTEXT_CHARS) return text;
|
||||||
|
return `${chars.slice(0, MAX_REPLY_CONTEXT_CHARS - 1).join("")}…`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user