From f8f9306ce7364c9857511008361027035f685a7b Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:00:45 -0500 Subject: [PATCH 1/8] Handle ai args better for duplicate action types --- apps/web/app/api/google/webhook/group-rule.ts | 23 ++++---- .../web/app/api/google/webhook/static-rule.ts | 23 ++++---- .../utils/ai/choose-rule/ai-choose-args.ts | 56 +++++++++---------- apps/web/utils/ai/choose-rule/choose.ts | 36 ++++++------ 4 files changed, 64 insertions(+), 74 deletions(-) diff --git a/apps/web/app/api/google/webhook/group-rule.ts b/apps/web/app/api/google/webhook/group-rule.ts index df99732017..ec44b0a7d2 100644 --- a/apps/web/app/api/google/webhook/group-rule.ts +++ b/apps/web/app/api/google/webhook/group-rule.ts @@ -3,7 +3,6 @@ import type { ParsedMessage } from "@/utils/types"; import type { User } from "@prisma/client"; import { emailToContent } from "@/utils/mail"; import { - getActionItemsFromAiArgsResponse, getActionsWithParameters, getArgsAiResponse, } from "@/utils/ai/choose-rule/ai-choose-args"; @@ -61,19 +60,17 @@ export async function handleGroupRule({ }; // generate args - const aiArgsResponse = - getActionsWithParameters(match.rule.actions).length > 0 - ? await getArgsAiResponse({ - email, - selectedRule: match.rule, - user, - }) - : undefined; + const shouldAiGenerateArgs = + getActionsWithParameters(match.rule.actions).length > 0; + const aiArgsResponse = shouldAiGenerateArgs + ? await getArgsAiResponse({ + email, + selectedRule: match.rule, + user, + }) + : match.rule.actions; - const actionItems = getActionItemsFromAiArgsResponse( - aiArgsResponse, - match.rule.actions, - ); + const actionItems = aiArgsResponse || match.rule.actions; // handle action // TODO isThread check to skip diff --git a/apps/web/app/api/google/webhook/static-rule.ts b/apps/web/app/api/google/webhook/static-rule.ts index f0772c811e..113c36426c 100644 --- a/apps/web/app/api/google/webhook/static-rule.ts +++ b/apps/web/app/api/google/webhook/static-rule.ts @@ -2,7 +2,6 @@ import type { gmail_v1 } from "@googleapis/gmail"; import type { ParsedMessage, RuleWithActions } from "@/utils/types"; import { RuleType, type User } from "@prisma/client"; import { - getActionItemsFromAiArgsResponse, getActionsWithParameters, getArgsAiResponse, } from "@/utils/ai/choose-rule/ai-choose-args"; @@ -53,19 +52,17 @@ export async function handleStaticRule({ }; // generate args - const aiArgsResponse = - getActionsWithParameters(staticRule.actions).length > 0 - ? await getArgsAiResponse({ - email, - selectedRule: staticRule, - user, - }) - : undefined; + const shouldAiGenerateArgs = + getActionsWithParameters(staticRule.actions).length > 0; + const aiArgsResponse = shouldAiGenerateArgs + ? await getArgsAiResponse({ + email, + selectedRule: staticRule, + user, + }) + : staticRule.actions; - const actionItems = getActionItemsFromAiArgsResponse( - aiArgsResponse, - staticRule.actions, - ); + const actionItems = aiArgsResponse || staticRule.actions; // handle action // TODO isThread check to skip diff --git a/apps/web/utils/ai/choose-rule/ai-choose-args.ts b/apps/web/utils/ai/choose-rule/ai-choose-args.ts index 814ec571a2..282372c9ce 100644 --- a/apps/web/utils/ai/choose-rule/ai-choose-args.ts +++ b/apps/web/utils/ai/choose-rule/ai-choose-args.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import type { UserAIFields } from "@/utils/llms/types"; import type { ActionItem } from "@/utils/ai/actions"; -import type { Action, ActionType, User } from "@prisma/client"; +import type { Action, User } from "@prisma/client"; import { chatCompletionTools } from "@/utils/llms"; import { type EmailForLLM, @@ -9,11 +9,6 @@ import { } from "@/utils/ai/choose-rule/stringify-email"; import { type RuleWithActions, isDefined } from "@/utils/types"; -type AIGeneratedArgs = Record< - ActionType, - Record, string> ->; - // Returns parameters for a zod.object for the rule that must be AI generated function getToolParametersForRule(actions: Action[]) { const actionsWithParameters = getActionsWithParameters(actions); @@ -21,16 +16,22 @@ function getToolParametersForRule(actions: Action[]) { // handle duplicate keys. e.g. "draft_email" and "draft_email" becomes: "draft_email" and "draft_email_2" // this is quite an edge case but need to handle regardless for when it happens const typeCount: Record = {}; - const parameters: Record> = {}; + const parameters: Record< + string, + { action: Action; parameters: z.ZodObject> } + > = {}; for (const action of actionsWithParameters) { // count how many times we have already had this type typeCount[action.type] = (typeCount[action.type] || 0) + 1; - parameters[ + const key = typeCount[action.type] === 1 ? action.type - : `${action.type}_${typeCount[action.type]}` - ] = action.parameters; + : `${action.type}_${typeCount[action.type]}`; + parameters[key] = { + action: action.action, + parameters: action.parameters, + }; } return parameters; @@ -48,6 +49,7 @@ export function getActionsWithParameters(actions: Action[]) { return { type: action.type, parameters, + action, }; }) .filter(isDefined); @@ -87,7 +89,7 @@ export async function getArgsAiResponse({ email: EmailForLLM; user: Pick & UserAIFields; selectedRule: RuleWithActions; -}) { +}): Promise { console.log( `Generating args for rule ${selectedRule.name} (${selectedRule.id})`, ); @@ -130,7 +132,14 @@ ${stringifyEmail(email, 3000)} tools: { apply_rule: { description: "Apply the rule with the given arguments", - parameters: z.object(parameters), + parameters: z.object( + Object.fromEntries( + Object.entries(parameters).map(([key, { parameters }]) => [ + key, + parameters, + ]), + ), + ), }, }, label: "Args for rule", @@ -141,25 +150,12 @@ ${stringifyEmail(email, 3000)} if (!toolCall?.toolName) return; - return toolCall.args; -} - -export function getActionItemsFromAiArgsResponse( - response: AIGeneratedArgs | undefined, - ruleActions: Action[], -) { - return ruleActions.map((ra) => { - // use prefilled values where we have them - const a = response?.[ra.type] || ({} as any); - + const actionItems = Object.entries(parameters).map(([key, { action }]) => { return { - type: ra.type, - label: ra.labelPrompt ? a.label : ra.label, - subject: ra.subjectPrompt ? a.subject : ra.subject, - content: ra.contentPrompt ? a.content : ra.content, - to: ra.toPrompt ? a.to : ra.to, - cc: ra.ccPrompt ? a.cc : ra.cc, - bcc: ra.bccPrompt ? a.bcc : ra.bcc, + ...action, + ...toolCall.args[key], }; }); + + return actionItems; } diff --git a/apps/web/utils/ai/choose-rule/choose.ts b/apps/web/utils/ai/choose-rule/choose.ts index 908ef38403..4a40eb4a38 100644 --- a/apps/web/utils/ai/choose-rule/choose.ts +++ b/apps/web/utils/ai/choose-rule/choose.ts @@ -1,7 +1,6 @@ import type { ActionItem } from "@/utils/ai/actions"; import { getArgsAiResponse, - getActionItemsFromAiArgsResponse, getActionsWithParameters, } from "@/utils/ai/choose-rule/ai-choose-args"; import { getAiResponse } from "@/utils/ai/choose-rule/ai-choose-rule"; @@ -46,22 +45,23 @@ export async function chooseRule(options: ChooseRuleOptions): Promise< const shouldAiGenerateArgs = getActionsWithParameters(selectedRule.actions).length > 0; - const aiArgsResponse = shouldAiGenerateArgs - ? await getArgsAiResponse({ - ...options, - email, - selectedRule, - }) - : undefined; + if (shouldAiGenerateArgs) { + const aiArgsResponse = await getArgsAiResponse({ + ...options, + email, + selectedRule, + }); - const actionItems = getActionItemsFromAiArgsResponse( - aiArgsResponse, - selectedRule.actions, - ); - - return { - rule: selectedRule, - actionItems, - reason: aiResponse?.reason, - }; + return { + rule: selectedRule, + actionItems: aiArgsResponse || [], + reason: aiResponse?.reason, + }; + } else { + return { + rule: selectedRule, + actionItems: selectedRule.actions, + reason: aiResponse?.reason, + }; + } } From 8c87b8a89df8bbad3b70c6cc48fe6b3c013f0191 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:24:20 -0500 Subject: [PATCH 2/8] Don't add extra fields to db --- apps/web/utils/ai/choose-rule/ai-choose-args.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/web/utils/ai/choose-rule/ai-choose-args.ts b/apps/web/utils/ai/choose-rule/ai-choose-args.ts index 282372c9ce..3e1a682f49 100644 --- a/apps/web/utils/ai/choose-rule/ai-choose-args.ts +++ b/apps/web/utils/ai/choose-rule/ai-choose-args.ts @@ -151,10 +151,16 @@ ${stringifyEmail(email, 3000)} if (!toolCall?.toolName) return; const actionItems = Object.entries(parameters).map(([key, { action }]) => { - return { - ...action, - ...toolCall.args[key], + const actionItem: ActionItem = { + type: action.type, + label: toolCall.args[key].label || action.label, + subject: toolCall.args[key].subject || action.subject, + content: toolCall.args[key].content || action.content, + to: toolCall.args[key].to || action.to, + cc: toolCall.args[key].cc || action.cc, + bcc: toolCall.args[key].bcc || action.bcc, }; + return actionItem; }); return actionItems; From 4910a1b63d454e3bf05309a1b829daeba2359932 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:24:26 -0500 Subject: [PATCH 3/8] Prettier logger --- .../app/api/google/webhook/process-history.ts | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/apps/web/app/api/google/webhook/process-history.ts b/apps/web/app/api/google/webhook/process-history.ts index 45aac8993f..63c418ca9b 100644 --- a/apps/web/app/api/google/webhook/process-history.ts +++ b/apps/web/app/api/google/webhook/process-history.ts @@ -25,6 +25,14 @@ import { blockUnsubscribedEmails } from "@/app/api/google/webhook/block-unsubscr import { categorizeSender } from "@/utils/actions/categorize"; import { unwatchEmails } from "@/app/api/google/watch/controller"; +const scope = "Process History"; +const log = (message: string) => { + console.log(`[${scope}]: ${message}`); +}; +const logError = (message: string, error?: unknown, extra?: unknown) => { + console.error(`[${scope}]: ${message}`, error, extra); +}; + export async function processHistoryForUser( decodedData: { emailAddress: string; @@ -70,7 +78,7 @@ export async function processHistoryForUser( }); if (!account) { - console.error(`Google webhook: Account not found. email: ${email}`); + logError(`Account not found. email: ${email}`); return NextResponse.json({ ok: true }); } @@ -79,7 +87,7 @@ export async function processHistoryForUser( : undefined; if (!premium) { - console.log(`Google webhook: Account not premium. email: ${email}`); + log(`Account not premium. email: ${email}`); await unwatchEmails(account); return NextResponse.json({ ok: true }); } @@ -94,9 +102,7 @@ export async function processHistoryForUser( ); if (!userHasAiAccess && !userHasColdEmailAccess) { - console.debug( - `Google webhook: does not have hasAiOrColdEmailAccess. email: ${email}`, - ); + console.debug(`does not have hasAiOrColdEmailAccess. email: ${email}`); await unwatchEmails(account); return NextResponse.json({ ok: true }); } @@ -107,13 +113,13 @@ export async function processHistoryForUser( account.user.coldEmailBlocker !== ColdEmailSetting.DISABLED; if (!hasAutomationRules && !shouldBlockColdEmails) { console.debug( - `Google webhook: has no rules set and cold email blocker disabled. email: ${email}`, + `has no rules set and cold email blocker disabled. email: ${email}`, ); return NextResponse.json({ ok: true }); } if (!account.access_token || !account.refresh_token) { - console.error( + logError( `Missing access or refresh token. User needs to re-authenticate. email: ${email}`, ); return NextResponse.json({ ok: true }); @@ -121,7 +127,7 @@ export async function processHistoryForUser( if (!account.user.email) { // shouldn't ever happen - console.error("Missing user email.", email); + logError(`Missing user email: ${email}`); return NextResponse.json({ ok: true }); } @@ -145,8 +151,8 @@ export async function processHistoryForUser( historyId - 500, // avoid going too far back ).toString(); - console.log( - `Webhook: Listing history... Start: ${startHistoryId} lastSyncedHistoryId: ${account.user.lastSyncedHistoryId} gmailHistoryId: ${startHistoryId} email: ${email}`, + log( + `Listing history... Start: ${startHistoryId} lastSyncedHistoryId: ${account.user.lastSyncedHistoryId} gmailHistoryId: ${startHistoryId} email: ${email}`, ); const history = await gmail.users.history.list({ @@ -160,8 +166,8 @@ export async function processHistoryForUser( }); if (history.data.history) { - console.log( - `Webhook: Processing... email: ${email} startHistoryId: ${startHistoryId} historyId: ${history.data.historyId}`, + log( + `Processing... email: ${email} startHistoryId: ${startHistoryId} historyId: ${history.data.historyId}`, ); await processHistory({ @@ -186,8 +192,8 @@ export async function processHistoryForUser( }, }); } else { - console.log( - `Webhook: No history. startHistoryId: ${startHistoryId}. ${JSON.stringify(decodedData)}`, + log( + `No history. startHistoryId: ${startHistoryId}. ${JSON.stringify(decodedData)}`, ); // important to save this or we can get into a loop with never receiving history @@ -197,12 +203,12 @@ export async function processHistoryForUser( }); } - console.log(`Webhook: Completed. ${JSON.stringify(decodedData)}`); + log(`Completed. ${JSON.stringify(decodedData)}`); return NextResponse.json({ ok: true }); } catch (error) { captureException(error, { extra: { decodedData } }, email); - console.error("Error processing webhook", error, decodedData); + logError("Error processing webhook", error, decodedData); return NextResponse.json({ error: true }); // be careful about calling an error here with the wrong settings, as otherwise PubSub will call the webhook over and over // return NextResponse.error(); @@ -260,7 +266,7 @@ async function processHistory(options: ProcessHistoryOptions) { { extra: { email, messageId: m.message?.id } }, email, ); - console.error(`Error processing history item. email: ${email}`, error); + logError(`Error processing history item. email: ${email}`, error); } } } @@ -292,7 +298,7 @@ async function processHistoryItem( if (!messageId) return; if (!threadId) return; - console.log( + log( `Getting message... email: ${user.email} messageId: ${messageId} threadId: ${threadId}`, ); @@ -309,7 +315,7 @@ async function processHistoryItem( // if the rule has already been executed, skip if (hasExistingRule) { - console.log("Skipping. Rule already exists."); + log("Skipping. Rule already exists."); return; } @@ -326,7 +332,7 @@ async function processHistoryItem( }); if (blocked) { - console.log( + log( `Skipping. Blocked unsubscribed email. email: ${user.email} messageId: ${messageId} threadId: ${threadId}`, ); return; @@ -341,7 +347,7 @@ async function processHistoryItem( ); if (shouldRunBlocker) { - console.log("Running cold email blocker..."); + log("Running cold email blocker..."); const hasPreviousEmail = await hasPreviousEmailsFromSenderOrDomain( gmail, @@ -386,7 +392,7 @@ async function processHistoryItem( } if (hasAutomationRules && hasAiAutomationAccess) { - console.log("Running rules..."); + log("Running rules..."); await runRulesOnMessage({ gmail, @@ -399,7 +405,7 @@ async function processHistoryItem( } catch (error: any) { // gmail bug or snoozed email: https://stackoverflow.com/questions/65290987/gmail-api-getmessage-method-returns-404-for-message-gotten-from-listhistory-meth if (error.message === "Requested entity was not found.") { - console.log( + log( `Message not found. email: ${user.email} messageId: ${messageId} threadId: ${threadId}`, ); return; From 77f5a7e92566f87bf801af79691aa82a406e3546 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:04:38 -0500 Subject: [PATCH 4/8] Adjust copy --- apps/web/app/(app)/automation/Rules.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/app/(app)/automation/Rules.tsx b/apps/web/app/(app)/automation/Rules.tsx index 6050abea37..242b86353f 100644 --- a/apps/web/app/(app)/automation/Rules.tsx +++ b/apps/web/app/(app)/automation/Rules.tsx @@ -245,7 +245,7 @@ export function Rules() {