diff --git a/apps/web/__tests__/ai-choose-rule.test.ts b/apps/web/__tests__/ai-choose-rule.test.ts index 1194a7a0ba..0442acc2f2 100644 --- a/apps/web/__tests__/ai-choose-rule.test.ts +++ b/apps/web/__tests__/ai-choose-rule.test.ts @@ -13,7 +13,7 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { test("Should return no rule when no rules passed", async () => { const result = await aiChooseRule({ rules: [], - email: getEmail(), + messages: [getEmail()], user: getUser(), }); @@ -26,7 +26,7 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { ); const result = await aiChooseRule({ - email: getEmail({ subject: "test" }), + messages: [getEmail({ subject: "test" })], rules: [rule], user: getUser(), }); @@ -47,7 +47,7 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { const result = await aiChooseRule({ rules: [rule1, rule2], - email: getEmail({ subject: "remember that call" }), + messages: [getEmail({ subject: "remember that call" })], user: getUser(), }); @@ -80,10 +80,12 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { const result = await aiChooseRule({ rules: [rule1, rule2], - email: getEmail({ - subject: "Joke", - content: "Tell me a joke about sheep", - }), + messages: [ + getEmail({ + subject: "Joke", + content: "Tell me a joke about sheep", + }), + ], user: getUser(), }); @@ -147,11 +149,13 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { test("Should match simple response required", async () => { const result = await aiChooseRule({ rules, - email: getEmail({ - from: "alicesmith@gmail.com", - subject: "Can we meet for lunch tomorrow?", - content: "LMK\n\n--\nAlice Smith,\nCEO, The Boring Fund", - }), + messages: [ + getEmail({ + from: "alicesmith@gmail.com", + subject: "Can we meet for lunch tomorrow?", + content: "LMK\n\n--\nAlice Smith,\nCEO, The Boring Fund", + }), + ], user: getUser(), }); @@ -164,11 +168,13 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { test("Should match technical issues", async () => { const result = await aiChooseRule({ rules, - email: getEmail({ - subject: "Server downtime reported", - content: - "We're experiencing critical server issues affecting production.", - }), + messages: [ + getEmail({ + subject: "Server downtime reported", + content: + "We're experiencing critical server issues affecting production.", + }), + ], user: getUser(), }); @@ -181,10 +187,12 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { test("Should match financial emails", async () => { const result = await aiChooseRule({ rules, - email: getEmail({ - subject: "Your invoice for March 2024", - content: "Please find attached your invoice for services rendered.", - }), + messages: [ + getEmail({ + subject: "Your invoice for March 2024", + content: "Please find attached your invoice for services rendered.", + }), + ], user: getUser(), }); @@ -197,11 +205,13 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { test("Should match recruiter emails", async () => { const result = await aiChooseRule({ rules, - email: getEmail({ - subject: "New job opportunity at Tech Corp", - content: - "I came across your profile and think you'd be perfect for...", - }), + messages: [ + getEmail({ + subject: "New job opportunity at Tech Corp", + content: + "I came across your profile and think you'd be perfect for...", + }), + ], user: getUser(), }); @@ -214,10 +224,12 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { test("Should match legal documents", async () => { const result = await aiChooseRule({ rules, - email: getEmail({ - subject: "Please review: Contract for new project", - content: "Attached is the contract for your review and signature.", - }), + messages: [ + getEmail({ + subject: "Please review: Contract for new project", + content: "Attached is the contract for your review and signature.", + }), + ], user: getUser(), }); @@ -230,10 +242,13 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { test("Should match emails requiring response", async () => { const result = await aiChooseRule({ rules, - email: getEmail({ - subject: "Team lunch tomorrow?", - content: "Would you like to join us for team lunch tomorrow at 12pm?", - }), + messages: [ + getEmail({ + subject: "Team lunch tomorrow?", + content: + "Would you like to join us for team lunch tomorrow at 12pm?", + }), + ], user: getUser(), }); @@ -246,10 +261,12 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { test("Should match product updates", async () => { const result = await aiChooseRule({ rules, - email: getEmail({ - subject: "New Feature Release: AI Integration", - content: "We're excited to announce our new AI features...", - }), + messages: [ + getEmail({ + subject: "New Feature Release: AI Integration", + content: "We're excited to announce our new AI features...", + }), + ], user: getUser(), }); @@ -262,10 +279,12 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { test("Should match marketing emails", async () => { const result = await aiChooseRule({ rules, - email: getEmail({ - subject: "50% off Spring Sale!", - content: "Don't miss out on our biggest sale of the season!", - }), + messages: [ + getEmail({ + subject: "50% off Spring Sale!", + content: "Don't miss out on our biggest sale of the season!", + }), + ], user: getUser(), }); @@ -278,10 +297,12 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { test("Should match team updates", async () => { const result = await aiChooseRule({ rules, - email: getEmail({ - subject: "Weekly Team Update", - content: "Here's what the team accomplished this week...", - }), + messages: [ + getEmail({ + subject: "Weekly Team Update", + content: "Here's what the team accomplished this week...", + }), + ], user: getUser(), }); @@ -294,10 +315,12 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { test("Should match customer feedback", async () => { const result = await aiChooseRule({ rules, - email: getEmail({ - subject: "Customer Feedback: App Performance", - content: "I've been experiencing slow loading times...", - }), + messages: [ + getEmail({ + subject: "Customer Feedback: App Performance", + content: "I've been experiencing slow loading times...", + }), + ], user: getUser(), }); @@ -310,10 +333,12 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => { test("Should match event invitations", async () => { const result = await aiChooseRule({ rules, - email: getEmail({ - subject: "Invitation: Annual Tech Conference", - content: "You're invited to speak at our annual conference...", - }), + messages: [ + getEmail({ + subject: "Invitation: Annual Tech Conference", + content: "You're invited to speak at our annual conference...", + }), + ], user: getUser(), }); diff --git a/apps/web/app/api/google/webhook/process-history-item.ts b/apps/web/app/api/google/webhook/process-history-item.ts index 6be59434aa..17a0b57f5c 100644 --- a/apps/web/app/api/google/webhook/process-history-item.ts +++ b/apps/web/app/api/google/webhook/process-history-item.ts @@ -1,8 +1,7 @@ import type { gmail_v1 } from "@googleapis/gmail"; import prisma from "@/utils/prisma"; -import { emailToContent, parseMessage } from "@/utils/mail"; +import { emailToContent } from "@/utils/mail"; import { GmailLabel } from "@/utils/gmail/label"; -import { getMessage } from "@/utils/gmail/message"; import { runColdEmailBlocker } from "@/utils/cold-email/is-cold-email"; import { runRulesOnMessage } from "@/utils/ai/choose-rule/run-rules"; import { blockUnsubscribedEmails } from "@/app/api/google/webhook/block-unsubscribed-emails"; @@ -16,6 +15,7 @@ import { ColdEmailSetting } from "@prisma/client"; import { logger } from "@/app/api/google/webhook/logger"; import { isIgnoredSender } from "@/utils/filter-ignored-senders"; import { internalDateToDate } from "@/utils/date"; +import { getThreadMessages } from "@/utils/gmail/thread"; export async function processHistoryItem( { @@ -53,8 +53,8 @@ export async function processHistoryItem( logger.info("Getting message", loggerOptions); try { - const [gmailMessage, hasExistingRule] = await Promise.all([ - getMessage(messageId, gmail, "full"), + const [messages, hasExistingRule] = await Promise.all([ + getThreadMessages(threadId, gmail), prisma.executedRule.findUnique({ where: { unique_user_thread_message: { userId: user.id, threadId, messageId }, @@ -69,7 +69,7 @@ export async function processHistoryItem( return; } - const message = parseMessage(gmailMessage); + const message = messages[messages.length - 1]; if (isIgnoredSender(message.headers.from)) { logger.info("Skipping. Ignored sender.", loggerOptions); @@ -169,7 +169,7 @@ export async function processHistoryItem( await runRulesOnMessage({ gmail, - message, + messages, rules, user, isTest: false, diff --git a/apps/web/utils/actions/ai-rule.ts b/apps/web/utils/actions/ai-rule.ts index e071301de6..cb4cdc2ce1 100644 --- a/apps/web/utils/actions/ai-rule.ts +++ b/apps/web/utils/actions/ai-rule.ts @@ -41,6 +41,7 @@ import { labelVisibility } from "@/utils/gmail/constants"; import type { CreateOrUpdateRuleSchemaWithCategories } from "@/utils/ai/rule/create-rule-schema"; import { deleteRule, safeCreateRule, safeUpdateRule } from "@/utils/rule/rule"; import { getUserCategoriesForNames } from "@/utils/category.server"; +import { getThreadMessages } from "@/utils/gmail/thread"; const logger = createScopedLogger("ai-rule"); @@ -75,8 +76,8 @@ export const runRulesAction = withActionInstrumentation( const fetchExecutedRule = !isTest && !rerun; - const [gmailMessage, executedRule] = await Promise.all([ - getMessage(messageId, gmail, "full"), + const [messages, executedRule] = await Promise.all([ + getThreadMessages(threadId, gmail), fetchExecutedRule ? prisma.executedRule.findUnique({ where: { @@ -112,12 +113,10 @@ export const runRulesAction = withActionInstrumentation( }; } - const message = parseMessage(gmailMessage); - const result = await runRulesOnMessage({ isTest, gmail, - message, + messages, rules: user.rules, user: { ...user, email: user.email }, }); @@ -158,21 +157,23 @@ export const testAiCustomContentAction = withActionInstrumentation( const result = await runRulesOnMessage({ isTest: true, gmail, - message: { - id: "testMessageId", - threadId: "testThreadId", - snippet: content, - textPlain: content, - headers: { - date: new Date().toISOString(), - from: "", - to: "", - subject: "", + messages: [ + { + id: "testMessageId", + threadId: "testThreadId", + snippet: content, + textPlain: content, + headers: { + date: new Date().toISOString(), + from: "", + to: "", + subject: "", + }, + historyId: "", + inline: [], + internalDate: new Date().toISOString(), }, - historyId: "", - inline: [], - internalDate: new Date().toISOString(), - }, + ], rules: user.rules, user, }); diff --git a/apps/web/utils/ai/choose-rule/ai-choose-rule.ts b/apps/web/utils/ai/choose-rule/ai-choose-rule.ts index a263a4b322..a492e6c74f 100644 --- a/apps/web/utils/ai/choose-rule/ai-choose-rule.ts +++ b/apps/web/utils/ai/choose-rule/ai-choose-rule.ts @@ -2,26 +2,27 @@ import { z } from "zod"; import type { UserAIFields } from "@/utils/llms/types"; import { chatCompletionObject } from "@/utils/llms"; import type { User } from "@prisma/client"; -import { stringifyEmail } from "@/utils/stringify-email"; +import { + stringifyEmail, + stringifyPreviousEmails, +} from "@/utils/stringify-email"; import type { EmailForLLM } from "@/utils/types"; import { createScopedLogger } from "@/utils/logger"; const logger = createScopedLogger("ai-choose-rule"); type GetAiResponseOptions = { - email: { - from: string; - subject: string; - content: string; - cc?: string; - replyTo?: string; - }; + messages: EmailForLLM[]; user: Pick & UserAIFields; rules: { instructions: string }[]; }; async function getAiResponse(options: GetAiResponseOptions) { - const { email, user, rules } = options; + const { messages, user, rules } = options; + + const lastMessage = messages.at(-1); + + if (!lastMessage) throw new Error("No messages"); const rulesWithUnknownRule = [ ...rules, @@ -56,9 +57,23 @@ Respond with a JSON object with the following fields: Select a rule to apply to the email:`; + // just using 1 message for now + const previousMessages = stringifyPreviousEmails(messages.slice(-2, -1), 300); + + const previous = + messages.length > 1 + ? ` +Previous email in thread for context: + +${previousMessages} +` + : ""; + const prompt = ` -${stringifyEmail(email, 500)} -`; +${stringifyEmail(lastMessage, 500)} + +${previous} +`.trim(); logger.trace("Input", { system, prompt }); @@ -100,16 +115,16 @@ ${stringifyEmail(email, 500)} export async function aiChooseRule< T extends { instructions: string }, >(options: { - email: EmailForLLM; + messages: EmailForLLM[]; rules: T[]; user: Pick & UserAIFields; }) { - const { email, rules, user } = options; + const { messages, rules, user } = options; if (!rules.length) return { reason: "No rules" }; const aiResponse = await getAiResponse({ - email, + messages, rules, user, }); diff --git a/apps/web/utils/ai/choose-rule/match-rules.test.ts b/apps/web/utils/ai/choose-rule/match-rules.test.ts index 248ec1c1f5..e8e2b9f432 100644 --- a/apps/web/utils/ai/choose-rule/match-rules.test.ts +++ b/apps/web/utils/ai/choose-rule/match-rules.test.ts @@ -105,7 +105,7 @@ describe("findMatchingRule", () => { }); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); expect(result.rule?.id).toBe(rule.id); expect(result.reason).toBe("Matched static conditions"); @@ -119,7 +119,7 @@ describe("findMatchingRule", () => { }); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); expect(result.rule?.id).toBe(rule.id); expect(result.reason).toBe("Matched static conditions"); @@ -133,7 +133,7 @@ describe("findMatchingRule", () => { }); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); expect(result.rule?.id).toBeUndefined(); expect(result.reason).toBeUndefined(); @@ -158,7 +158,7 @@ describe("findMatchingRule", () => { }); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); expect(result.rule?.id).toBe(rule.id); expect(result.reason).toBe(`Matched group item: "FROM: test@example.com"`); @@ -177,7 +177,7 @@ describe("findMatchingRule", () => { const message = getMessage(); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); expect(result.rule?.id).toBe(rule.id); expect(result.reason).toBe('Matched category: "category"'); @@ -196,7 +196,7 @@ describe("findMatchingRule", () => { const message = getMessage(); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); expect(result.rule?.id).toBeUndefined(); expect(result.reason).toBeUndefined(); @@ -232,7 +232,7 @@ describe("findMatchingRule", () => { }); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); expect(result.rule?.id).toBe(rule.id); expect(result.reason).toBe(`Matched group item: "FROM: test@example.com"`); @@ -262,7 +262,7 @@ describe("findMatchingRule", () => { }); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); expect(result.rule?.id).toBe(rule.id); expect(result.reason).toBeDefined(); @@ -293,7 +293,7 @@ describe("findMatchingRule", () => { }); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); expect(result.rule).toBeUndefined(); expect(result.reason).toBeDefined(); @@ -315,7 +315,7 @@ describe("findMatchingRule", () => { const message = getMessage(); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); expect(result.rule?.id).toBe(rule.id); expect(result.reason).toBe('Matched category: "category"'); @@ -336,7 +336,7 @@ describe("findMatchingRule", () => { const message = getMessage(); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); expect(result.rule?.id).toBe(rule.id); expect(result.reason).toBe('Matched category: "category"'); @@ -378,7 +378,7 @@ describe("findMatchingRule", () => { }); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); expect(result.rule).toBeUndefined(); expect(result.reason).toBeUndefined(); @@ -418,7 +418,7 @@ describe("findMatchingRule", () => { }); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); expect(result.rule?.id).toBe(rule.id); expect(result.reason).toContain("test@example.com"); @@ -459,7 +459,7 @@ describe("findMatchingRule", () => { }); const user = getUser(); - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, [message], user); // Should match the first rule only expect(result.rule?.id).toBe("rule1"); diff --git a/apps/web/utils/ai/choose-rule/match-rules.ts b/apps/web/utils/ai/choose-rule/match-rules.ts index 01760958a1..a875473d6d 100644 --- a/apps/web/utils/ai/choose-rule/match-rules.ts +++ b/apps/web/utils/ai/choose-rule/match-rules.ts @@ -17,7 +17,6 @@ import { import prisma from "@/utils/prisma"; import { aiChooseRule } from "@/utils/ai/choose-rule/ai-choose-rule"; import { getEmailForLLM } from "@/utils/get-email-from-message"; -import { isReplyInThread } from "@/utils/thread"; import type { UserAIFields } from "@/utils/llms/types"; import { createScopedLogger } from "@/utils/logger"; import type { @@ -165,10 +164,10 @@ function getMatchReason(matchReasons?: MatchReason[]): string | undefined { export async function findMatchingRule( rules: RuleWithActionsAndCategories[], - message: ParsedMessage, + messages: ParsedMessage[], user: Pick & UserAIFields, ) { - const result = await findMatchingRuleWithReasons(rules, message, user); + const result = await findMatchingRuleWithReasons(rules, messages, user); return { ...result, reason: result.reason || getMatchReason(result.matchReasons || []), @@ -177,18 +176,18 @@ export async function findMatchingRule( async function findMatchingRuleWithReasons( rules: RuleWithActionsAndCategories[], - message: ParsedMessage, + messages: ParsedMessage[], user: Pick & UserAIFields, ): Promise<{ rule?: RuleWithActionsAndCategories; matchReasons?: MatchReason[]; reason?: string; }> { - const isThread = isReplyInThread(message.id, message.threadId); + const isThread = messages.length > 1; const { match, matchReasons, potentialMatches } = await findPotentialMatchingRules({ rules, - message, + message: messages[messages.length - 1], isThread, }); @@ -196,7 +195,7 @@ async function findMatchingRuleWithReasons( if (potentialMatches?.length) { const result = await aiChooseRule({ - email: getEmailForLLM(message), + messages: messages.map((m) => getEmailForLLM(m)), rules: potentialMatches, user, }); diff --git a/apps/web/utils/ai/choose-rule/run-rules.ts b/apps/web/utils/ai/choose-rule/run-rules.ts index 17b6113b54..bbc29c2672 100644 --- a/apps/web/utils/ai/choose-rule/run-rules.ts +++ b/apps/web/utils/ai/choose-rule/run-rules.ts @@ -32,25 +32,25 @@ export type RunRulesResult = { export async function runRulesOnMessage({ gmail, - message, + messages, rules, user, isTest, }: { gmail: gmail_v1.Gmail; - message: ParsedMessage; + messages: ParsedMessage[]; rules: RuleWithActionsAndCategories[]; user: Pick & UserAIFields; isTest: boolean; }): Promise { - const result = await findMatchingRule(rules, message, user); + const result = await findMatchingRule(rules, messages, user); logger.trace("Matching rule", { result }); if (result.rule) { return await runRule( result.rule, - message, + messages, user, gmail, result.reason, @@ -58,6 +58,7 @@ export async function runRulesOnMessage({ isTest, ); } else { + const message = messages[messages.length - 1]; await saveSkippedExecutedRule({ userId: user.id, threadId: message.threadId, @@ -70,13 +71,14 @@ export async function runRulesOnMessage({ async function runRule( rule: RuleWithActionsAndCategories, - message: ParsedMessage, + messages: ParsedMessage[], user: Pick & UserAIFields, gmail: gmail_v1.Gmail, reason: string | undefined, matchReasons: MatchReason[] | undefined, isTest: boolean, ) { + const message = messages[messages.length - 1]; const email = getEmailForLLM(message); // get action items with args diff --git a/apps/web/utils/ai/clean/ai-clean.ts b/apps/web/utils/ai/clean/ai-clean.ts index 79d2aa7d98..eb5dc0669d 100644 --- a/apps/web/utils/ai/clean/ai-clean.ts +++ b/apps/web/utils/ai/clean/ai-clean.ts @@ -5,11 +5,11 @@ import type { UserEmailWithAI } from "@/utils/llms/types"; import { createScopedLogger } from "@/utils/logger"; import type { EmailForLLM } from "@/utils/types"; import { - stringifyEmailFromBody, stringifyEmailSimple, + stringifyPreviousEmails, } from "@/utils/stringify-email"; import { formatDateForLLM, formatRelativeTimeForLLM } from "@/utils/date"; -// import { Braintrust } from "@/utils/braintrust"; +import { Braintrust } from "@/utils/braintrust"; const logger = createScopedLogger("ai/clean"); @@ -21,7 +21,7 @@ const schema = z.object({ // reasoning: z.string(), }); -// const braintrust = new Braintrust("cleaner-1"); +const braintrust = new Braintrust("cleaner-1"); export async function aiClean({ user, @@ -72,13 +72,8 @@ However, do archive payment-related communications like overdue payment notifica : "" }`; - const previousMessages = messages - .slice(-3, -1) // Take 2 messages before the last message - .map( - (message) => - `${stringifyEmailFromBody(message).slice(0, 500)}`, - ) - .join("\n"); + // Take the last 2 messages for context + const previousMessages = stringifyPreviousEmails(messages.slice(-2, -1), 300); const previous = messages.length > 1 @@ -125,11 +120,11 @@ The current date is ${currentDate}. logger.trace("Result", { response: aiResponse.object }); - // braintrust.insertToDataset({ - // id: messageId, - // input: { message, previousMessages, currentDate }, - // expected: aiResponse.object, - // }); + braintrust.insertToDataset({ + id: messageId, + input: { message, previousMessages, currentDate }, + expected: aiResponse.object, + }); return aiResponse.object; } diff --git a/apps/web/utils/gmail/thread.ts b/apps/web/utils/gmail/thread.ts index 53a47493c8..dade413591 100644 --- a/apps/web/utils/gmail/thread.ts +++ b/apps/web/utils/gmail/thread.ts @@ -16,6 +16,17 @@ export async function getThread( return thread.data as ThreadWithPayloadMessages; } +export async function getThreadMessages( + threadId: string, + gmail: gmail_v1.Gmail, +) { + const thread = await getThread(threadId, gmail); + if (!thread?.messages) return []; + return thread.messages + .map((m) => parseMessage(m as MessageWithPayload)) + .filter((m) => !m.labelIds?.includes(GmailLabel.DRAFT)); +} + export async function getThreads( q: string, labelIds: string[], @@ -146,14 +157,3 @@ export async function getThreadsFromSenderWithSubject( ) .filter(isDefined); } - -export async function getThreadMessages( - threadId: string, - gmail: gmail_v1.Gmail, -) { - const thread = await getThread(threadId, gmail); - if (!thread?.messages) return []; - return thread.messages - .map((m) => parseMessage(m as MessageWithPayload)) - .filter((m) => !m.labelIds?.includes(GmailLabel.DRAFT)); -} diff --git a/apps/web/utils/reply-tracker/check-previous-emails.ts b/apps/web/utils/reply-tracker/check-previous-emails.ts index 0c4cb5fdc1..862c052dc2 100644 --- a/apps/web/utils/reply-tracker/check-previous-emails.ts +++ b/apps/web/utils/reply-tracker/check-previous-emails.ts @@ -59,7 +59,7 @@ export async function processPreviousSentEmails( } else { // inbound logger.info("Processing inbound reply", loggerOptions); - await handleInboundReply(user, latestMessage, gmail); + await handleInboundReply(user, threadMessages, gmail); } revalidatePath("/reply-zero"); diff --git a/apps/web/utils/reply-tracker/inbound.ts b/apps/web/utils/reply-tracker/inbound.ts index 1682e3f7dc..0eac94e519 100644 --- a/apps/web/utils/reply-tracker/inbound.ts +++ b/apps/web/utils/reply-tracker/inbound.ts @@ -114,7 +114,7 @@ export async function markNeedsReply( // Currently this is used when enabling reply tracking. Otherwise we use regular AI rule processing to handle inbound replies export async function handleInboundReply( user: Pick & UserEmailWithAI, - message: ParsedMessage, + messages: ParsedMessage[], gmail: gmail_v1.Gmail, ) { // 1. Run rules check @@ -126,7 +126,7 @@ export async function handleInboundReply( if (!replyTrackingRule?.instructions) return; const result = await aiChooseRule({ - email: getEmailForLLM(message), + messages: messages.map((m) => getEmailForLLM(m)), rules: [ { id: replyTrackingRule.id, @@ -137,6 +137,8 @@ export async function handleInboundReply( }); if (result.rule?.id === replyTrackingRule.id) { + const message = messages[messages.length - 1]; + await markNeedsReply( user.id, user.email, diff --git a/apps/web/utils/stringify-email.ts b/apps/web/utils/stringify-email.ts index e7b9a6bf60..27163c02bc 100644 --- a/apps/web/utils/stringify-email.ts +++ b/apps/web/utils/stringify-email.ts @@ -32,3 +32,15 @@ export function stringifyEmailFromBody(email: EmailForLLM) { return emailParts.filter(Boolean).join("\n"); } + +export function stringifyPreviousEmails( + messages: EmailForLLM[], + maxLength: number, +) { + return messages + .map( + (message) => + `${stringifyEmailFromBody(message).slice(0, maxLength)}`, + ) + .join("\n"); +}