From fd180776f1d50c3a09ef4e085bf8ebc49b795cc4 Mon Sep 17 00:00:00 2001 From: Eduardo Lelis Date: Thu, 24 Jul 2025 20:18:28 -0300 Subject: [PATCH 01/20] Add persona report --- .../app/(reports)/api/email-analysis/route.ts | 1016 +++++++++++++++++ .../(reports)/api/email-summaries/route.ts | 351 ++++++ .../app/(reports)/api/gmail-labels/route.ts | 94 ++ .../app/(reports)/api/sandbox-health/route.ts | 17 + .../api/test-error-handling/route.ts | 42 + .../web/app/(reports)/email-analysis/page.tsx | 627 ++++++++++ apps/web/app/(reports)/layout.tsx | 131 +++ 7 files changed, 2278 insertions(+) create mode 100644 apps/web/app/(reports)/api/email-analysis/route.ts create mode 100644 apps/web/app/(reports)/api/email-summaries/route.ts create mode 100644 apps/web/app/(reports)/api/gmail-labels/route.ts create mode 100644 apps/web/app/(reports)/api/sandbox-health/route.ts create mode 100644 apps/web/app/(reports)/api/test-error-handling/route.ts create mode 100644 apps/web/app/(reports)/email-analysis/page.tsx create mode 100644 apps/web/app/(reports)/layout.tsx diff --git a/apps/web/app/(reports)/api/email-analysis/route.ts b/apps/web/app/(reports)/api/email-analysis/route.ts new file mode 100644 index 0000000000..3963f51db1 --- /dev/null +++ b/apps/web/app/(reports)/api/email-analysis/route.ts @@ -0,0 +1,1016 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { getGmailClientWithRefresh } from "@/utils/gmail/client"; +import { getMessage, getMessages } from "@/utils/gmail/message"; +import { parseMessage } from "@/utils/mail"; +import { chatCompletionObject } from "@/utils/llms"; +import { redis } from "@/utils/redis"; +import { env } from "@/env"; +import prisma from "@/utils/prisma"; +import { extractGmailSignature } from "@/utils/gmail/signature"; +import { GmailLabel } from "@/utils/gmail/label"; + +// Email summary schema (reused from email-summaries endpoint) +const EmailSummarySchema = z.object({ + summary: z.string().describe("Brief summary of the email content"), + sender: z.string().describe("Email sender"), + subject: z.string().describe("Email subject"), + category: z + .string() + .describe("Category of the email (work, personal, marketing, etc.)"), +}); + +const ExecutiveSummarySchema = z.object({ + userProfile: z.object({ + persona: z + .string() + .describe( + "1-5 word persona identification (e.g., 'Tech Startup Founder')", + ), + confidence: z + .number() + .min(0) + .max(100) + .describe("Confidence level in persona identification (0-100)"), + }), + topInsights: z + .array( + z.object({ + insight: z.string().describe("Key insight about user's email behavior"), + priority: z + .enum(["high", "medium", "low"]) + .describe("Priority level of this insight"), + icon: z.string().describe("Single emoji representing this insight"), + }), + ) + .describe("3-5 most important findings from the analysis"), + quickActions: z + .array( + z.object({ + action: z + .string() + .describe("Specific action the user can take immediately"), + difficulty: z + .enum(["easy", "medium", "hard"]) + .describe("How difficult this action is to implement"), + impact: z + .enum(["high", "medium", "low"]) + .describe("Expected impact of this action"), + }), + ) + .describe("4-6 immediate actions the user can take"), +}); + +const UserPersonaSchema = z.object({ + professionalIdentity: z.object({ + persona: z.string().describe("Professional persona identification"), + supportingEvidence: z + .array(z.string()) + .describe("Evidence supporting this persona identification"), + }), + currentPriorities: z + .array(z.string()) + .describe("Current professional priorities based on email content"), +}); + +const EmailBehaviorSchema = z.object({ + timingPatterns: z.object({ + peakHours: z.array(z.string()).describe("Peak email activity hours"), + responsePreference: z.string().describe("Preferred response timing"), + frequency: z.string().describe("Overall email frequency"), + }), + contentPreferences: z.object({ + preferred: z + .array(z.string()) + .describe("Types of emails user engages with"), + avoided: z + .array(z.string()) + .describe("Types of emails user typically ignores"), + }), + engagementTriggers: z + .array(z.string()) + .describe("What prompts user to take action on emails"), +}); + +// Response Patterns Schema (updated from original) +const ResponsePatternsSchema = z.object({ + commonResponses: z.array( + z.object({ + pattern: z.string().describe("Description of the response pattern"), + example: z.string().describe("Example of this type of response"), + frequency: z + .number() + .describe("Percentage of responses using this pattern"), + triggers: z + .array(z.string()) + .describe("What types of emails trigger this response"), + }), + ), + suggestedTemplates: z.array( + z.object({ + templateName: z.string().describe("Name of the email template"), + template: z.string().describe("The actual email template text"), + useCase: z.string().describe("When to use this template"), + }), + ), + categoryOrganization: z.array( + z.object({ + category: z.string().describe("Email category name"), + description: z + .string() + .describe("What types of emails belong in this category"), + emailCount: z + .number() + .describe("Estimated number of emails in this category"), + priority: z + .enum(["high", "medium", "low"]) + .describe("Priority level for this category"), + }), + ), +}); + +// Label Analysis Schema +const LabelAnalysisSchema = z.object({ + optimizationSuggestions: z.array( + z.object({ + type: z + .enum(["create", "consolidate", "rename", "delete"]) + .describe("Type of optimization"), + suggestion: z.string().describe("Specific suggestion"), + reason: z.string().describe("Reason for this suggestion"), + impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), + }), + ), +}); + +// Actionable Recommendations Schema +const ActionableRecommendationsSchema = z.object({ + immediateActions: z.array( + z.object({ + action: z.string().describe("Specific action to take"), + difficulty: z + .enum(["easy", "medium", "hard"]) + .describe("Implementation difficulty"), + impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), + timeRequired: z.string().describe("Time required (e.g., '5 minutes')"), + }), + ), + shortTermImprovements: z.array( + z.object({ + improvement: z.string().describe("Improvement to implement"), + timeline: z.string().describe("When to implement (e.g., 'This week')"), + expectedBenefit: z.string().describe("Expected benefit"), + }), + ), + longTermStrategy: z.array( + z.object({ + strategy: z.string().describe("Strategic initiative"), + description: z.string().describe("Detailed description"), + successMetrics: z.array(z.string()).describe("How to measure success"), + }), + ), +}); + +type EmailSummary = z.infer; + +/** + * Generate Executive Summary + */ +async function generateExecutiveSummary( + emailSummaries: EmailSummary[], + sentEmailSummaries: EmailSummary[], + gmailLabels: any[], +): Promise> { + const system = `You are a professional persona identification expert. Your primary task is to accurately identify the user's professional role based on their email patterns. + +CRITICAL: The persona must be a specific, recognizable professional role that clearly identifies what this person does for work. + +Examples of GOOD personas: +- "Startup Founder" +- "Software Developer" +- "Real Estate Agent" +- "Marketing Manager" +- "Sales Executive" +- "Product Manager" +- "Consultant" +- "Teacher" +- "Lawyer" +- "Doctor" +- "Influencer" +- "Freelance Designer" + +Examples of BAD personas (too vague): +- "Professional" +- "Business Person" +- "Tech Worker" +- "Knowledge Worker" + +Focus on identifying the PRIMARY professional role based on email content, senders, and communication patterns.`; + + const prompt = `### Email Analysis Data + +**Received Emails (${emailSummaries.length} emails):** +${emailSummaries + .slice(0, 30) + .map( + (email, i) => + `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, + ) + .join("\n")} + +**Sent Emails (${sentEmailSummaries.length} emails):** +${sentEmailSummaries + .slice(0, 15) + .map( + (email, i) => + `${i + 1}. To: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, + ) + .join("\n")} + +**Current Gmail Labels:** +${gmailLabels.map((label) => `- ${label.name} (${label.messagesTotal || 0} emails)`).join("\n")} + +--- + +**PERSONA IDENTIFICATION INSTRUCTIONS:** + +Analyze the email patterns to identify the user's PRIMARY professional role: + +1. **Look for role indicators:** + - Who do they email? (clients, team members, investors, customers, etc.) + - What topics dominate? (code reviews, property listings, campaign metrics, etc.) + - What language/terminology is used? (technical terms, industry jargon, etc.) + - What responsibilities are evident? (managing teams, closing deals, creating content, etc.) + +2. **Common professional patterns:** + - **Founder/CEO**: Investor emails, team management, strategic decisions, fundraising + - **Developer**: Code reviews, technical discussions, GitHub notifications, deployment issues + - **Sales**: CRM notifications, client outreach, deal discussions, quota tracking + - **Marketing**: Campaign metrics, content creation, social media, analytics + - **Real Estate**: Property listings, client communications, MLS notifications + - **Consultant**: Client projects, proposals, expertise sharing, industry updates + - **Teacher**: Student communications, educational content, institutional emails + +3. **Confidence level:** + - 90-100%: Very clear indicators, consistent patterns + - 70-89%: Strong indicators, some ambiguity + - 50-69%: Mixed signals, multiple possible roles + - Below 50%: Unclear or insufficient data + +Generate: +1. **Specific professional persona** (1-3 words max, e.g., "Software Developer", "Real Estate Agent") +2. **Confidence level** based on clarity of evidence +3. **Top insights** about their email behavior +4. **Quick actions** for immediate improvement`; + + const result = await chatCompletionObject({ + userAi: { + aiProvider: env.DEFAULT_LLM_PROVIDER as any, + aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", + aiApiKey: env.GOOGLE_API_KEY || null, + }, + system, + prompt, + schema: ExecutiveSummarySchema, + userEmail: "sandbox@inboxzero.com", + usageLabel: "sandbox-executive-summary", + }); + + return result.object; +} + +/** + * Build Enhanced User Persona + */ +async function buildUserPersona( + emailSummaries: EmailSummary[], + sentEmailSummaries?: EmailSummary[], + gmailSignature?: string, + gmailTemplates?: string[], +): Promise> { + const system = `You are a highly skilled AI analyst tasked with generating a focused professional persona of a user based on their email activity. + +Analyze the email summaries, signatures, and templates to identify: +1. Professional identity with supporting evidence +2. Current professional priorities based on email content + +Focus on understanding the user's role and what they're currently focused on professionally.`; + + const prompt = `### Input Data + +**Received Email Summaries:** +${emailSummaries.map((summary, index) => `Email ${index + 1} Summary: ${summary.summary} (Category: ${summary.category})`).join("\n")} + +${ + sentEmailSummaries && sentEmailSummaries.length > 0 + ? ` +**Sent Email Summaries:** +${sentEmailSummaries.map((summary, index) => `Sent ${index + 1} Summary: ${summary.summary} (Category: ${summary.category})`).join("\n")} +` + : "" +} + +**User's Signature:** +${gmailSignature || "[No signature data available – analyze based on email content only]"} + +${ + gmailTemplates && gmailTemplates.length > 0 + ? ` +**User's Gmail Templates:** +${gmailTemplates.map((template, index) => `Template ${index + 1}: ${template}`).join("\n")} +` + : "" +} + +--- + +Analyze the data and identify: +1. **Professional Identity**: What is their role and what evidence supports this? +2. **Current Priorities**: What are they focused on professionally based on email content?`; + + const result = await chatCompletionObject({ + userAi: { + aiProvider: env.DEFAULT_LLM_PROVIDER as any, + aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", + aiApiKey: env.GOOGLE_API_KEY || null, + }, + system, + prompt, + schema: UserPersonaSchema, + userEmail: "sandbox@inboxzero.com", + usageLabel: "sandbox-user-persona", + }); + + return result.object; +} + +/** + * Analyze Email Behavior + */ +async function analyzeEmailBehavior( + emailSummaries: EmailSummary[], + sentEmailSummaries?: EmailSummary[], +): Promise> { + const system = `You are an expert AI system that analyzes a user's email behavior to infer timing patterns, content preferences, and automation opportunities. + +Focus on identifying patterns that can be automated and providing specific, actionable automation rules that would save time and improve email management efficiency.`; + + const prompt = `### Email Analysis Data + +**Received Emails:** +${emailSummaries.map((email, i) => `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`).join("\n")} + +${ + sentEmailSummaries && sentEmailSummaries.length > 0 + ? ` +**Sent Emails:** +${sentEmailSummaries.map((email, i) => `${i + 1}. To: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`).join("\n")} +` + : "" +} + +--- + +Analyze the email patterns and identify: +1. Timing patterns (when emails are most active, response preferences) +2. Content preferences (what types of emails they engage with vs avoid) +3. Engagement triggers (what prompts them to take action) +4. Specific automation opportunities with estimated time savings`; + + const result = await chatCompletionObject({ + userAi: { + aiProvider: env.DEFAULT_LLM_PROVIDER as any, + aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", + aiApiKey: env.GOOGLE_API_KEY || null, + }, + system, + prompt, + schema: EmailBehaviorSchema, + userEmail: "sandbox@inboxzero.com", + usageLabel: "sandbox-email-behavior", + }); + + return result.object; +} + +/** + * Analyze Response Patterns + */ +async function analyzeResponsePatterns( + emailSummaries: EmailSummary[], + sentEmailSummaries?: EmailSummary[], +): Promise> { + const system = `You are an expert email behavior analyst. Your task is to identify common response patterns and suggest email categorization and templates based on the user's email activity. + +Focus on practical, actionable insights for email management including reusable templates and smart categorization. + +IMPORTANT: When creating email categories, avoid meaningless or generic categories such as: +- "Other", "Unknown", "Unclear", "Miscellaneous" +- "Personal" (too generic and meaningless) +- "Unclear Content/HTML Code", "HTML Content", "Raw Content" +- "General", "Random", "Various" + +Only suggest categories that are meaningful and provide clear organizational value. If an email doesn't fit into a meaningful category, don't create a category for it.`; + + const prompt = `### Input Data + +**Received Email Summaries:** +${emailSummaries.map((summary, index) => `Email ${index + 1}: ${summary.summary} (Category: ${summary.category})`).join("\n")} + +${ + sentEmailSummaries && sentEmailSummaries.length > 0 + ? ` +**Sent Email Summaries (User's Response Patterns):** +${sentEmailSummaries.map((summary, index) => `Sent ${index + 1}: ${summary.summary} (Category: ${summary.category})`).join("\n")} +` + : "" +} + +--- + +Analyze the data and identify: +1. Common response patterns the user uses with examples and frequency +2. Suggested email templates that would save time +3. Email categorization strategy with volume estimates and priorities + +For email categorization, create simple, practical categories based on actual email content. Examples of good categories: +- "Work", "Finance", "Meetings", "Marketing", "Support", "Sales" +- "Projects", "Billing", "Team", "Clients", "Products", "Services" +- "Administrative", "Technical", "Legal", "HR", "Operations" + +Only suggest categories that are meaningful and provide clear organizational value. If emails don't fit into meaningful categories, don't create categories for them.`; + + const result = await chatCompletionObject({ + userAi: { + aiProvider: env.DEFAULT_LLM_PROVIDER as any, + aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", + aiApiKey: env.GOOGLE_API_KEY || null, + }, + system, + prompt, + schema: ResponsePatternsSchema, + userEmail: "sandbox@inboxzero.com", + usageLabel: "sandbox-response-patterns", + }); + + return result.object; +} + +/** + * Analyze Label Optimization + */ +async function analyzeLabelOptimization( + emailSummaries: EmailSummary[], + gmailLabels: any[], +): Promise> { + const system = `You are a Gmail organization expert. Analyze the user's current labels and email patterns to suggest specific optimizations that will improve their email organization and workflow efficiency. + +Focus on practical suggestions that will reduce email management time and improve organization.`; + + const prompt = `### Current Gmail Labels +${gmailLabels.map((label) => `- ${label.name}: ${label.messagesTotal || 0} emails, ${label.messagesUnread || 0} unread`).join("\n")} + +### Email Content Analysis +${emailSummaries + .slice(0, 30) + .map( + (email, i) => + `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, + ) + .join("\n")} + +--- + +Based on the current labels and email content, suggest specific optimizations: +1. Labels to create based on email patterns +2. Labels to consolidate that have overlapping purposes +3. Labels to rename for better clarity +4. Labels to delete that are unused or redundant + +Each suggestion should include the reason and expected impact.`; + + const result = await chatCompletionObject({ + userAi: { + aiProvider: env.DEFAULT_LLM_PROVIDER as any, + aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", + aiApiKey: env.GOOGLE_API_KEY || null, + }, + system, + prompt, + schema: LabelAnalysisSchema, + userEmail: "sandbox@inboxzero.com", + usageLabel: "sandbox-label-analysis", + }); + + return result.object; +} + +/** + * Generate Actionable Recommendations + */ +async function generateActionableRecommendations( + emailSummaries: EmailSummary[], + userPersona: z.infer, + emailBehavior: z.infer, +): Promise> { + const system = `You are an email productivity consultant. Based on the comprehensive email analysis, create specific, actionable recommendations that the user can implement to improve their email workflow. + +Organize recommendations by timeline (immediate, short-term, long-term) and include specific implementation details and expected benefits.`; + + const prompt = `### Analysis Summary + +**User Persona:** ${userPersona.professionalIdentity.persona} +**Current Priorities:** ${userPersona.currentPriorities.join(", ")} +**Email Volume:** ${emailSummaries.length} emails analyzed + + + + + +--- + +Create actionable recommendations in three categories: +1. **Immediate Actions** (can be done today): 4-6 specific actions with time requirements +2. **Short-term Improvements** (this week): 3-4 improvements with timelines and benefits +3. **Long-term Strategy** (ongoing): 2-3 strategic initiatives with success metrics + +Focus on practical, implementable solutions that improve email organization and workflow efficiency.`; + + const result = await chatCompletionObject({ + userAi: { + aiProvider: env.DEFAULT_LLM_PROVIDER as any, + aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", + aiApiKey: env.GOOGLE_API_KEY || null, + }, + system, + prompt, + schema: ActionableRecommendationsSchema, + userEmail: "sandbox@inboxzero.com", + usageLabel: "sandbox-actionable-recommendations", + }); + + return result.object; +} + +/** + * Fetch emails from Gmail based on query + */ +async function fetchEmailsByQuery( + gmail: any, + query: string, + count: number, +): Promise { + const emails: any[] = []; + let nextPageToken: string | undefined; + let retryCount = 0; + const maxRetries = 3; + + while (emails.length < count && retryCount < maxRetries) { + try { + const response = await getMessages(gmail, { + query: query || undefined, + maxResults: Math.min(100, count - emails.length), + pageToken: nextPageToken, + }); + + if (!response.messages || response.messages.length === 0) { + break; + } + + // Get full message details for each email with retry logic + const messagePromises = response.messages.map(async (message: any) => { + if (!message.id) return null; + + for (let i = 0; i < 3; i++) { + try { + const messageWithPayload = await getMessage( + message.id, + gmail, + "full", + ); + return parseMessage(messageWithPayload); + } catch (error) { + if (i === 2) { + console.warn( + `Failed to fetch message ${message.id} after 3 attempts:`, + error, + ); + return null; + } + // Wait before retry + await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1))); + } + } + return null; + }); + + const messages = await Promise.all(messagePromises); + const validMessages = messages.filter((msg) => msg !== null); + + emails.push(...validMessages); + + nextPageToken = response.nextPageToken || undefined; + if (!nextPageToken) { + break; + } + + retryCount = 0; // Reset retry count on successful request + } catch (error) { + retryCount++; + console.warn( + `Gmail API error (attempt ${retryCount}/${maxRetries}):`, + error, + ); + + if (retryCount >= maxRetries) { + console.error( + `Failed to fetch emails after ${maxRetries} attempts:`, + error, + ); + break; + } + + // Wait before retry with exponential backoff + await new Promise((resolve) => setTimeout(resolve, 2000 * retryCount)); + } + } + + return emails; +} + +/** + * Fetch Gmail labels with message counts + */ +async function fetchGmailLabels(gmail: any): Promise { + try { + const response = await gmail.users.labels.list({ userId: "me" }); + + // Filter out system labels, keep only user-created labels + const userLabels = + response.data.labels?.filter( + (label: any) => + label.type === "user" && + !label.name.startsWith("CATEGORY_") && + !label.name.startsWith("CHAT"), + ) || []; + + // Get detailed info for each label to get message counts + const labelsWithCounts = await Promise.all( + userLabels.map(async (label: any) => { + try { + const labelDetail = await gmail.users.labels.get({ + userId: "me", + id: label.id, + }); + return { + ...label, + messagesTotal: labelDetail.data.messagesTotal || 0, + messagesUnread: labelDetail.data.messagesUnread || 0, + threadsTotal: labelDetail.data.threadsTotal || 0, + threadsUnread: labelDetail.data.threadsUnread || 0, + }; + } catch (error) { + console.warn(`Failed to get details for label ${label.name}:`, error); + return { + ...label, + messagesTotal: 0, + messagesUnread: 0, + threadsTotal: 0, + threadsUnread: 0, + }; + } + }), + ); + + return labelsWithCounts; + } catch (error) { + console.warn("Failed to fetch Gmail labels:", error); + return []; + } +} + +/** + * Fetch Gmail signature + */ +async function fetchGmailSignature(gmail: any): Promise { + try { + const messages = await getMessages(gmail, { + query: "from:me", + maxResults: 10, + }); + + for (const message of messages.messages || []) { + if (!message.id) continue; + const messageWithPayload = await getMessage(message.id, gmail); + const parsedEmail = parseMessage(messageWithPayload); + if (!parsedEmail.labelIds?.includes(GmailLabel.SENT)) continue; + if (!parsedEmail.textHtml) continue; + + const signature = extractGmailSignature(parsedEmail.textHtml); + if (signature) { + return signature; + } + } + + return ""; + } catch (error) { + console.warn("Failed to fetch Gmail signature:", error); + return ""; + } +} + +/** + * Fetch Gmail templates + */ +async function fetchGmailTemplates(gmail: any): Promise { + try { + const drafts = await gmail.users.drafts.list({ + userId: "me", + maxResults: 50, + }); + + if (!drafts.data.drafts || drafts.data.drafts.length === 0) { + return []; + } + + const templates: string[] = []; + + for (const draft of drafts.data.drafts) { + try { + if (!draft.message) continue; + + const draftDetail = await gmail.users.drafts.get({ + userId: "me", + id: draft.id!, + }); + + const message = draftDetail.data.message; + if (!message) continue; + + const parsedEmail = parseMessage(message); + if (parsedEmail.textPlain?.trim()) { + templates.push(parsedEmail.textPlain.trim()); + } + + if (templates.length >= 10) break; // Limit to 10 templates + } catch (error) { + console.warn(`Failed to fetch draft ${draft.id}:`, error); + } + } + + return templates; + } catch (error) { + console.warn("Failed to fetch Gmail templates:", error); + return []; + } +} + +export async function POST(request: NextRequest) { + const startTime = Date.now(); + const requestId = Math.random().toString(36).substring(7); + + console.log( + `[${requestId}] Starting comprehensive analysis for user: ${request.url}`, + ); + + try { + const body = await request.json(); + const { userEmail } = body; + + if (!userEmail) { + console.log(`[${requestId}] Error: No userEmail provided`); + return NextResponse.json( + { error: "Email address is required" }, + { status: 400 }, + ); + } + + console.log(`[${requestId}] Processing analysis for: ${userEmail}`); + + // Fetch user's email account + const emailAccount = await prisma.emailAccount.findFirst({ + where: { user: { email: userEmail } }, + include: { account: true }, + }); + + if (!emailAccount) { + console.log( + `[${requestId}] Error: Email account not found for ${userEmail}`, + ); + return NextResponse.json( + { error: "Email account not found" }, + { status: 404 }, + ); + } + + console.log(`[${requestId}] Found email account: ${emailAccount.email}`); + + // Get Gmail client + console.log(`[${requestId}] Initializing Gmail client...`); + const gmail = await getGmailClientWithRefresh({ + accessToken: emailAccount.account?.access_token ?? "", + refreshToken: emailAccount.account?.refresh_token ?? "", + expiresAt: emailAccount.account?.expires_at, + emailAccountId: emailAccount.id, + }); + console.log(`[${requestId}] Gmail client initialized successfully`); + + // Fetch raw emails to get date information + console.log(`[${requestId}] Fetching raw emails for date analysis...`); + const [receivedEmails, sentEmails] = await Promise.all([ + fetchEmailsByQuery(gmail, "", 200), + fetchEmailsByQuery(gmail, "from:me", 50), + ]); + console.log( + `[${requestId}] Fetched ${receivedEmails.length} received emails, ${sentEmails.length} sent emails`, + ); + + // Get date range from actual emails + const allEmails = [...receivedEmails, ...sentEmails]; + const emailDates = allEmails + .map((email) => + email.headers?.date ? new Date(email.headers.date) : null, + ) + .filter((date) => date !== null) + .sort((a, b) => a!.getTime() - b!.getTime()); + + const oldestDate = + emailDates.length > 0 + ? emailDates[0] + : new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); + const newestDate = + emailDates.length > 0 ? emailDates[emailDates.length - 1] : new Date(); + const totalDays = Math.ceil( + (newestDate.getTime() - oldestDate.getTime()) / (24 * 60 * 60 * 1000), + ); + + // Fetch email summaries from the email-summaries endpoint + console.log(`[${requestId}] Fetching email summaries...`); + const emailSummariesUrl = `${request.url.replace("/comprehensive-analysis", "/email-summaries")}`; + + // Create AbortController for timeout handling + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 300000); // 5 minute timeout + + let receivedResponse: Response, sentResponse: Response; + try { + [receivedResponse, sentResponse] = await Promise.all([ + fetch(emailSummariesUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userEmail, count: 200 }), + signal: controller.signal, + }), + fetch(emailSummariesUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userEmail, query: "from:me", count: 50 }), + signal: controller.signal, + }), + ]); + } finally { + clearTimeout(timeoutId); + } + + // Check for HTTP errors + if (!receivedResponse.ok) { + const errorText = await receivedResponse.text(); + console.error(`[${requestId}] Received emails API error:`, { + status: receivedResponse.status, + statusText: receivedResponse.statusText, + body: errorText.substring(0, 500), + }); + throw new Error( + `Failed to fetch received emails: ${receivedResponse.status} ${receivedResponse.statusText}`, + ); + } + + if (!sentResponse.ok) { + const errorText = await sentResponse.text(); + console.error(`[${requestId}] Sent emails API error:`, { + status: sentResponse.status, + statusText: sentResponse.statusText, + body: errorText.substring(0, 500), + }); + throw new Error( + `Failed to fetch sent emails: ${sentResponse.status} ${sentResponse.statusText}`, + ); + } + + const receivedData = await receivedResponse.json(); + const sentData = await sentResponse.json(); + + console.log(`[${requestId}] Email summaries fetched successfully:`, { + receivedCount: receivedData.summaries?.length || 0, + sentCount: sentData.summaries?.length || 0, + }); + + // Fetch additional Gmail data + console.log(`[${requestId}] Fetching additional Gmail data...`); + const [gmailLabels, gmailSignature, gmailTemplates] = await Promise.all([ + fetchGmailLabels(gmail), + fetchGmailSignature(gmail), + fetchGmailTemplates(gmail), + ]); + console.log(`[${requestId}] Gmail data fetched:`, { + labelsCount: gmailLabels.length, + hasSignature: !!gmailSignature, + templatesCount: gmailTemplates.length, + }); + + // Run all analysis functions + console.log(`[${requestId}] Starting AI analysis functions...`); + const [ + executiveSummary, + userPersona, + emailBehavior, + responsePatterns, + labelAnalysis, + ] = await Promise.all([ + generateExecutiveSummary( + receivedData.summaries, + sentData.summaries, + gmailLabels, + ), + buildUserPersona( + receivedData.summaries, + sentData.summaries, + gmailSignature, + gmailTemplates, + ), + analyzeEmailBehavior(receivedData.summaries, sentData.summaries), + analyzeResponsePatterns(receivedData.summaries, sentData.summaries), + analyzeLabelOptimization(receivedData.summaries, gmailLabels), + ]); + console.log(`[${requestId}] AI analysis functions completed successfully`); + + // Generate actionable recommendations based on all analysis + console.log(`[${requestId}] Generating actionable recommendations...`); + const actionableRecommendations = await generateActionableRecommendations( + receivedData.summaries, + userPersona, + emailBehavior, + ); + console.log(`[${requestId}] Actionable recommendations generated`); + + // Compile comprehensive report + console.log(`[${requestId}] Compiling final report...`); + const comprehensiveReport = { + executiveSummary: { + ...executiveSummary, + keyMetrics: { + totalEmails: receivedData.totalEmails + sentData.totalEmails, + dateRange: `${totalDays} days (${oldestDate.toLocaleDateString()} - ${newestDate.toLocaleDateString()})`, + analysisFreshness: "Just now", + }, + }, + emailActivityOverview: { + dataSources: { + inbox: Math.floor(receivedData.totalEmails * 0.6), + archived: Math.floor(receivedData.totalEmails * 0.3), + trash: Math.floor(receivedData.totalEmails * 0.1), + sent: sentData.totalEmails, + }, + }, + userPersona, + emailBehavior, + responsePatterns, + labelAnalysis: { + currentLabels: gmailLabels.map((label) => ({ + name: label.name, + emailCount: label.messagesTotal || 0, + unreadCount: label.messagesUnread || 0, + })), + optimizationSuggestions: labelAnalysis.optimizationSuggestions, + }, + actionableRecommendations, + processingTime: Date.now() - startTime, + }; + + console.log( + `[${requestId}] Analysis completed successfully in ${Date.now() - startTime}ms`, + ); + return NextResponse.json(comprehensiveReport); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + const errorStack = error instanceof Error ? error.stack : undefined; + + console.error(`[${requestId}] Comprehensive analysis error:`, { + error: errorMessage, + stack: errorStack, + processingTime: Date.now() - startTime, + url: request.url, + }); + + return NextResponse.json( + { + error: "Analysis failed", + details: errorMessage, + requestId, + processingTime: Date.now() - startTime, + }, + { status: 500 }, + ); + } +} diff --git a/apps/web/app/(reports)/api/email-summaries/route.ts b/apps/web/app/(reports)/api/email-summaries/route.ts new file mode 100644 index 0000000000..001259902a --- /dev/null +++ b/apps/web/app/(reports)/api/email-summaries/route.ts @@ -0,0 +1,351 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; +import { getGmailClientWithRefresh } from "@/utils/gmail/client"; +import { getMessage, getMessages } from "@/utils/gmail/message"; +import { parseMessage } from "@/utils/mail"; +import { chatCompletionObject } from "@/utils/llms"; +import { redis } from "@/utils/redis"; +import { env } from "@/env"; +import prisma from "@/utils/prisma"; + +const EmailSummarySchema = z.object({ + summary: z.string().describe("Brief summary of the email content"), + sender: z.string().describe("Email sender"), + subject: z.string().describe("Email subject"), + category: z + .string() + .describe("Category of the email (work, personal, marketing, etc.)"), +}); + +type EmailSummary = z.infer; + +const SummarizeRequestSchema = z.object({ + userEmail: z.string().email(), + query: z.string().optional().default(""), // Gmail query (e.g., "from:me", "label:work") + count: z.number().min(1).max(200).optional().default(50), + forceRefresh: z.boolean().optional().default(false), + batchSize: z.number().min(10).max(50).optional().default(20), +}); + +const SummarizeResponseSchema = z.object({ + summaries: z.array(EmailSummarySchema), + totalEmails: z.number(), + cached: z.boolean(), + query: z.string(), + processingTime: z.number(), +}); + +const BATCH_SUMMARY_EXPIRATION = 60 * 60 * 24; + +/** + * Generate cache key for email batch + */ +function getBatchSummaryKey(query: string, count: number): string { + return `sandbox:email-summary:${query}:${count}`; +} + +/** + * Get cached batch summary from Redis + */ +async function getBatchSummary( + query: string, + count: number, +): Promise { + const key = getBatchSummaryKey(query, count); + return redis.get(key); +} + +/** + * Save batch summary to Redis cache + */ +async function saveBatchSummary( + summaries: EmailSummary[], + query: string, + count: number, +): Promise { + const key = getBatchSummaryKey(query, count); + await redis.set(key, summaries, { ex: BATCH_SUMMARY_EXPIRATION }); +} + +/** + * Fetch emails from Gmail based on query + */ +async function fetchEmailsByQuery( + gmail: any, + query: string, + count: number, +): Promise { + const emails: any[] = []; + let nextPageToken: string | undefined; + + while (emails.length < count) { + const response = await getMessages(gmail, { + query: query || undefined, + maxResults: Math.min(100, count - emails.length), + pageToken: nextPageToken, + }); + + if (!response.messages || response.messages.length === 0) { + break; + } + + // Get full message details for each email + const messagePromises = response.messages.map(async (message: any) => { + if (!message.id) return null; + try { + const messageWithPayload = await getMessage(message.id, gmail, "full"); + return parseMessage(messageWithPayload); + } catch (error) { + console.warn(`Failed to fetch message ${message.id}:`, error); + return null; + } + }); + + const messages = await Promise.all(messagePromises); + const validMessages = messages.filter((msg) => msg !== null); + + emails.push(...validMessages); + + nextPageToken = response.nextPageToken || undefined; + if (!nextPageToken) { + break; + } + } + + return emails; +} + +/** + * Summarize a batch of emails using AI + */ +async function summarizeEmailBatch( + emails: any[], + query: string, + forceRefresh = false, +): Promise { + // Check cache first (unless force refresh is requested) + if (!forceRefresh) { + const cachedSummaries = await getBatchSummary(query, emails.length); + if (cachedSummaries) { + return cachedSummaries; + } + } + + const emailTexts = emails.map((email) => { + const sender = email.headers?.from || "Unknown"; + const subject = email.headers?.subject || "No subject"; + const content = email.textPlain || email.textHtml || ""; + + return `From: ${sender}\nSubject: ${subject}\nContent: ${content.substring(0, 1000)}`; + }); + + const system = `You are an assistant that processes user emails to extract their core meaning for later analysis. + +For each email, write a **factual summary of 3–5 sentences** that clearly describes: +- The main topic or purpose of the email +- What the sender wants, requests, or informs +- Any relevant secondary detail (e.g., urgency, timing, sender role, or context) +- Optional: mention tools, platforms, or projects if they help clarify the email's purpose + +**Important Rules:** +- Be objective. Do **not** speculate, interpret intent, or invent details. +- Summarize only what is in the actual content of the email. +- Use professional and concise language. +- **Include** marketing/newsletter emails **only if** they reflect the user's professional interests (e.g., product updates, industry news, job boards). +- **Skip** irrelevant promotions, spam, or generic sales offers (e.g., holiday deals, coupon codes). + +--- +`; + + const prompt = ` +**Input Emails:** + +${emailTexts.join("\n\n---\n\n")} + +Return the analysis as a JSON array with objects containing: summary, sender, subject, category.`; + + const result = await chatCompletionObject({ + userAi: { + aiProvider: env.DEFAULT_LLM_PROVIDER as any, + aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", + aiApiKey: env.GOOGLE_API_KEY || null, + }, + system, + prompt, + schema: z.array(EmailSummarySchema), + userEmail: "sandbox@inboxzero.com", + usageLabel: "sandbox-email-summary-generation", + }); + + const summaries = result.object; + + // Cache the results + await saveBatchSummary(summaries, query, emails.length); + + return summaries; +} + +/** + * Process emails in batches and summarize + */ +async function processEmailBatches( + emails: any[], + query: string, + batchSize: number, + forceRefresh: boolean, +): Promise { + const allSummaries: EmailSummary[] = []; + + for (let i = 0; i < emails.length; i += batchSize) { + const batch = emails.slice(i, i + batchSize); + const batchSummaries = await summarizeEmailBatch( + batch, + query, + forceRefresh, + ); + allSummaries.push(...batchSummaries); + } + + return allSummaries; +} + +export async function POST(request: NextRequest) { + const startTime = Date.now(); + const requestId = Math.random().toString(36).substring(7); + + console.log(`[${requestId}] Starting email summarization: ${request.url}`); + + try { + // Parse and validate request + const body = await request.json(); + const { userEmail, query, count, forceRefresh, batchSize } = + SummarizeRequestSchema.parse(body); + + console.log(`[${requestId}] Processing request:`, { + userEmail, + query, + count, + forceRefresh, + batchSize, + }); + + // Fetch user's email account + const emailAccount = await prisma.emailAccount.findFirst({ + where: { user: { email: userEmail } }, + include: { account: true }, + }); + + if (!emailAccount) { + console.log( + `[${requestId}] Error: Email account not found for ${userEmail}`, + ); + return NextResponse.json( + { error: "Email account not found" }, + { status: 404 }, + ); + } + + console.log(`[${requestId}] Found email account: ${emailAccount.email}`); + + // Get Gmail client + console.log(`[${requestId}] Initializing Gmail client...`); + const gmail = await getGmailClientWithRefresh({ + accessToken: emailAccount.account?.access_token!, + refreshToken: emailAccount.account?.refresh_token!, + expiresAt: emailAccount.account?.expires_at, + emailAccountId: emailAccount.id, + }); + console.log(`[${requestId}] Gmail client initialized successfully`); + + // Fetch emails based on query + console.log( + `[${requestId}] Fetching emails with query: "${query}", count: ${count}`, + ); + const emails = await fetchEmailsByQuery(gmail, query, count); + console.log(`[${requestId}] Fetched ${emails.length} emails`); + + if (emails.length === 0) { + console.log(`[${requestId}] No emails found for query: "${query}"`); + return NextResponse.json({ + summaries: [], + totalEmails: 0, + cached: false, + query, + processingTime: Date.now() - startTime, + }); + } + + // Check if we can use cached results + let cached = false; + if (!forceRefresh) { + console.log(`[${requestId}] Checking cache for query: "${query}"`); + const cachedSummaries = await getBatchSummary(query, emails.length); + if (cachedSummaries) { + cached = true; + console.log(`[${requestId}] Using cached results`); + } else { + console.log(`[${requestId}] No cache found, processing emails`); + } + } else { + console.log(`[${requestId}] Force refresh requested, ignoring cache`); + } + + // Process emails in batches + console.log( + `[${requestId}] Processing ${emails.length} emails in batches of ${batchSize}`, + ); + const summaries = await processEmailBatches( + emails, + query, + batchSize, + forceRefresh, + ); + console.log(`[${requestId}] Generated ${summaries.length} summaries`); + + const processingTime = Date.now() - startTime; + console.log( + `[${requestId}] Email summarization completed in ${processingTime}ms`, + ); + + return NextResponse.json({ + summaries, + totalEmails: emails.length, + cached, + query, + processingTime, + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + const errorStack = error instanceof Error ? error.stack : undefined; + + console.error(`[${requestId}] Sandbox email summarization error:`, { + error: errorMessage, + stack: errorStack, + processingTime: Date.now() - startTime, + url: request.url, + }); + + if (error instanceof z.ZodError) { + return NextResponse.json( + { + error: "Invalid request data", + details: error.errors, + requestId, + processingTime: Date.now() - startTime, + }, + { status: 400 }, + ); + } + + return NextResponse.json( + { + error: "Failed to summarize emails", + details: errorMessage, + requestId, + processingTime: Date.now() - startTime, + }, + { status: 500 }, + ); + } +} diff --git a/apps/web/app/(reports)/api/gmail-labels/route.ts b/apps/web/app/(reports)/api/gmail-labels/route.ts new file mode 100644 index 0000000000..3b722acac7 --- /dev/null +++ b/apps/web/app/(reports)/api/gmail-labels/route.ts @@ -0,0 +1,94 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { getGmailClientWithRefresh } from "@/utils/gmail/client"; +import prisma from "@/utils/prisma"; + +export async function POST(request: NextRequest) { + try { + const { userEmail } = await request.json(); + + if (!userEmail) { + return NextResponse.json( + { error: "Email address is required" }, + { status: 400 }, + ); + } + + const emailAccount = await prisma.emailAccount.findFirst({ + where: { user: { email: userEmail } }, + include: { account: true }, + }); + + if (!emailAccount) { + return NextResponse.json( + { error: "Email account not found" }, + { status: 404 }, + ); + } + + const gmail = await getGmailClientWithRefresh({ + accessToken: emailAccount.account.access_token!, + refreshToken: emailAccount.account.refresh_token!, + expiresAt: emailAccount.account.expires_at, + emailAccountId: emailAccount.id, + }); + + const response = await gmail.users.labels.list({ userId: "me" }); + + const userLabels = + response.data.labels?.filter( + (label: any) => + label.type === "user" && + !label.name.startsWith("CATEGORY_") && + !label.name.startsWith("CHAT"), + ) || []; + + const labelsWithCounts = await Promise.all( + userLabels.map(async (label: any) => { + try { + const labelDetail = await gmail.users.labels.get({ + userId: "me", + id: label.id, + }); + return { + ...label, + messagesTotal: labelDetail.data.messagesTotal || 0, + messagesUnread: labelDetail.data.messagesUnread || 0, + threadsTotal: labelDetail.data.threadsTotal || 0, + threadsUnread: labelDetail.data.threadsUnread || 0, + }; + } catch (error) { + console.warn(`Failed to get details for label ${label.name}:`, error); + return { + ...label, + messagesTotal: 0, + messagesUnread: 0, + threadsTotal: 0, + threadsUnread: 0, + }; + } + }), + ); + + const sortedLabels = labelsWithCounts.sort( + (a: any, b: any) => (b.messagesTotal || 0) - (a.messagesTotal || 0), + ); + + return NextResponse.json({ + labels: sortedLabels.map((label: any) => ({ + id: label.id, + name: label.name, + messagesTotal: label.messagesTotal || 0, + messagesUnread: label.messagesUnread || 0, + color: label.color || null, + type: label.type, + })), + totalLabels: sortedLabels.length, + }); + } catch (error) { + console.error("Gmail labels fetch error:", error); + return NextResponse.json( + { error: "Failed to fetch Gmail labels" }, + { status: 500 }, + ); + } +} diff --git a/apps/web/app/(reports)/api/sandbox-health/route.ts b/apps/web/app/(reports)/api/sandbox-health/route.ts new file mode 100644 index 0000000000..2333246b91 --- /dev/null +++ b/apps/web/app/(reports)/api/sandbox-health/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + return NextResponse.json({ + status: "ok", + message: "Sandbox API is working", + timestamp: new Date().toISOString(), + }); +} + +export async function POST() { + return NextResponse.json({ + status: "ok", + message: "Sandbox API POST is working", + timestamp: new Date().toISOString(), + }); +} diff --git a/apps/web/app/(reports)/api/test-error-handling/route.ts b/apps/web/app/(reports)/api/test-error-handling/route.ts new file mode 100644 index 0000000000..f77ac7ed33 --- /dev/null +++ b/apps/web/app/(reports)/api/test-error-handling/route.ts @@ -0,0 +1,42 @@ +import { type NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + const requestId = Math.random().toString(36).substring(7); + const startTime = Date.now(); + + console.log(`[${requestId}] Test error handling endpoint called`); + + try { + // Simulate some processing + await new Promise((resolve) => setTimeout(resolve, 1000)); + + console.log( + `[${requestId}] Test completed successfully in ${Date.now() - startTime}ms`, + ); + + return NextResponse.json({ + success: true, + message: "Error handling test completed", + requestId, + processingTime: Date.now() - startTime, + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + + console.error(`[${requestId}] Test error:`, { + error: errorMessage, + processingTime: Date.now() - startTime, + }); + + return NextResponse.json( + { + error: "Test failed", + details: errorMessage, + requestId, + processingTime: Date.now() - startTime, + }, + { status: 500 }, + ); + } +} diff --git a/apps/web/app/(reports)/email-analysis/page.tsx b/apps/web/app/(reports)/email-analysis/page.tsx new file mode 100644 index 0000000000..d5d0a85869 --- /dev/null +++ b/apps/web/app/(reports)/email-analysis/page.tsx @@ -0,0 +1,627 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/Input"; +import { Badge } from "@/components/ui/badge"; +import { + Loader2, + Clock, + Mail, + TrendingUp, + Target, + Zap, + CheckCircle, +} from "lucide-react"; + +// Mock data types based on email-analysis-report.mdc +interface ExecutiveSummary { + userProfile: { + persona: string; + confidence: number; + }; + keyMetrics: { + totalEmails: number; + dateRange: string; + analysisFreshness: string; + }; + topInsights: Array<{ + insight: string; + priority: "high" | "medium" | "low"; + icon: string; + }>; + quickActions: Array<{ + action: string; + difficulty: "easy" | "medium" | "hard"; + impact: "high" | "medium" | "low"; + }>; +} + +interface EmailActivityOverview { + dataSources: { + inbox: number; + archived: number; + trash: number; + sent: number; + }; +} + +interface UserPersona { + professionalIdentity: { + persona: string; + supportingEvidence: string[]; + }; + currentPriorities: string[]; +} + +interface EmailBehavior { + timingPatterns: { + peakHours: string[]; + responsePreference: string; + frequency: string; + }; + contentPreferences: { + preferred: string[]; + avoided: string[]; + }; + engagementTriggers: string[]; +} + +interface ResponsePatterns { + commonResponses: Array<{ + pattern: string; + example: string; + frequency: number; + triggers: string[]; + }>; + suggestedTemplates: Array<{ + templateName: string; + template: string; + useCase: string; + }>; + categoryOrganization: Array<{ + category: string; + description: string; + emailCount: number; + priority: "high" | "medium" | "low"; + }>; +} + +interface LabelAnalysis { + currentLabels: Array<{ + name: string; + emailCount: number; + unreadCount: number; + }>; + optimizationSuggestions: Array<{ + type: "consolidate" | "rename" | "create" | "delete"; + suggestion: string; + reason: string; + impact: "high" | "medium" | "low"; + }>; +} + +interface ActionableRecommendations { + immediateActions: Array<{ + action: string; + difficulty: "easy" | "medium" | "hard"; + impact: "high" | "medium" | "low"; + timeRequired: string; + }>; + shortTermImprovements: Array<{ + improvement: string; + timeline: string; + expectedBenefit: string; + }>; + longTermStrategy: Array<{ + strategy: string; + description: string; + successMetrics: string[]; + }>; +} + +interface ComprehensiveAnalysisReport { + executiveSummary: ExecutiveSummary; + emailActivityOverview: EmailActivityOverview; + userPersona: UserPersona; + emailBehavior: EmailBehavior; + responsePatterns: ResponsePatterns; + labelAnalysis: LabelAnalysis; + actionableRecommendations: ActionableRecommendations; +} + +export default function EmailAnalysisPage() { + const [email, setEmail] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [report, setReport] = useState( + null, + ); + + const handleAnalyze = async () => { + if (!email.trim()) return; + + setIsLoading(true); + try { + const response = await fetch("/api/email-analysis", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ userEmail: email.trim() }), + }); + + if (!response.ok) { + throw new Error("Failed to generate report"); + } + + const data = await response.json(); + setReport(data); + } catch (error) { + console.error("Error generating report:", error); + // You might want to show a toast notification here + } finally { + setIsLoading(false); + } + }; + + const getPriorityColor = (priority: "high" | "medium" | "low") => { + switch (priority) { + case "high": + return "bg-red-100 text-red-800"; + case "medium": + return "bg-yellow-100 text-yellow-800"; + case "low": + return "bg-green-100 text-green-800"; + } + }; + + const getDifficultyColor = (difficulty: "easy" | "medium" | "hard") => { + switch (difficulty) { + case "easy": + return "bg-green-100 text-green-800"; + case "medium": + return "bg-yellow-100 text-yellow-800"; + case "hard": + return "bg-red-100 text-red-800"; + } + }; + + return ( +
+ {/* Input Section */} + + + + + Generate Email Analysis Report + + + +
+
+ ) => + setEmail(e.target.value), + }} + className="w-full" + /> +
+ +
+
+
+ + {/* Report Display */} + {report && ( +
+ {/* Executive Summary */} + + + + + Executive Summary + + + +
+
+

+ Professional Persona +

+

+ {report.executiveSummary.userProfile.persona} +

+

+ Confidence: {report.executiveSummary.userProfile.confidence} + % +

+
+
+

+ Total Emails Analyzed +

+

+ {report.executiveSummary.keyMetrics.totalEmails.toLocaleString()} +

+

+ {report.executiveSummary.keyMetrics.dateRange} +

+
+
+

Email Sources

+
+
+ Inbox: + + {report.emailActivityOverview.dataSources.inbox} + +
+
+ Archived: + + {report.emailActivityOverview.dataSources.archived} + +
+
+ Sent: + + {report.emailActivityOverview.dataSources.sent} + +
+
+
+
+ +
+

+ Top Insights +

+
+ {report.executiveSummary.topInsights.map((insight, index) => ( +
+ + {insight.priority} + +

{insight.insight}

+
+ ))} +
+
+
+
+ + {/* User Persona & Communication Style */} + + + + + User Persona & Communication Style + + + +
+

+ Professional Identity +

+

+ {report.userPersona.professionalIdentity.persona} +

+
+ {report.userPersona.professionalIdentity.supportingEvidence.map( + (evidence, index) => ( +

+ + {evidence} +

+ ), + )} +
+
+ +
+

+ Current Priorities +

+
+ {report.userPersona.currentPriorities.map( + (priority, index) => ( + + {priority} + + ), + )} +
+
+ +
+

+ Email Behavior Patterns +

+
+
+
+ Timing Patterns +
+

+ Peak hours:{" "} + {report.emailBehavior.timingPatterns.peakHours.join(", ")} +

+

+ Response preference:{" "} + {report.emailBehavior.timingPatterns.responsePreference} +

+
+
+
+ Content Preferences +
+

+ Preferred:{" "} + {report.emailBehavior.contentPreferences.preferred.join( + ", ", + )} +

+
+
+
+ Engagement Triggers +
+
+ {report.emailBehavior.engagementTriggers.map( + (trigger, index) => ( +

+ • {trigger} +

+ ), + )} +
+
+
+
+
+
+ + {/* Response Patterns */} + + + + + Response Patterns & Categories + + + +
+

+ Common Response Patterns +

+
+ {report.responsePatterns.commonResponses.map( + (response, index) => ( +
+
+
+ {response.pattern} +
+ + Frequency: {response.frequency} + +
+

+ "{response.example}" +

+
+ {response.triggers.map((trigger, triggerIndex) => ( + + {trigger} + + ))} +
+
+ ), + )} +
+
+ +
+

+ Email Categories +

+
+ {report.responsePatterns.categoryOrganization.map( + (category, index) => ( +
+
+
+ {category.category} +
+ + {category.priority} + +
+

+ {category.description} +

+

+ {category.emailCount} emails +

+
+ ), + )} +
+
+
+
+ + {/* Label Analysis */} + + + + + Current Labels + + + +
+ {report.labelAnalysis.currentLabels.map((label, index) => ( +
+

+ {label.name} +

+

+ {label.emailCount} +

+

+ {label.unreadCount} unread +

+
+ ))} +
+
+
+ + {/* Suggestions */} + + + + + Suggestions + + + +
+

+ Immediate Actions +

+
+ {report.actionableRecommendations.immediateActions.map( + (action, index) => ( +
+
+

+ {action.action} +

+

+ Time required: {action.timeRequired} +

+
+
+ + {action.difficulty} + + + {action.impact} impact + +
+
+ ), + )} +
+
+ +
+

+ Short-term Improvements +

+
+ {report.actionableRecommendations.shortTermImprovements.map( + (improvement, index) => ( +
+
+ {improvement.improvement} +
+

+ Timeline: {improvement.timeline} +

+

+ {improvement.expectedBenefit} +

+
+ ), + )} +
+
+ +
+

+ Long-term Strategy +

+
+ {report.actionableRecommendations.longTermStrategy.map( + (strategy, index) => ( +
+
+ {strategy.strategy} +
+

+ {strategy.description} +

+
+ {strategy.successMetrics.map( + (metric, metricIndex) => ( + + {metric} + + ), + )} +
+
+ ), + )} +
+
+
+
+
+ )} +
+ ); +} diff --git a/apps/web/app/(reports)/layout.tsx b/apps/web/app/(reports)/layout.tsx new file mode 100644 index 0000000000..d17c44dc4b --- /dev/null +++ b/apps/web/app/(reports)/layout.tsx @@ -0,0 +1,131 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { auth } from "@/app/api/auth/[...nextauth]/auth"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { User, LogIn, UserPlus, Shield } from "lucide-react"; +import { isAdmin } from "@/utils/admin"; +import { ErrorPage } from "@/components/ErrorPage"; + +export const metadata: Metadata = { + title: "Sandbox - Email Analysis", + description: "Email analysis sandbox environment", +}; + +async function AuthStatus() { + const session = await auth(); + + if (session?.user) { + const userIsAdmin = isAdmin({ email: session.user.email }); + + return ( +
+
+ + {session.user.email} +
+
+ + Authenticated + + {userIsAdmin && ( + + + Admin + + )} +
+
+ ); + } + + return ( +
+ + Not Authenticated + +
+ + +
+
+ ); +} + +export default async function SandboxLayout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await auth(); + + // Require authentication + if (!session?.user.email) { + redirect("/login"); + } + + // Require admin access + if (!isAdmin({ email: session.user.email })) { + return ( +
+ + Return to Home + + } + /> +
+ ); + } + + return ( +
+
+
+
+
+ +
+ S +
+

+ Email Intelligence Sandbox +

+ + + Development Environment + +
+ +
+
+
+
+ {children} +
+
+ ); +} From fc2622f96649072175f297644ae518f85b4fc897 Mon Sep 17 00:00:00 2001 From: Eduardo Lelis Date: Thu, 24 Jul 2025 20:19:19 -0300 Subject: [PATCH 02/20] Remove unused endpoints --- .../app/(reports)/api/sandbox-health/route.ts | 17 -------- .../api/test-error-handling/route.ts | 42 ------------------- 2 files changed, 59 deletions(-) delete mode 100644 apps/web/app/(reports)/api/sandbox-health/route.ts delete mode 100644 apps/web/app/(reports)/api/test-error-handling/route.ts diff --git a/apps/web/app/(reports)/api/sandbox-health/route.ts b/apps/web/app/(reports)/api/sandbox-health/route.ts deleted file mode 100644 index 2333246b91..0000000000 --- a/apps/web/app/(reports)/api/sandbox-health/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextResponse } from "next/server"; - -export async function GET() { - return NextResponse.json({ - status: "ok", - message: "Sandbox API is working", - timestamp: new Date().toISOString(), - }); -} - -export async function POST() { - return NextResponse.json({ - status: "ok", - message: "Sandbox API POST is working", - timestamp: new Date().toISOString(), - }); -} diff --git a/apps/web/app/(reports)/api/test-error-handling/route.ts b/apps/web/app/(reports)/api/test-error-handling/route.ts deleted file mode 100644 index f77ac7ed33..0000000000 --- a/apps/web/app/(reports)/api/test-error-handling/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; - -export async function GET(request: NextRequest) { - const requestId = Math.random().toString(36).substring(7); - const startTime = Date.now(); - - console.log(`[${requestId}] Test error handling endpoint called`); - - try { - // Simulate some processing - await new Promise((resolve) => setTimeout(resolve, 1000)); - - console.log( - `[${requestId}] Test completed successfully in ${Date.now() - startTime}ms`, - ); - - return NextResponse.json({ - success: true, - message: "Error handling test completed", - requestId, - processingTime: Date.now() - startTime, - }); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - - console.error(`[${requestId}] Test error:`, { - error: errorMessage, - processingTime: Date.now() - startTime, - }); - - return NextResponse.json( - { - error: "Test failed", - details: errorMessage, - requestId, - processingTime: Date.now() - startTime, - }, - { status: 500 }, - ); - } -} From 6f720a606bb987c61348a39ce12e8740d033e05f Mon Sep 17 00:00:00 2001 From: Eduardo Lelis Date: Fri, 25 Jul 2025 11:47:39 -0300 Subject: [PATCH 03/20] Simplify to a single API route. Move page to debug --- .../app/(app)/[emailAccountId]/debug/page.tsx | 5 + .../[emailAccountId]/debug/report}/page.tsx | 384 ++++--- .../app/(reports)/api/email-analysis/route.ts | 1016 ----------------- .../app/(reports)/api/email-report/fetch.ts | 411 +++++++ .../app/(reports)/api/email-report/prompts.ts | 534 +++++++++ .../app/(reports)/api/email-report/route.ts | 436 +++++++ .../app/(reports)/api/email-report/schemas.ts | 160 +++ .../(reports)/api/email-summaries/route.ts | 351 ------ .../app/(reports)/api/gmail-labels/route.ts | 94 -- 9 files changed, 1774 insertions(+), 1617 deletions(-) rename apps/web/app/{(reports)/email-analysis => (app)/[emailAccountId]/debug/report}/page.tsx (64%) delete mode 100644 apps/web/app/(reports)/api/email-analysis/route.ts create mode 100644 apps/web/app/(reports)/api/email-report/fetch.ts create mode 100644 apps/web/app/(reports)/api/email-report/prompts.ts create mode 100644 apps/web/app/(reports)/api/email-report/route.ts create mode 100644 apps/web/app/(reports)/api/email-report/schemas.ts delete mode 100644 apps/web/app/(reports)/api/email-summaries/route.ts delete mode 100644 apps/web/app/(reports)/api/gmail-labels/route.ts diff --git a/apps/web/app/(app)/[emailAccountId]/debug/page.tsx b/apps/web/app/(app)/[emailAccountId]/debug/page.tsx index ca483878bc..23dc3194f8 100644 --- a/apps/web/app/(app)/[emailAccountId]/debug/page.tsx +++ b/apps/web/app/(app)/[emailAccountId]/debug/page.tsx @@ -26,6 +26,11 @@ export default async function DebugPage(props: { Rule History + ); diff --git a/apps/web/app/(reports)/email-analysis/page.tsx b/apps/web/app/(app)/[emailAccountId]/debug/report/page.tsx similarity index 64% rename from apps/web/app/(reports)/email-analysis/page.tsx rename to apps/web/app/(app)/[emailAccountId]/debug/report/page.tsx index d5d0a85869..8ab7538f11 100644 --- a/apps/web/app/(reports)/email-analysis/page.tsx +++ b/apps/web/app/(app)/[emailAccountId]/debug/report/page.tsx @@ -1,31 +1,27 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Input } from "@/components/Input"; import { Badge } from "@/components/ui/badge"; import { Loader2, - Clock, Mail, TrendingUp, Target, Zap, CheckCircle, + Clock, } from "lucide-react"; +import { useParams } from "next/navigation"; +import { fetchWithAccount } from "@/utils/fetch"; -// Mock data types based on email-analysis-report.mdc +// Types based on the email report API response interface ExecutiveSummary { userProfile: { persona: string; confidence: number; }; - keyMetrics: { - totalEmails: number; - dateRange: string; - analysisFreshness: string; - }; topInsights: Array<{ insight: string; priority: "high" | "medium" | "low"; @@ -93,9 +89,13 @@ interface LabelAnalysis { name: string; emailCount: number; unreadCount: number; + threadCount: number; + unreadThreads: number; + color: string | null; + type: string; }>; optimizationSuggestions: Array<{ - type: "consolidate" | "rename" | "create" | "delete"; + type: "create" | "consolidate" | "rename" | "delete"; suggestion: string; reason: string; impact: "high" | "medium" | "low"; @@ -121,7 +121,7 @@ interface ActionableRecommendations { }>; } -interface ComprehensiveAnalysisReport { +interface EmailReportData { executiveSummary: ExecutiveSummary; emailActivityOverview: EmailActivityOverview; userPersona: UserPersona; @@ -131,39 +131,48 @@ interface ComprehensiveAnalysisReport { actionableRecommendations: ActionableRecommendations; } -export default function EmailAnalysisPage() { - const [email, setEmail] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [report, setReport] = useState( - null, - ); +export default function EmailReportPage() { + const params = useParams(); + const emailAccountId = params.emailAccountId as string; + + const [isLoading, setIsLoading] = useState(true); + const [report, setReport] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + const generateReport = async () => { + if (!emailAccountId) return; + + try { + const response = await fetchWithAccount({ + url: "/api/email-report", + emailAccountId, + init: { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }, + }); - const handleAnalyze = async () => { - if (!email.trim()) return; - - setIsLoading(true); - try { - const response = await fetch("/api/email-analysis", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ userEmail: email.trim() }), - }); - - if (!response.ok) { - throw new Error("Failed to generate report"); + if (!response.ok) { + throw new Error(`Failed to generate report: ${response.statusText}`); + } + + const data = await response.json(); + setReport(data); + } catch (error) { + console.error("Error generating report:", error); + setError( + error instanceof Error ? error.message : "Failed to generate report", + ); + } finally { + setIsLoading(false); } + }; - const data = await response.json(); - setReport(data); - } catch (error) { - console.error("Error generating report:", error); - // You might want to show a toast notification here - } finally { - setIsLoading(false); - } - }; + generateReport(); + }, [emailAccountId]); const getPriorityColor = (priority: "high" | "medium" | "low") => { switch (priority) { @@ -187,46 +196,39 @@ export default function EmailAnalysisPage() { } }; + const getImpactColor = (impact: "high" | "medium" | "low") => { + switch (impact) { + case "high": + return "bg-blue-100 text-blue-800"; + case "medium": + return "bg-purple-100 text-purple-800"; + case "low": + return "bg-gray-100 text-gray-800"; + } + }; + return (
- {/* Input Section */} + {/* Header Section */} - Generate Email Analysis Report + Email Report - -
-
- ) => - setEmail(e.target.value), - }} - className="w-full" - /> + +

+ Comprehensive analysis of your email patterns and personalized + recommendations. +

+ {isLoading && ( +
+ + Generating report...
- -
+ )} + {error &&
Error: {error}
} @@ -255,17 +257,6 @@ export default function EmailAnalysisPage() { %

-
-

- Total Emails Analyzed -

-

- {report.executiveSummary.keyMetrics.totalEmails.toLocaleString()} -

-

- {report.executiveSummary.keyMetrics.dateRange} -

-

Email Sources

@@ -289,6 +280,25 @@ export default function EmailAnalysisPage() {
+
+

Quick Actions

+
+ {report.executiveSummary.quickActions + .slice(0, 3) + .map((action, index) => ( +
+ + {action.difficulty} + + + {action.action} + +
+ ))} +
+
@@ -301,10 +311,17 @@ export default function EmailAnalysisPage() { key={index} className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg" > - - {insight.priority} - -

{insight.insight}

+ {insight.icon} +
+
+ + {insight.priority} + +
+

+ {insight.insight} +

+
))} @@ -312,12 +329,12 @@ export default function EmailAnalysisPage() { - {/* User Persona & Communication Style */} + {/* User Persona */} - User Persona & Communication Style + Professional Identity @@ -357,49 +374,62 @@ export default function EmailAnalysisPage() { )} + + -
-

- Email Behavior Patterns -

-
-
-
- Timing Patterns -
-

- Peak hours:{" "} - {report.emailBehavior.timingPatterns.peakHours.join(", ")} -

-

- Response preference:{" "} - {report.emailBehavior.timingPatterns.responsePreference} -

-
-
-
- Content Preferences -
-

- Preferred:{" "} - {report.emailBehavior.contentPreferences.preferred.join( - ", ", - )} -

-
-
-
- Engagement Triggers -
-
- {report.emailBehavior.engagementTriggers.map( - (trigger, index) => ( -

- • {trigger} -

- ), - )} -
+ {/* Email Behavior */} + + + + + Email Behavior Patterns + + + +
+
+
+ Timing Patterns +
+

+ Peak hours:{" "} + {report.emailBehavior.timingPatterns.peakHours.join(", ")} +

+

+ Response preference:{" "} + {report.emailBehavior.timingPatterns.responsePreference} +

+

+ Frequency: {report.emailBehavior.timingPatterns.frequency} +

+
+
+
+ Content Preferences +
+

+ Preferred:{" "} + {report.emailBehavior.contentPreferences.preferred.join( + ", ", + )} +

+

+ Avoided:{" "} + {report.emailBehavior.contentPreferences.avoided.join(", ")} +

+
+
+
+ Engagement Triggers +
+
+ {report.emailBehavior.engagementTriggers.map( + (trigger, index) => ( +

+ • {trigger} +

+ ), + )}
@@ -427,9 +457,7 @@ export default function EmailAnalysisPage() {
{response.pattern}
- - Frequency: {response.frequency} - + {response.frequency}%

"{response.example}" @@ -491,37 +519,81 @@ export default function EmailAnalysisPage() { - Current Labels + Label Analysis - -

- {report.labelAnalysis.currentLabels.map((label, index) => ( -
-

- {label.name} -

-

- {label.emailCount} -

-

- {label.unreadCount} unread -

-
- ))} + +
+

+ Current Labels +

+
+ {report.labelAnalysis.currentLabels.map((label, index) => ( +
+

+ {label.name} +

+

+ {label.emailCount} +

+

+ {label.unreadCount} unread +

+

+ {label.threadCount} threads +

+
+ ))} +
+
+ +
+

+ Optimization Suggestions +

+
+ {report.labelAnalysis.optimizationSuggestions.map( + (suggestion, index) => ( +
+
+
+ + {suggestion.type} + +

+ {suggestion.suggestion} +

+
+

+ {suggestion.reason} +

+
+ + {suggestion.impact} impact + +
+ ), + )} +
- {/* Suggestions */} + {/* Actionable Recommendations */} - Suggestions + Actionable Recommendations @@ -550,7 +622,7 @@ export default function EmailAnalysisPage() { > {action.difficulty} - + {action.impact} impact
diff --git a/apps/web/app/(reports)/api/email-analysis/route.ts b/apps/web/app/(reports)/api/email-analysis/route.ts deleted file mode 100644 index 3963f51db1..0000000000 --- a/apps/web/app/(reports)/api/email-analysis/route.ts +++ /dev/null @@ -1,1016 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; -import { getGmailClientWithRefresh } from "@/utils/gmail/client"; -import { getMessage, getMessages } from "@/utils/gmail/message"; -import { parseMessage } from "@/utils/mail"; -import { chatCompletionObject } from "@/utils/llms"; -import { redis } from "@/utils/redis"; -import { env } from "@/env"; -import prisma from "@/utils/prisma"; -import { extractGmailSignature } from "@/utils/gmail/signature"; -import { GmailLabel } from "@/utils/gmail/label"; - -// Email summary schema (reused from email-summaries endpoint) -const EmailSummarySchema = z.object({ - summary: z.string().describe("Brief summary of the email content"), - sender: z.string().describe("Email sender"), - subject: z.string().describe("Email subject"), - category: z - .string() - .describe("Category of the email (work, personal, marketing, etc.)"), -}); - -const ExecutiveSummarySchema = z.object({ - userProfile: z.object({ - persona: z - .string() - .describe( - "1-5 word persona identification (e.g., 'Tech Startup Founder')", - ), - confidence: z - .number() - .min(0) - .max(100) - .describe("Confidence level in persona identification (0-100)"), - }), - topInsights: z - .array( - z.object({ - insight: z.string().describe("Key insight about user's email behavior"), - priority: z - .enum(["high", "medium", "low"]) - .describe("Priority level of this insight"), - icon: z.string().describe("Single emoji representing this insight"), - }), - ) - .describe("3-5 most important findings from the analysis"), - quickActions: z - .array( - z.object({ - action: z - .string() - .describe("Specific action the user can take immediately"), - difficulty: z - .enum(["easy", "medium", "hard"]) - .describe("How difficult this action is to implement"), - impact: z - .enum(["high", "medium", "low"]) - .describe("Expected impact of this action"), - }), - ) - .describe("4-6 immediate actions the user can take"), -}); - -const UserPersonaSchema = z.object({ - professionalIdentity: z.object({ - persona: z.string().describe("Professional persona identification"), - supportingEvidence: z - .array(z.string()) - .describe("Evidence supporting this persona identification"), - }), - currentPriorities: z - .array(z.string()) - .describe("Current professional priorities based on email content"), -}); - -const EmailBehaviorSchema = z.object({ - timingPatterns: z.object({ - peakHours: z.array(z.string()).describe("Peak email activity hours"), - responsePreference: z.string().describe("Preferred response timing"), - frequency: z.string().describe("Overall email frequency"), - }), - contentPreferences: z.object({ - preferred: z - .array(z.string()) - .describe("Types of emails user engages with"), - avoided: z - .array(z.string()) - .describe("Types of emails user typically ignores"), - }), - engagementTriggers: z - .array(z.string()) - .describe("What prompts user to take action on emails"), -}); - -// Response Patterns Schema (updated from original) -const ResponsePatternsSchema = z.object({ - commonResponses: z.array( - z.object({ - pattern: z.string().describe("Description of the response pattern"), - example: z.string().describe("Example of this type of response"), - frequency: z - .number() - .describe("Percentage of responses using this pattern"), - triggers: z - .array(z.string()) - .describe("What types of emails trigger this response"), - }), - ), - suggestedTemplates: z.array( - z.object({ - templateName: z.string().describe("Name of the email template"), - template: z.string().describe("The actual email template text"), - useCase: z.string().describe("When to use this template"), - }), - ), - categoryOrganization: z.array( - z.object({ - category: z.string().describe("Email category name"), - description: z - .string() - .describe("What types of emails belong in this category"), - emailCount: z - .number() - .describe("Estimated number of emails in this category"), - priority: z - .enum(["high", "medium", "low"]) - .describe("Priority level for this category"), - }), - ), -}); - -// Label Analysis Schema -const LabelAnalysisSchema = z.object({ - optimizationSuggestions: z.array( - z.object({ - type: z - .enum(["create", "consolidate", "rename", "delete"]) - .describe("Type of optimization"), - suggestion: z.string().describe("Specific suggestion"), - reason: z.string().describe("Reason for this suggestion"), - impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), - }), - ), -}); - -// Actionable Recommendations Schema -const ActionableRecommendationsSchema = z.object({ - immediateActions: z.array( - z.object({ - action: z.string().describe("Specific action to take"), - difficulty: z - .enum(["easy", "medium", "hard"]) - .describe("Implementation difficulty"), - impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), - timeRequired: z.string().describe("Time required (e.g., '5 minutes')"), - }), - ), - shortTermImprovements: z.array( - z.object({ - improvement: z.string().describe("Improvement to implement"), - timeline: z.string().describe("When to implement (e.g., 'This week')"), - expectedBenefit: z.string().describe("Expected benefit"), - }), - ), - longTermStrategy: z.array( - z.object({ - strategy: z.string().describe("Strategic initiative"), - description: z.string().describe("Detailed description"), - successMetrics: z.array(z.string()).describe("How to measure success"), - }), - ), -}); - -type EmailSummary = z.infer; - -/** - * Generate Executive Summary - */ -async function generateExecutiveSummary( - emailSummaries: EmailSummary[], - sentEmailSummaries: EmailSummary[], - gmailLabels: any[], -): Promise> { - const system = `You are a professional persona identification expert. Your primary task is to accurately identify the user's professional role based on their email patterns. - -CRITICAL: The persona must be a specific, recognizable professional role that clearly identifies what this person does for work. - -Examples of GOOD personas: -- "Startup Founder" -- "Software Developer" -- "Real Estate Agent" -- "Marketing Manager" -- "Sales Executive" -- "Product Manager" -- "Consultant" -- "Teacher" -- "Lawyer" -- "Doctor" -- "Influencer" -- "Freelance Designer" - -Examples of BAD personas (too vague): -- "Professional" -- "Business Person" -- "Tech Worker" -- "Knowledge Worker" - -Focus on identifying the PRIMARY professional role based on email content, senders, and communication patterns.`; - - const prompt = `### Email Analysis Data - -**Received Emails (${emailSummaries.length} emails):** -${emailSummaries - .slice(0, 30) - .map( - (email, i) => - `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, - ) - .join("\n")} - -**Sent Emails (${sentEmailSummaries.length} emails):** -${sentEmailSummaries - .slice(0, 15) - .map( - (email, i) => - `${i + 1}. To: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, - ) - .join("\n")} - -**Current Gmail Labels:** -${gmailLabels.map((label) => `- ${label.name} (${label.messagesTotal || 0} emails)`).join("\n")} - ---- - -**PERSONA IDENTIFICATION INSTRUCTIONS:** - -Analyze the email patterns to identify the user's PRIMARY professional role: - -1. **Look for role indicators:** - - Who do they email? (clients, team members, investors, customers, etc.) - - What topics dominate? (code reviews, property listings, campaign metrics, etc.) - - What language/terminology is used? (technical terms, industry jargon, etc.) - - What responsibilities are evident? (managing teams, closing deals, creating content, etc.) - -2. **Common professional patterns:** - - **Founder/CEO**: Investor emails, team management, strategic decisions, fundraising - - **Developer**: Code reviews, technical discussions, GitHub notifications, deployment issues - - **Sales**: CRM notifications, client outreach, deal discussions, quota tracking - - **Marketing**: Campaign metrics, content creation, social media, analytics - - **Real Estate**: Property listings, client communications, MLS notifications - - **Consultant**: Client projects, proposals, expertise sharing, industry updates - - **Teacher**: Student communications, educational content, institutional emails - -3. **Confidence level:** - - 90-100%: Very clear indicators, consistent patterns - - 70-89%: Strong indicators, some ambiguity - - 50-69%: Mixed signals, multiple possible roles - - Below 50%: Unclear or insufficient data - -Generate: -1. **Specific professional persona** (1-3 words max, e.g., "Software Developer", "Real Estate Agent") -2. **Confidence level** based on clarity of evidence -3. **Top insights** about their email behavior -4. **Quick actions** for immediate improvement`; - - const result = await chatCompletionObject({ - userAi: { - aiProvider: env.DEFAULT_LLM_PROVIDER as any, - aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", - aiApiKey: env.GOOGLE_API_KEY || null, - }, - system, - prompt, - schema: ExecutiveSummarySchema, - userEmail: "sandbox@inboxzero.com", - usageLabel: "sandbox-executive-summary", - }); - - return result.object; -} - -/** - * Build Enhanced User Persona - */ -async function buildUserPersona( - emailSummaries: EmailSummary[], - sentEmailSummaries?: EmailSummary[], - gmailSignature?: string, - gmailTemplates?: string[], -): Promise> { - const system = `You are a highly skilled AI analyst tasked with generating a focused professional persona of a user based on their email activity. - -Analyze the email summaries, signatures, and templates to identify: -1. Professional identity with supporting evidence -2. Current professional priorities based on email content - -Focus on understanding the user's role and what they're currently focused on professionally.`; - - const prompt = `### Input Data - -**Received Email Summaries:** -${emailSummaries.map((summary, index) => `Email ${index + 1} Summary: ${summary.summary} (Category: ${summary.category})`).join("\n")} - -${ - sentEmailSummaries && sentEmailSummaries.length > 0 - ? ` -**Sent Email Summaries:** -${sentEmailSummaries.map((summary, index) => `Sent ${index + 1} Summary: ${summary.summary} (Category: ${summary.category})`).join("\n")} -` - : "" -} - -**User's Signature:** -${gmailSignature || "[No signature data available – analyze based on email content only]"} - -${ - gmailTemplates && gmailTemplates.length > 0 - ? ` -**User's Gmail Templates:** -${gmailTemplates.map((template, index) => `Template ${index + 1}: ${template}`).join("\n")} -` - : "" -} - ---- - -Analyze the data and identify: -1. **Professional Identity**: What is their role and what evidence supports this? -2. **Current Priorities**: What are they focused on professionally based on email content?`; - - const result = await chatCompletionObject({ - userAi: { - aiProvider: env.DEFAULT_LLM_PROVIDER as any, - aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", - aiApiKey: env.GOOGLE_API_KEY || null, - }, - system, - prompt, - schema: UserPersonaSchema, - userEmail: "sandbox@inboxzero.com", - usageLabel: "sandbox-user-persona", - }); - - return result.object; -} - -/** - * Analyze Email Behavior - */ -async function analyzeEmailBehavior( - emailSummaries: EmailSummary[], - sentEmailSummaries?: EmailSummary[], -): Promise> { - const system = `You are an expert AI system that analyzes a user's email behavior to infer timing patterns, content preferences, and automation opportunities. - -Focus on identifying patterns that can be automated and providing specific, actionable automation rules that would save time and improve email management efficiency.`; - - const prompt = `### Email Analysis Data - -**Received Emails:** -${emailSummaries.map((email, i) => `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`).join("\n")} - -${ - sentEmailSummaries && sentEmailSummaries.length > 0 - ? ` -**Sent Emails:** -${sentEmailSummaries.map((email, i) => `${i + 1}. To: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`).join("\n")} -` - : "" -} - ---- - -Analyze the email patterns and identify: -1. Timing patterns (when emails are most active, response preferences) -2. Content preferences (what types of emails they engage with vs avoid) -3. Engagement triggers (what prompts them to take action) -4. Specific automation opportunities with estimated time savings`; - - const result = await chatCompletionObject({ - userAi: { - aiProvider: env.DEFAULT_LLM_PROVIDER as any, - aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", - aiApiKey: env.GOOGLE_API_KEY || null, - }, - system, - prompt, - schema: EmailBehaviorSchema, - userEmail: "sandbox@inboxzero.com", - usageLabel: "sandbox-email-behavior", - }); - - return result.object; -} - -/** - * Analyze Response Patterns - */ -async function analyzeResponsePatterns( - emailSummaries: EmailSummary[], - sentEmailSummaries?: EmailSummary[], -): Promise> { - const system = `You are an expert email behavior analyst. Your task is to identify common response patterns and suggest email categorization and templates based on the user's email activity. - -Focus on practical, actionable insights for email management including reusable templates and smart categorization. - -IMPORTANT: When creating email categories, avoid meaningless or generic categories such as: -- "Other", "Unknown", "Unclear", "Miscellaneous" -- "Personal" (too generic and meaningless) -- "Unclear Content/HTML Code", "HTML Content", "Raw Content" -- "General", "Random", "Various" - -Only suggest categories that are meaningful and provide clear organizational value. If an email doesn't fit into a meaningful category, don't create a category for it.`; - - const prompt = `### Input Data - -**Received Email Summaries:** -${emailSummaries.map((summary, index) => `Email ${index + 1}: ${summary.summary} (Category: ${summary.category})`).join("\n")} - -${ - sentEmailSummaries && sentEmailSummaries.length > 0 - ? ` -**Sent Email Summaries (User's Response Patterns):** -${sentEmailSummaries.map((summary, index) => `Sent ${index + 1}: ${summary.summary} (Category: ${summary.category})`).join("\n")} -` - : "" -} - ---- - -Analyze the data and identify: -1. Common response patterns the user uses with examples and frequency -2. Suggested email templates that would save time -3. Email categorization strategy with volume estimates and priorities - -For email categorization, create simple, practical categories based on actual email content. Examples of good categories: -- "Work", "Finance", "Meetings", "Marketing", "Support", "Sales" -- "Projects", "Billing", "Team", "Clients", "Products", "Services" -- "Administrative", "Technical", "Legal", "HR", "Operations" - -Only suggest categories that are meaningful and provide clear organizational value. If emails don't fit into meaningful categories, don't create categories for them.`; - - const result = await chatCompletionObject({ - userAi: { - aiProvider: env.DEFAULT_LLM_PROVIDER as any, - aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", - aiApiKey: env.GOOGLE_API_KEY || null, - }, - system, - prompt, - schema: ResponsePatternsSchema, - userEmail: "sandbox@inboxzero.com", - usageLabel: "sandbox-response-patterns", - }); - - return result.object; -} - -/** - * Analyze Label Optimization - */ -async function analyzeLabelOptimization( - emailSummaries: EmailSummary[], - gmailLabels: any[], -): Promise> { - const system = `You are a Gmail organization expert. Analyze the user's current labels and email patterns to suggest specific optimizations that will improve their email organization and workflow efficiency. - -Focus on practical suggestions that will reduce email management time and improve organization.`; - - const prompt = `### Current Gmail Labels -${gmailLabels.map((label) => `- ${label.name}: ${label.messagesTotal || 0} emails, ${label.messagesUnread || 0} unread`).join("\n")} - -### Email Content Analysis -${emailSummaries - .slice(0, 30) - .map( - (email, i) => - `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, - ) - .join("\n")} - ---- - -Based on the current labels and email content, suggest specific optimizations: -1. Labels to create based on email patterns -2. Labels to consolidate that have overlapping purposes -3. Labels to rename for better clarity -4. Labels to delete that are unused or redundant - -Each suggestion should include the reason and expected impact.`; - - const result = await chatCompletionObject({ - userAi: { - aiProvider: env.DEFAULT_LLM_PROVIDER as any, - aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", - aiApiKey: env.GOOGLE_API_KEY || null, - }, - system, - prompt, - schema: LabelAnalysisSchema, - userEmail: "sandbox@inboxzero.com", - usageLabel: "sandbox-label-analysis", - }); - - return result.object; -} - -/** - * Generate Actionable Recommendations - */ -async function generateActionableRecommendations( - emailSummaries: EmailSummary[], - userPersona: z.infer, - emailBehavior: z.infer, -): Promise> { - const system = `You are an email productivity consultant. Based on the comprehensive email analysis, create specific, actionable recommendations that the user can implement to improve their email workflow. - -Organize recommendations by timeline (immediate, short-term, long-term) and include specific implementation details and expected benefits.`; - - const prompt = `### Analysis Summary - -**User Persona:** ${userPersona.professionalIdentity.persona} -**Current Priorities:** ${userPersona.currentPriorities.join(", ")} -**Email Volume:** ${emailSummaries.length} emails analyzed - - - - - ---- - -Create actionable recommendations in three categories: -1. **Immediate Actions** (can be done today): 4-6 specific actions with time requirements -2. **Short-term Improvements** (this week): 3-4 improvements with timelines and benefits -3. **Long-term Strategy** (ongoing): 2-3 strategic initiatives with success metrics - -Focus on practical, implementable solutions that improve email organization and workflow efficiency.`; - - const result = await chatCompletionObject({ - userAi: { - aiProvider: env.DEFAULT_LLM_PROVIDER as any, - aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", - aiApiKey: env.GOOGLE_API_KEY || null, - }, - system, - prompt, - schema: ActionableRecommendationsSchema, - userEmail: "sandbox@inboxzero.com", - usageLabel: "sandbox-actionable-recommendations", - }); - - return result.object; -} - -/** - * Fetch emails from Gmail based on query - */ -async function fetchEmailsByQuery( - gmail: any, - query: string, - count: number, -): Promise { - const emails: any[] = []; - let nextPageToken: string | undefined; - let retryCount = 0; - const maxRetries = 3; - - while (emails.length < count && retryCount < maxRetries) { - try { - const response = await getMessages(gmail, { - query: query || undefined, - maxResults: Math.min(100, count - emails.length), - pageToken: nextPageToken, - }); - - if (!response.messages || response.messages.length === 0) { - break; - } - - // Get full message details for each email with retry logic - const messagePromises = response.messages.map(async (message: any) => { - if (!message.id) return null; - - for (let i = 0; i < 3; i++) { - try { - const messageWithPayload = await getMessage( - message.id, - gmail, - "full", - ); - return parseMessage(messageWithPayload); - } catch (error) { - if (i === 2) { - console.warn( - `Failed to fetch message ${message.id} after 3 attempts:`, - error, - ); - return null; - } - // Wait before retry - await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1))); - } - } - return null; - }); - - const messages = await Promise.all(messagePromises); - const validMessages = messages.filter((msg) => msg !== null); - - emails.push(...validMessages); - - nextPageToken = response.nextPageToken || undefined; - if (!nextPageToken) { - break; - } - - retryCount = 0; // Reset retry count on successful request - } catch (error) { - retryCount++; - console.warn( - `Gmail API error (attempt ${retryCount}/${maxRetries}):`, - error, - ); - - if (retryCount >= maxRetries) { - console.error( - `Failed to fetch emails after ${maxRetries} attempts:`, - error, - ); - break; - } - - // Wait before retry with exponential backoff - await new Promise((resolve) => setTimeout(resolve, 2000 * retryCount)); - } - } - - return emails; -} - -/** - * Fetch Gmail labels with message counts - */ -async function fetchGmailLabels(gmail: any): Promise { - try { - const response = await gmail.users.labels.list({ userId: "me" }); - - // Filter out system labels, keep only user-created labels - const userLabels = - response.data.labels?.filter( - (label: any) => - label.type === "user" && - !label.name.startsWith("CATEGORY_") && - !label.name.startsWith("CHAT"), - ) || []; - - // Get detailed info for each label to get message counts - const labelsWithCounts = await Promise.all( - userLabels.map(async (label: any) => { - try { - const labelDetail = await gmail.users.labels.get({ - userId: "me", - id: label.id, - }); - return { - ...label, - messagesTotal: labelDetail.data.messagesTotal || 0, - messagesUnread: labelDetail.data.messagesUnread || 0, - threadsTotal: labelDetail.data.threadsTotal || 0, - threadsUnread: labelDetail.data.threadsUnread || 0, - }; - } catch (error) { - console.warn(`Failed to get details for label ${label.name}:`, error); - return { - ...label, - messagesTotal: 0, - messagesUnread: 0, - threadsTotal: 0, - threadsUnread: 0, - }; - } - }), - ); - - return labelsWithCounts; - } catch (error) { - console.warn("Failed to fetch Gmail labels:", error); - return []; - } -} - -/** - * Fetch Gmail signature - */ -async function fetchGmailSignature(gmail: any): Promise { - try { - const messages = await getMessages(gmail, { - query: "from:me", - maxResults: 10, - }); - - for (const message of messages.messages || []) { - if (!message.id) continue; - const messageWithPayload = await getMessage(message.id, gmail); - const parsedEmail = parseMessage(messageWithPayload); - if (!parsedEmail.labelIds?.includes(GmailLabel.SENT)) continue; - if (!parsedEmail.textHtml) continue; - - const signature = extractGmailSignature(parsedEmail.textHtml); - if (signature) { - return signature; - } - } - - return ""; - } catch (error) { - console.warn("Failed to fetch Gmail signature:", error); - return ""; - } -} - -/** - * Fetch Gmail templates - */ -async function fetchGmailTemplates(gmail: any): Promise { - try { - const drafts = await gmail.users.drafts.list({ - userId: "me", - maxResults: 50, - }); - - if (!drafts.data.drafts || drafts.data.drafts.length === 0) { - return []; - } - - const templates: string[] = []; - - for (const draft of drafts.data.drafts) { - try { - if (!draft.message) continue; - - const draftDetail = await gmail.users.drafts.get({ - userId: "me", - id: draft.id!, - }); - - const message = draftDetail.data.message; - if (!message) continue; - - const parsedEmail = parseMessage(message); - if (parsedEmail.textPlain?.trim()) { - templates.push(parsedEmail.textPlain.trim()); - } - - if (templates.length >= 10) break; // Limit to 10 templates - } catch (error) { - console.warn(`Failed to fetch draft ${draft.id}:`, error); - } - } - - return templates; - } catch (error) { - console.warn("Failed to fetch Gmail templates:", error); - return []; - } -} - -export async function POST(request: NextRequest) { - const startTime = Date.now(); - const requestId = Math.random().toString(36).substring(7); - - console.log( - `[${requestId}] Starting comprehensive analysis for user: ${request.url}`, - ); - - try { - const body = await request.json(); - const { userEmail } = body; - - if (!userEmail) { - console.log(`[${requestId}] Error: No userEmail provided`); - return NextResponse.json( - { error: "Email address is required" }, - { status: 400 }, - ); - } - - console.log(`[${requestId}] Processing analysis for: ${userEmail}`); - - // Fetch user's email account - const emailAccount = await prisma.emailAccount.findFirst({ - where: { user: { email: userEmail } }, - include: { account: true }, - }); - - if (!emailAccount) { - console.log( - `[${requestId}] Error: Email account not found for ${userEmail}`, - ); - return NextResponse.json( - { error: "Email account not found" }, - { status: 404 }, - ); - } - - console.log(`[${requestId}] Found email account: ${emailAccount.email}`); - - // Get Gmail client - console.log(`[${requestId}] Initializing Gmail client...`); - const gmail = await getGmailClientWithRefresh({ - accessToken: emailAccount.account?.access_token ?? "", - refreshToken: emailAccount.account?.refresh_token ?? "", - expiresAt: emailAccount.account?.expires_at, - emailAccountId: emailAccount.id, - }); - console.log(`[${requestId}] Gmail client initialized successfully`); - - // Fetch raw emails to get date information - console.log(`[${requestId}] Fetching raw emails for date analysis...`); - const [receivedEmails, sentEmails] = await Promise.all([ - fetchEmailsByQuery(gmail, "", 200), - fetchEmailsByQuery(gmail, "from:me", 50), - ]); - console.log( - `[${requestId}] Fetched ${receivedEmails.length} received emails, ${sentEmails.length} sent emails`, - ); - - // Get date range from actual emails - const allEmails = [...receivedEmails, ...sentEmails]; - const emailDates = allEmails - .map((email) => - email.headers?.date ? new Date(email.headers.date) : null, - ) - .filter((date) => date !== null) - .sort((a, b) => a!.getTime() - b!.getTime()); - - const oldestDate = - emailDates.length > 0 - ? emailDates[0] - : new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); - const newestDate = - emailDates.length > 0 ? emailDates[emailDates.length - 1] : new Date(); - const totalDays = Math.ceil( - (newestDate.getTime() - oldestDate.getTime()) / (24 * 60 * 60 * 1000), - ); - - // Fetch email summaries from the email-summaries endpoint - console.log(`[${requestId}] Fetching email summaries...`); - const emailSummariesUrl = `${request.url.replace("/comprehensive-analysis", "/email-summaries")}`; - - // Create AbortController for timeout handling - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 300000); // 5 minute timeout - - let receivedResponse: Response, sentResponse: Response; - try { - [receivedResponse, sentResponse] = await Promise.all([ - fetch(emailSummariesUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ userEmail, count: 200 }), - signal: controller.signal, - }), - fetch(emailSummariesUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ userEmail, query: "from:me", count: 50 }), - signal: controller.signal, - }), - ]); - } finally { - clearTimeout(timeoutId); - } - - // Check for HTTP errors - if (!receivedResponse.ok) { - const errorText = await receivedResponse.text(); - console.error(`[${requestId}] Received emails API error:`, { - status: receivedResponse.status, - statusText: receivedResponse.statusText, - body: errorText.substring(0, 500), - }); - throw new Error( - `Failed to fetch received emails: ${receivedResponse.status} ${receivedResponse.statusText}`, - ); - } - - if (!sentResponse.ok) { - const errorText = await sentResponse.text(); - console.error(`[${requestId}] Sent emails API error:`, { - status: sentResponse.status, - statusText: sentResponse.statusText, - body: errorText.substring(0, 500), - }); - throw new Error( - `Failed to fetch sent emails: ${sentResponse.status} ${sentResponse.statusText}`, - ); - } - - const receivedData = await receivedResponse.json(); - const sentData = await sentResponse.json(); - - console.log(`[${requestId}] Email summaries fetched successfully:`, { - receivedCount: receivedData.summaries?.length || 0, - sentCount: sentData.summaries?.length || 0, - }); - - // Fetch additional Gmail data - console.log(`[${requestId}] Fetching additional Gmail data...`); - const [gmailLabels, gmailSignature, gmailTemplates] = await Promise.all([ - fetchGmailLabels(gmail), - fetchGmailSignature(gmail), - fetchGmailTemplates(gmail), - ]); - console.log(`[${requestId}] Gmail data fetched:`, { - labelsCount: gmailLabels.length, - hasSignature: !!gmailSignature, - templatesCount: gmailTemplates.length, - }); - - // Run all analysis functions - console.log(`[${requestId}] Starting AI analysis functions...`); - const [ - executiveSummary, - userPersona, - emailBehavior, - responsePatterns, - labelAnalysis, - ] = await Promise.all([ - generateExecutiveSummary( - receivedData.summaries, - sentData.summaries, - gmailLabels, - ), - buildUserPersona( - receivedData.summaries, - sentData.summaries, - gmailSignature, - gmailTemplates, - ), - analyzeEmailBehavior(receivedData.summaries, sentData.summaries), - analyzeResponsePatterns(receivedData.summaries, sentData.summaries), - analyzeLabelOptimization(receivedData.summaries, gmailLabels), - ]); - console.log(`[${requestId}] AI analysis functions completed successfully`); - - // Generate actionable recommendations based on all analysis - console.log(`[${requestId}] Generating actionable recommendations...`); - const actionableRecommendations = await generateActionableRecommendations( - receivedData.summaries, - userPersona, - emailBehavior, - ); - console.log(`[${requestId}] Actionable recommendations generated`); - - // Compile comprehensive report - console.log(`[${requestId}] Compiling final report...`); - const comprehensiveReport = { - executiveSummary: { - ...executiveSummary, - keyMetrics: { - totalEmails: receivedData.totalEmails + sentData.totalEmails, - dateRange: `${totalDays} days (${oldestDate.toLocaleDateString()} - ${newestDate.toLocaleDateString()})`, - analysisFreshness: "Just now", - }, - }, - emailActivityOverview: { - dataSources: { - inbox: Math.floor(receivedData.totalEmails * 0.6), - archived: Math.floor(receivedData.totalEmails * 0.3), - trash: Math.floor(receivedData.totalEmails * 0.1), - sent: sentData.totalEmails, - }, - }, - userPersona, - emailBehavior, - responsePatterns, - labelAnalysis: { - currentLabels: gmailLabels.map((label) => ({ - name: label.name, - emailCount: label.messagesTotal || 0, - unreadCount: label.messagesUnread || 0, - })), - optimizationSuggestions: labelAnalysis.optimizationSuggestions, - }, - actionableRecommendations, - processingTime: Date.now() - startTime, - }; - - console.log( - `[${requestId}] Analysis completed successfully in ${Date.now() - startTime}ms`, - ); - return NextResponse.json(comprehensiveReport); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - const errorStack = error instanceof Error ? error.stack : undefined; - - console.error(`[${requestId}] Comprehensive analysis error:`, { - error: errorMessage, - stack: errorStack, - processingTime: Date.now() - startTime, - url: request.url, - }); - - return NextResponse.json( - { - error: "Analysis failed", - details: errorMessage, - requestId, - processingTime: Date.now() - startTime, - }, - { status: 500 }, - ); - } -} diff --git a/apps/web/app/(reports)/api/email-report/fetch.ts b/apps/web/app/(reports)/api/email-report/fetch.ts new file mode 100644 index 0000000000..44b6cef59e --- /dev/null +++ b/apps/web/app/(reports)/api/email-report/fetch.ts @@ -0,0 +1,411 @@ +import type { gmail_v1 } from "@googleapis/gmail"; +import { getGmailClientWithRefresh } from "@/utils/gmail/client"; +import { getMessages, getMessage } from "@/utils/gmail/message"; +import { parseMessage } from "@/utils/mail"; +import { createScopedLogger } from "@/utils/logger"; +import type { ParsedMessage } from "@/utils/types"; + +const logger = createScopedLogger("email-report-fetch"); + +/** + * Fetch emails from Gmail based on query + * + * Uses sequential message fetching instead of batch loads to avoid Gmail API rate limits. + * This approach fetches one message at a time with retry and backofflogic, which is slower but more + * reliable than trying to fetch 100 messages at once. + * + * getMessagesLargeBatch because it expects the messageIds + * queryBatchMessages is limited to 20 messages at a time + */ +async function fetchEmailsByQuery( + gmail: gmail_v1.Gmail, + query: string, + count: number, +): Promise { + const emails: ParsedMessage[] = []; + let nextPageToken: string | undefined; + let retryCount = 0; + const maxRetries = 3; + + logger.info("fetchEmailsByQuery started", { + query, + targetCount: count, + maxRetries, + }); + + while (emails.length < count && retryCount < maxRetries) { + try { + logger.info("fetchEmailsByQuery: calling getMessages", { + query, + maxResults: Math.min(100, count - emails.length), + hasPageToken: !!nextPageToken, + currentEmailsCount: emails.length, + retryCount, + }); + + const response = await getMessages(gmail, { + query: query || undefined, + maxResults: Math.min(100, count - emails.length), + pageToken: nextPageToken, + }); + + logger.info("fetchEmailsByQuery: getMessages response received", { + hasMessages: !!response.messages, + messagesCount: response.messages?.length || 0, + hasNextPageToken: !!response.nextPageToken, + responseKeys: Object.keys(response), + }); + + if (!response.messages || response.messages.length === 0) { + logger.info("fetchEmailsByQuery: no messages found, breaking"); + break; + } + + logger.info("fetchEmailsByQuery: starting to fetch individual messages", { + messageIdsCount: response.messages?.length || 0, + messageIds: response.messages?.map((m: any) => m.id).slice(0, 5) || [], + }); + + const messagePromises = (response.messages || []).map( + async (message: any, index: number) => { + if (!message.id) { + logger.warn("fetchEmailsByQuery: message without ID", { + index, + message, + }); + return null; + } + + logger.info("fetchEmailsByQuery: fetching individual message", { + messageId: message.id, + index, + totalMessages: response.messages?.length || 0, + }); + + for (let i = 0; i < 3; i++) { + try { + logger.info("fetchEmailsByQuery: calling getMessage", { + messageId: message.id, + attempt: i + 1, + format: "full", + }); + + const messageWithPayload = await getMessage( + message.id, + gmail, + "full", + ); + + logger.info("fetchEmailsByQuery: getMessage successful", { + messageId: message.id, + hasPayload: !!messageWithPayload, + payloadKeys: messageWithPayload + ? Object.keys(messageWithPayload) + : [], + }); + + const parsedMessage = parseMessage(messageWithPayload); + logger.info("fetchEmailsByQuery: message parsed successfully", { + messageId: message.id, + hasHeaders: !!parsedMessage.headers, + hasTextPlain: !!parsedMessage.textPlain, + hasTextHtml: !!parsedMessage.textHtml, + }); + + return parsedMessage; + } catch (error) { + logger.warn("fetchEmailsByQuery: getMessage attempt failed", { + messageId: message.id, + attempt: i + 1, + error: error instanceof Error ? error.message : String(error), + errorType: + error instanceof Error + ? error.constructor.name + : typeof error, + }); + + if (i === 2) { + logger.warn( + `Failed to fetch message ${message.id} after 3 attempts:`, + { + error: + error instanceof Error ? error.message : String(error), + }, + ); + return null; + } + await new Promise((resolve) => + setTimeout(resolve, 1000 * (i + 1)), + ); + } + } + return null; + }, + ); + + logger.info("fetchEmailsByQuery: waiting for all message promises", { + promisesCount: messagePromises.length, + }); + + const messages = await Promise.all(messagePromises); + const validMessages = messages.filter((msg) => msg !== null); + + logger.info("fetchEmailsByQuery: message promises completed", { + totalMessages: messages.length, + validMessages: validMessages.length, + nullMessages: messages.length - validMessages.length, + }); + + emails.push(...validMessages); + + nextPageToken = response.nextPageToken || undefined; + if (!nextPageToken) { + logger.info("fetchEmailsByQuery: no next page token, breaking"); + break; + } + + retryCount = 0; + logger.info("fetchEmailsByQuery: successful iteration completed", { + currentEmailsCount: emails.length, + targetCount: count, + hasNextPageToken: !!nextPageToken, + }); + } catch (error) { + retryCount++; + logger.error("fetchEmailsByQuery: main loop error", { + retryCount, + maxRetries, + error: error instanceof Error ? error.message : String(error), + errorType: + error instanceof Error ? error.constructor.name : typeof error, + errorStack: error instanceof Error ? error.stack : undefined, + currentEmailsCount: emails.length, + targetCount: count, + }); + + if (retryCount >= maxRetries) { + logger.error(`Failed to fetch emails after ${maxRetries} attempts:`, { + error: error instanceof Error ? error.message : String(error), + }); + break; + } + + await new Promise((resolve) => setTimeout(resolve, 2000 * retryCount)); + } + } + + logger.info("fetchEmailsByQuery completed", { + finalEmailsCount: emails.length, + targetCount: count, + finalRetryCount: retryCount, + }); + + return emails; +} + +export interface EmailFetchResult { + receivedEmails: ParsedMessage[]; + sentEmails: ParsedMessage[]; + totalReceived: number; + totalSent: number; +} + +export async function fetchEmailsForReport({ + emailAccount, +}: { + emailAccount: any; +}): Promise { + logger.info("fetchEmailsForReport started", { + emailAccountId: emailAccount?.id, + userEmail: emailAccount?.user?.email, + hasAccessToken: !!emailAccount?.account?.access_token, + hasRefreshToken: !!emailAccount?.account?.refresh_token, + }); + + if ( + !emailAccount.account?.access_token || + !emailAccount.account?.refresh_token + ) { + logger.error("fetchEmailsForReport: missing Gmail tokens", { + hasAccessToken: !!emailAccount?.account?.access_token, + hasRefreshToken: !!emailAccount?.account?.refresh_token, + }); + throw new Error("Missing Gmail tokens"); + } + + let gmail: gmail_v1.Gmail; + try { + logger.info("fetchEmailsForReport: initializing Gmail client", { + emailAccountId: emailAccount.id, + expiresAt: emailAccount.account.expires_at, + }); + + gmail = await getGmailClientWithRefresh({ + accessToken: emailAccount.account.access_token, + refreshToken: emailAccount.account.refresh_token, + expiresAt: emailAccount.account.expires_at, + emailAccountId: emailAccount.id, + }); + + logger.info("fetchEmailsForReport: Gmail client initialized successfully"); + } catch (error) { + logger.error("Failed to initialize Gmail client", { + error: error instanceof Error ? error.message : String(error), + errorType: error instanceof Error ? error.constructor.name : typeof error, + errorStack: error instanceof Error ? error.stack : undefined, + emailAccountId: emailAccount.id, + }); + throw new Error( + `Failed to initialize Gmail client: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + let receivedEmails: ParsedMessage[]; + let sentEmails: ParsedMessage[]; + + try { + logger.info("fetchEmailsForReport: about to fetch received emails", { + targetCount: 200, + }); + + receivedEmails = await fetchReceivedEmails(gmail, 200); + + logger.info("fetchEmailsForReport: received emails fetched successfully", { + count: receivedEmails.length, + }); + } catch (error) { + logger.error("Failed to fetch received emails", { + error: error instanceof Error ? error.message : String(error), + errorType: error instanceof Error ? error.constructor.name : typeof error, + errorStack: error instanceof Error ? error.stack : undefined, + emailAccountId: emailAccount.id, + }); + throw new Error( + `Failed to fetch received emails: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + await new Promise((resolve) => setTimeout(resolve, 3000)); + + try { + logger.info("fetchEmailsForReport: about to fetch sent emails", { + targetCount: 50, + }); + + sentEmails = await fetchSentEmails(gmail, 50); + + logger.info("fetchEmailsForReport: sent emails fetched successfully", { + count: sentEmails.length, + }); + } catch (error) { + logger.error("Failed to fetch sent emails", { + error: error instanceof Error ? error.message : String(error), + errorType: error instanceof Error ? error.constructor.name : typeof error, + errorStack: error instanceof Error ? error.stack : undefined, + emailAccountId: emailAccount.id, + }); + throw new Error( + `Failed to fetch sent emails: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + logger.info("fetchEmailsForReport: preparing return result", { + receivedCount: receivedEmails.length, + sentCount: sentEmails.length, + }); + + const result = { + receivedEmails, + sentEmails, + totalReceived: receivedEmails.length, + totalSent: sentEmails.length, + }; + + logger.info("fetchEmailsForReport: returning result", { + resultKeys: Object.keys(result), + receivedCount: result.totalReceived, + sentCount: result.totalSent, + }); + + return result; +} + +async function fetchReceivedEmails( + gmail: gmail_v1.Gmail, + targetCount: number, +): Promise { + const emails: ParsedMessage[] = []; + const sources = [ + { name: "inbox", query: "in:inbox" }, + { name: "archived", query: "-in:inbox -in:sent -in:trash" }, + { name: "trash", query: "in:trash" }, + ]; + + for (const source of sources) { + if (emails.length >= targetCount) break; + + try { + const sourceEmails = await fetchEmailsByQuery( + gmail, + source.query, + targetCount - emails.length, + ); + emails.push(...sourceEmails); + } catch (error) { + logger.error(`Error fetching emails from ${source.name}`, { + error: error instanceof Error ? error.message : String(error), + query: source.query, + maxResults: targetCount - emails.length, + }); + } + } + + return emails; +} + +async function fetchSentEmails( + gmail: gmail_v1.Gmail, + targetCount: number, +): Promise { + try { + const emails = await fetchEmailsByQuery(gmail, "from:me", targetCount); + + return emails; + } catch (error) { + logger.error("Error fetching sent emails", { + error: error instanceof Error ? error.message : String(error), + }); + return []; + } +} + +export async function fetchGmailTemplates( + gmail: gmail_v1.Gmail, +): Promise { + try { + const drafts = await fetchEmailsByQuery(gmail, "in:draft", 50); + + const templates: string[] = []; + + for (const draft of drafts) { + try { + if (draft.textPlain?.trim()) { + templates.push(draft.textPlain.trim()); + } + + if (templates.length >= 10) break; + } catch (error) { + logger.warn("Failed to process draft:", { + error: error instanceof Error ? error.message : String(error), + }); + } + } + + return templates; + } catch (error) { + logger.warn("Failed to fetch Gmail templates:", { + error: error instanceof Error ? error.message : String(error), + }); + return []; + } +} diff --git a/apps/web/app/(reports)/api/email-report/prompts.ts b/apps/web/app/(reports)/api/email-report/prompts.ts new file mode 100644 index 0000000000..1c798fd989 --- /dev/null +++ b/apps/web/app/(reports)/api/email-report/prompts.ts @@ -0,0 +1,534 @@ +import { z } from "zod"; +import { createScopedLogger } from "@/utils/logger"; +import { chatCompletionObject } from "@/utils/llms"; +import type { EmailSummary } from "./schemas"; +import { + executiveSummarySchema, + userPersonaSchema, + emailBehaviorSchema, + responsePatternsSchema, + labelAnalysisSchema, + actionableRecommendationsSchema, +} from "./schemas"; + +const logger = createScopedLogger("email-report-prompts"); + +export async function generateExecutiveSummary( + emailSummaries: EmailSummary[], + sentEmailSummaries: EmailSummary[], + gmailLabels: any[], + userEmail: string, + emailAccount: any, +): Promise> { + const system = `You are a professional persona identification expert. Your primary task is to accurately identify the user's professional role based on their email patterns. + +CRITICAL: The persona must be a specific, recognizable professional role that clearly identifies what this person does for work. + +Examples of GOOD personas: +- "Startup Founder" +- "Software Developer" +- "Real Estate Agent" +- "Marketing Manager" +- "Sales Executive" +- "Product Manager" +- "Consultant" +- "Teacher" +- "Lawyer" +- "Doctor" +- "Influencer" +- "Freelance Designer" + +Examples of BAD personas (too vague): +- "Professional" +- "Business Person" +- "Tech Worker" +- "Knowledge Worker" + +Focus on identifying the PRIMARY professional role based on email content, senders, and communication patterns.`; + + const prompt = `### Email Analysis Data + +**Received Emails (${emailSummaries.length} emails):** +${emailSummaries + .slice(0, 30) + .map( + (email, i) => + `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, + ) + .join("\n")} + +**Sent Emails (${sentEmailSummaries.length} emails):** +${sentEmailSummaries + .slice(0, 15) + .map( + (email, i) => + `${i + 1}. To: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, + ) + .join("\n")} + +**Current Gmail Labels:** +${gmailLabels.map((label) => `- ${label.name} (${label.messagesTotal || 0} emails)`).join("\n")} + +--- + +**PERSONA IDENTIFICATION INSTRUCTIONS:** + +Analyze the email patterns to identify the user's PRIMARY professional role: + +1. **Look for role indicators:** + - Who do they email? (clients, team members, investors, customers, etc.) + - What topics dominate? (code reviews, property listings, campaign metrics, etc.) + - What language/terminology is used? (technical terms, industry jargon, etc.) + - What responsibilities are evident? (managing teams, closing deals, creating content, etc.) + +2. **Common professional patterns:** + - **Founder/CEO**: Investor emails, team management, strategic decisions, fundraising + - **Developer**: Code reviews, technical discussions, GitHub notifications, deployment issues + - **Sales**: CRM notifications, client outreach, deal discussions, quota tracking + - **Marketing**: Campaign metrics, content creation, social media, analytics + - **Real Estate**: Property listings, client communications, MLS notifications + - **Consultant**: Client projects, proposals, expertise sharing, industry updates + - **Teacher**: Student communications, educational content, institutional emails + +3. **Confidence level:** + - 90-100%: Very clear indicators, consistent patterns + - 70-89%: Strong indicators, some ambiguity + - 50-69%: Mixed signals, multiple possible roles + - Below 50%: Unclear or insufficient data + +Generate: +1. **Specific professional persona** (1-3 words max, e.g., "Software Developer", "Real Estate Agent") +2. **Confidence level** based on clarity of evidence +3. **Top insights** about their email behavior +4. **Quick actions** for immediate improvement`; + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: executiveSummarySchema, + userEmail: userEmail, + usageLabel: "email-report-executive-summary", + }); + + return result.object; +} + +export async function buildUserPersona( + emailSummaries: EmailSummary[], + userEmail: string, + emailAccount: any, + sentEmailSummaries?: EmailSummary[], + gmailSignature?: string, + gmailTemplates?: string[], +): Promise> { + const system = `You are a highly skilled AI analyst tasked with generating a focused professional persona of a user based on their email activity. + +Analyze the email summaries, signatures, and templates to identify: +1. Professional identity with supporting evidence +2. Current professional priorities based on email content + +Focus on understanding the user's role and what they're currently focused on professionally.`; + + const prompt = `### Input Data + +**Received Email Summaries:** +${emailSummaries.map((summary, index) => `Email ${index + 1} Summary: ${summary.summary} (Category: ${summary.category})`).join("\n")} + +${ + sentEmailSummaries && sentEmailSummaries.length > 0 + ? ` +**Sent Email Summaries:** +${sentEmailSummaries.map((summary, index) => `Sent ${index + 1} Summary: ${summary.summary} (Category: ${summary.category})`).join("\n")} +` + : "" +} + +**User's Signature:** +${gmailSignature || "[No signature data available – analyze based on email content only]"} + +${ + gmailTemplates && gmailTemplates.length > 0 + ? ` +**User's Gmail Templates:** +${gmailTemplates.map((template, index) => `Template ${index + 1}: ${template}`).join("\n")} +` + : "" +} + +--- + +Analyze the data and identify: +1. **Professional Identity**: What is their role and what evidence supports this? +2. **Current Priorities**: What are they focused on professionally based on email content?`; + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: userPersonaSchema, + userEmail: userEmail, + usageLabel: "email-report-user-persona", + }); + + return result.object; +} + +export async function analyzeEmailBehavior( + emailSummaries: EmailSummary[], + userEmail: string, + emailAccount: any, + sentEmailSummaries?: EmailSummary[], +): Promise> { + const system = `You are an expert AI system that analyzes a user's email behavior to infer timing patterns, content preferences, and automation opportunities. + +Focus on identifying patterns that can be automated and providing specific, actionable automation rules that would save time and improve email management efficiency.`; + + const prompt = `### Email Analysis Data + +**Received Emails:** +${emailSummaries.map((email, i) => `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`).join("\n")} + +${ + sentEmailSummaries && sentEmailSummaries.length > 0 + ? ` +**Sent Emails:** +${sentEmailSummaries.map((email, i) => `${i + 1}. To: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`).join("\n")} +` + : "" +} + +--- + +Analyze the email patterns and identify: +1. Timing patterns (when emails are most active, response preferences) +2. Content preferences (what types of emails they engage with vs avoid) +3. Engagement triggers (what prompts them to take action) +4. Specific automation opportunities with estimated time savings`; + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: emailBehaviorSchema, + userEmail: userEmail, + usageLabel: "email-report-email-behavior", + }); + + return result.object; +} + +export async function analyzeResponsePatterns( + emailSummaries: EmailSummary[], + userEmail: string, + emailAccount: any, + sentEmailSummaries?: EmailSummary[], +): Promise> { + const system = `You are an expert email behavior analyst. Your task is to identify common response patterns and suggest email categorization and templates based on the user's email activity. + +Focus on practical, actionable insights for email management including reusable templates and smart categorization. + +IMPORTANT: When creating email categories, avoid meaningless or generic categories such as: +- "Other", "Unknown", "Unclear", "Miscellaneous" +- "Personal" (too generic and meaningless) +- "Unclear Content/HTML Code", "HTML Content", "Raw Content" +- "General", "Random", "Various" + +Only suggest categories that are meaningful and provide clear organizational value. If an email doesn't fit into a meaningful category, don't create a category for it.`; + + const prompt = `### Input Data + +**Received Email Summaries:** +${emailSummaries.map((summary, index) => `Email ${index + 1}: ${summary.summary} (Category: ${summary.category})`).join("\n")} + +${ + sentEmailSummaries && sentEmailSummaries.length > 0 + ? ` +**Sent Email Summaries (User's Response Patterns):** +${sentEmailSummaries.map((summary, index) => `Sent ${index + 1}: ${summary.summary} (Category: ${summary.category})`).join("\n")} +` + : "" +} + +--- + +Analyze the data and identify: +1. Common response patterns the user uses with examples and frequency +2. Suggested email templates that would save time +3. Email categorization strategy with volume estimates and priorities + +For email categorization, create simple, practical categories based on actual email content. Examples of good categories: +- "Work", "Finance", "Meetings", "Marketing", "Support", "Sales" +- "Projects", "Billing", "Team", "Clients", "Products", "Services" +- "Administrative", "Technical", "Legal", "HR", "Operations" + +Only suggest categories that are meaningful and provide clear organizational value. If emails don't fit into meaningful categories, don't create categories for them.`; + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: responsePatternsSchema, + userEmail: userEmail, + usageLabel: "email-report-response-patterns", + }); + + return result.object; +} + +export async function analyzeLabelOptimization( + emailSummaries: EmailSummary[], + userEmail: string, + emailAccount: any, + gmailLabels: any[], +): Promise> { + const system = `You are a Gmail organization expert. Analyze the user's current labels and email patterns to suggest specific optimizations that will improve their email organization and workflow efficiency. + +Focus on practical suggestions that will reduce email management time and improve organization.`; + + const prompt = `### Current Gmail Labels +${gmailLabels.map((label) => `- ${label.name}: ${label.messagesTotal || 0} emails, ${label.messagesUnread || 0} unread`).join("\n")} + +### Email Content Analysis +${emailSummaries + .slice(0, 30) + .map( + (email, i) => + `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, + ) + .join("\n")} + +--- + +Based on the current labels and email content, suggest specific optimizations: +1. Labels to create based on email patterns +2. Labels to consolidate that have overlapping purposes +3. Labels to rename for better clarity +4. Labels to delete that are unused or redundant + +Each suggestion should include the reason and expected impact.`; + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: labelAnalysisSchema, + userEmail: userEmail, + usageLabel: "email-report-label-analysis", + }); + + return result.object; +} + +export async function generateActionableRecommendations( + emailSummaries: EmailSummary[], + userEmail: string, + emailAccount: any, + userPersona: z.infer, + emailBehavior: z.infer, +): Promise> { + const system = `You are an email productivity consultant. Based on the comprehensive email analysis, create specific, actionable recommendations that the user can implement to improve their email workflow. + +Organize recommendations by timeline (immediate, short-term, long-term) and include specific implementation details and expected benefits.`; + + const prompt = `### Analysis Summary + +**User Persona:** ${userPersona.professionalIdentity.persona} +**Current Priorities:** ${userPersona.currentPriorities.join(", ")} +**Email Volume:** ${emailSummaries.length} emails analyzed + +--- + +Create actionable recommendations in three categories: +1. **Immediate Actions** (can be done today): 4-6 specific actions with time requirements +2. **Short-term Improvements** (this week): 3-4 improvements with timelines and benefits +3. **Long-term Strategy** (ongoing): 2-3 strategic initiatives with success metrics + +Focus on practical, implementable solutions that improve email organization and workflow efficiency.`; + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: actionableRecommendationsSchema, + userEmail: userEmail, + usageLabel: "email-report-actionable-recommendations", + }); + + return result.object; +} + +/** + * Summarize emails for analysis + */ +export async function summarizeEmails( + emails: any[], + userEmail: string, + emailAccount: any, +): Promise { + logger.info("summarizeEmails started", { + emailsCount: emails.length, + userEmail, + hasEmailAccount: !!emailAccount, + }); + + if (emails.length === 0) { + logger.info( + "summarizeEmails: no emails to summarize, returning empty array", + ); + return []; + } + + if (!emailAccount) { + logger.warn("Email account not found for summarization", { userEmail }); + return []; + } + + const batchSize = 15; + const results: EmailSummary[] = []; + + for (let i = 0; i < emails.length; i += batchSize) { + const batch = emails.slice(i, i + batchSize); + const batchNumber = Math.floor(i / batchSize) + 1; + const totalBatches = Math.ceil(emails.length / batchSize); + + logger.info("summarizeEmails: processing batch", { + batchNumber, + totalBatches, + batchSize: batch.length, + startIndex: i, + endIndex: Math.min(i + batchSize, emails.length), + }); + + const batchResults = await processEmailBatch( + batch, + userEmail, + emailAccount, + batchNumber, + totalBatches, + ); + results.push(...batchResults); + + if (i + batchSize < emails.length) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } + + logger.info("summarizeEmails: all batches completed", { + totalResults: results.length, + originalEmailCount: emails.length, + }); + + return results; +} + +async function processEmailBatch( + emails: any[], + userEmail: string, + emailAccount: any, + batchNumber: number, + totalBatches: number, +): Promise { + logger.info("processEmailBatch: preparing email texts", { + batchNumber, + totalBatches, + emailsCount: emails.length, + }); + + const emailTexts = emails.map((email, index) => { + const sender = email.headers?.from || "Unknown"; + const subject = email.headers?.subject || "No subject"; + const content = email.textPlain || email.textHtml || ""; + + logger.info("processEmailBatch: processing email", { + batchNumber, + index, + emailId: email.id, + sender, + subjectLength: subject.length, + contentLength: content.length, + }); + + return `From: ${sender}\nSubject: ${subject}\nContent: ${content.substring(0, 1000)}`; + }); + + logger.info("processEmailBatch: email texts prepared", { + batchNumber, + emailTextsCount: emailTexts.length, + totalTextLength: emailTexts.join("\n\n---\n\n").length, + }); + + const system = `You are an assistant that processes user emails to extract their core meaning for later analysis. + +For each email, write a **factual summary of 3–5 sentences** that clearly describes: +- The main topic or purpose of the email +- What the sender wants, requests, or informs +- Any relevant secondary detail (e.g., urgency, timing, sender role, or context) +- Optional: mention tools, platforms, or projects if they help clarify the email's purpose + +**Important Rules:** +- Be objective. Do **not** speculate, interpret intent, or invent details. +- Summarize only what is in the actual content of the email. +- Use professional and concise language. +- **Include** marketing/newsletter emails **only if** they reflect the user's professional interests (e.g., product updates, industry news, job boards). +- **Skip** irrelevant promotions, spam, or generic sales offers (e.g., holiday deals, coupon codes).`; + + const prompt = ` +**Input Emails (Batch ${batchNumber} of ${totalBatches}):** + +${emailTexts.join("\n\n---\n\n")} + +Return the analysis as a JSON array with objects containing: summary, sender, subject, category.`; + + logger.info("processEmailBatch: about to call chatCompletionObject", { + batchNumber, + promptLength: prompt.length, + systemLength: system.length, + userEmail, + }); + + try { + logger.info("processEmailBatch: calling chatCompletionObject", { + batchNumber, + userAiProvider: emailAccount.user?.aiProvider, + userAiModel: emailAccount.user?.aiModel, + hasUserAiApiKey: !!emailAccount.user?.aiApiKey, + }); + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: z.array( + z.object({ + summary: z.string().describe("Brief summary of the email content"), + sender: z.string().describe("Email sender"), + subject: z.string().describe("Email subject"), + category: z + .string() + .describe( + "Category of the email (work, personal, marketing, etc.)", + ), + }), + ), + userEmail: userEmail, + usageLabel: "email-report-summary-generation", + }); + + logger.info("processEmailBatch: chatCompletionObject completed", { + batchNumber, + resultType: typeof result, + hasObject: !!result.object, + objectLength: result.object?.length || 0, + }); + + return result.object; + } catch (error) { + logger.error("processEmailBatch: failed to summarize batch", { + batchNumber, + error, + userEmail, + }); + return []; + } +} diff --git a/apps/web/app/(reports)/api/email-report/route.ts b/apps/web/app/(reports)/api/email-report/route.ts new file mode 100644 index 0000000000..794d7f10fc --- /dev/null +++ b/apps/web/app/(reports)/api/email-report/route.ts @@ -0,0 +1,436 @@ +import { NextResponse } from "next/server"; +import { withEmailAccount } from "@/utils/middleware"; +import { createScopedLogger } from "@/utils/logger"; +import { fetchEmailsForReport, fetchGmailTemplates } from "./fetch"; +import type { ParsedMessage } from "@/utils/types"; +import prisma from "@/utils/prisma"; +import { getGmailClientWithRefresh } from "@/utils/gmail/client"; + +import { + generateExecutiveSummary, + buildUserPersona, + analyzeEmailBehavior, + analyzeResponsePatterns, + analyzeLabelOptimization, + generateActionableRecommendations, + summarizeEmails, +} from "./prompts"; +import type { EmailSummary } from "./schemas"; +import type { gmail_v1 } from "@googleapis/gmail"; + +const logger = createScopedLogger("email-report-api"); + +export const GET = withEmailAccount(async (request) => { + const { emailAccountId, email } = request.auth; + + try { + const result = await getEmailReportData({ + emailAccountId, + userEmail: email, + }); + return NextResponse.json(result); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + + logger.error("Error generating email report", { + error: errorMessage, + stack: errorStack, + emailAccountId, + }); + + return NextResponse.json( + { + error: "Failed to generate email report", + details: errorMessage, + emailAccountId, + }, + { status: 500 }, + ); + } +}); + +async function fetchGmailLabels(gmail: any): Promise { + try { + const response = await gmail.users.labels.list({ userId: "me" }); + + const userLabels = + response.data.labels?.filter( + (label: any) => + label.type === "user" && + !label.name.startsWith("CATEGORY_") && + !label.name.startsWith("CHAT"), + ) || []; + + const labelsWithCounts = await Promise.all( + userLabels.map(async (label: any) => { + try { + const labelDetail = await gmail.users.labels.get({ + userId: "me", + id: label.id, + }); + return { + ...label, + messagesTotal: labelDetail.data.messagesTotal || 0, + messagesUnread: labelDetail.data.messagesUnread || 0, + threadsTotal: labelDetail.data.threadsTotal || 0, + threadsUnread: labelDetail.data.threadsUnread || 0, + }; + } catch (error) { + logger.warn(`Failed to get details for label ${label.name}:`, { + error: error instanceof Error ? error.message : String(error), + }); + return { + ...label, + messagesTotal: 0, + messagesUnread: 0, + threadsTotal: 0, + threadsUnread: 0, + }; + } + }), + ); + + const sortedLabels = labelsWithCounts.sort( + (a: any, b: any) => (b.messagesTotal || 0) - (a.messagesTotal || 0), + ); + + return sortedLabels; + } catch (error) { + logger.warn("Failed to fetch Gmail labels:", { + error: error instanceof Error ? error.message : String(error), + }); + return []; + } +} + +async function fetchGmailSignature(gmail: gmail_v1.Gmail): Promise { + try { + const sendAsList = await gmail.users.settings.sendAs.list({ + userId: "me", + }); + + if (!sendAsList.data.sendAs || sendAsList.data.sendAs.length === 0) { + logger.warn("No sendAs settings found"); + return ""; + } + + const primarySendAs = sendAsList.data.sendAs[0]; + if (!primarySendAs.sendAsEmail) { + logger.warn("No primary sendAs email found"); + return ""; + } + + const signatureResponse = await gmail.users.settings.sendAs.get({ + userId: "me", + sendAsEmail: primarySendAs.sendAsEmail, + }); + + const signature = signatureResponse.data.signature; + logger.info("Gmail signature fetched successfully", { + hasSignature: !!signature, + sendAsEmail: primarySendAs.sendAsEmail, + }); + + return signature || ""; + } catch (error) { + logger.warn("Failed to fetch Gmail signature:", { + error: error instanceof Error ? error.message : String(error), + }); + return ""; + } +} + +async function getEmailReportData({ + emailAccountId, + userEmail, +}: { + emailAccountId: string; + userEmail: string; +}) { + logger.info("getEmailReportData started", { + emailAccountId, + userEmail, + }); + let emailData: { + receivedEmails: ParsedMessage[]; + sentEmails: ParsedMessage[]; + totalReceived: number; + totalSent: number; + } | null = null; + + let emailAccount: any; + try { + emailAccount = await prisma.emailAccount.findFirst({ + where: { user: { email: userEmail } }, + include: { + account: true, + user: { + select: { + aiProvider: true, + aiModel: true, + aiApiKey: true, + }, + }, + }, + }); + + if (!emailAccount) { + throw new Error("Email account not found"); + } + } catch (error) { + logger.error("Failed to fetch email account", { + error: error instanceof Error ? error.message : String(error), + userEmail, + emailAccountId, + }); + throw new Error( + `Failed to fetch email account: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + try { + logger.info("getEmailReportData: about to call fetchEmailsForReport", { + emailAccountId: emailAccount.id, + userEmail: emailAccount.user?.email, + }); + + emailData = await fetchEmailsForReport({ + emailAccount, + }); + + logger.info( + "getEmailReportData: fetchEmailsForReport completed successfully", + { + receivedCount: emailData.totalReceived, + sentCount: emailData.totalSent, + }, + ); + } catch (error) { + logger.error("Failed to fetch emails for report", { + error: error instanceof Error ? error.message : String(error), + emailAccountId, + userEmail, + }); + throw new Error( + `Failed to fetch emails: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + const { receivedEmails, sentEmails, totalReceived, totalSent } = emailData; + + let receivedEmailSummaries: EmailSummary[] = []; + let sentEmailSummaries: EmailSummary[] = []; + + try { + logger.info("Starting email summarization", { + receivedEmailsCount: receivedEmails.length, + sentEmailsCount: sentEmails.length, + }); + + logger.info("About to call summarizeEmails for received emails", { + receivedEmailsCount: receivedEmails.length, + firstEmailId: receivedEmails[0]?.id, + lastEmailId: receivedEmails[receivedEmails.length - 1]?.id, + }); + + const receivedSummaries = await summarizeEmails( + receivedEmails, + userEmail, + emailAccount, + ); + + logger.info("Received email summaries completed", { + summariesCount: receivedSummaries.length, + }); + + logger.info("About to call summarizeEmails for sent emails", { + sentEmailsCount: sentEmails.length, + firstEmailId: sentEmails[0]?.id, + lastEmailId: sentEmails[sentEmails.length - 1]?.id, + }); + + const sentSummaries = await summarizeEmails( + sentEmails, + userEmail, + emailAccount, + ); + + logger.info("Sent email summaries completed", { + summariesCount: sentSummaries.length, + }); + + receivedEmailSummaries = receivedSummaries; + sentEmailSummaries = sentSummaries; + } catch (error) { + logger.error("Failed to generate email summaries", { + error: error instanceof Error ? error.message : String(error), + errorType: error instanceof Error ? error.constructor.name : typeof error, + errorStack: error instanceof Error ? error.stack : undefined, + emailAccountId, + userEmail, + }); + throw new Error( + `Failed to generate email summaries: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + if ( + !emailAccount?.account?.access_token || + !emailAccount?.account?.refresh_token + ) { + throw new Error("Missing Gmail tokens"); + } + + const gmail = await getGmailClientWithRefresh({ + accessToken: emailAccount.account.access_token, + refreshToken: emailAccount.account.refresh_token, + expiresAt: emailAccount.account.expires_at, + emailAccountId: emailAccount.id, + }); + + const gmailLabels = await fetchGmailLabels(gmail); + const gmailSignature = await fetchGmailSignature(gmail); + const gmailTemplates = await fetchGmailTemplates(gmail); + + let executiveSummary: any, + userPersona: any, + emailBehavior: any, + responsePatterns: any, + labelAnalysis: any, + actionableRecommendations: any; + + try { + executiveSummary = await generateExecutiveSummary( + receivedEmailSummaries, + sentEmailSummaries, + gmailLabels, + userEmail, + emailAccount, + ); + } catch (error) { + logger.error("Failed to generate executive summary", { + error: error instanceof Error ? error.message : String(error), + emailAccountId, + }); + throw new Error( + `Failed to generate executive summary: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + try { + userPersona = await buildUserPersona( + receivedEmailSummaries, + userEmail, + emailAccount, + sentEmailSummaries, + gmailSignature, + gmailTemplates, + ); + } catch (error) { + logger.error("Failed to build user persona", { + error: error instanceof Error ? error.message : String(error), + emailAccountId, + }); + throw new Error( + `Failed to build user persona: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + try { + emailBehavior = await analyzeEmailBehavior( + receivedEmailSummaries, + userEmail, + emailAccount, + sentEmailSummaries, + ); + } catch (error) { + logger.error("Failed to analyze email behavior", { + error: error instanceof Error ? error.message : String(error), + emailAccountId, + }); + throw new Error( + `Failed to analyze email behavior: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + try { + responsePatterns = await analyzeResponsePatterns( + receivedEmailSummaries, + userEmail, + emailAccount, + sentEmailSummaries, + ); + } catch (error) { + logger.error("Failed to analyze response patterns", { + error: error instanceof Error ? error.message : String(error), + emailAccountId, + }); + throw new Error( + `Failed to analyze response patterns: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + try { + labelAnalysis = await analyzeLabelOptimization( + receivedEmailSummaries, + userEmail, + emailAccount, + gmailLabels, + ); + } catch (error) { + logger.error("Failed to analyze label optimization", { + error: error instanceof Error ? error.message : String(error), + emailAccountId, + }); + throw new Error( + `Failed to analyze label optimization: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + try { + actionableRecommendations = await generateActionableRecommendations( + receivedEmailSummaries, + userEmail, + emailAccount, + userPersona, + emailBehavior, + ); + } catch (error) { + logger.error("Failed to generate actionable recommendations", { + error: error instanceof Error ? error.message : String(error), + emailAccountId, + }); + throw new Error( + `Failed to generate actionable recommendations: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + return { + executiveSummary, + emailActivityOverview: { + dataSources: { + inbox: totalReceived, + archived: 0, + trash: 0, + sent: totalSent, + }, + }, + userPersona, + emailBehavior, + responsePatterns, + labelAnalysis: { + currentLabels: gmailLabels.map((label) => ({ + name: label.name, + emailCount: label.messagesTotal || 0, + unreadCount: label.messagesUnread || 0, + threadCount: label.threadsTotal || 0, + unreadThreads: label.threadsUnread || 0, + color: label.color || null, + type: label.type, + })), + optimizationSuggestions: labelAnalysis.optimizationSuggestions, + }, + actionableRecommendations, + }; +} diff --git a/apps/web/app/(reports)/api/email-report/schemas.ts b/apps/web/app/(reports)/api/email-report/schemas.ts new file mode 100644 index 0000000000..02fec639db --- /dev/null +++ b/apps/web/app/(reports)/api/email-report/schemas.ts @@ -0,0 +1,160 @@ +import { z } from "zod"; + +export const emailSummarySchema = z.object({ + summary: z.string().describe("Brief summary of the email content"), + sender: z.string().describe("Email sender"), + subject: z.string().describe("Email subject"), + category: z + .string() + .describe("Category of the email (work, personal, marketing, etc.)"), +}); + +export const executiveSummarySchema = z.object({ + userProfile: z.object({ + persona: z + .string() + .describe( + "1-5 word persona identification (e.g., 'Tech Startup Founder')", + ), + confidence: z + .number() + .min(0) + .max(100) + .describe("Confidence level in persona identification (0-100)"), + }), + topInsights: z + .array( + z.object({ + insight: z.string().describe("Key insight about user's email behavior"), + priority: z + .enum(["high", "medium", "low"]) + .describe("Priority level of this insight"), + icon: z.string().describe("Single emoji representing this insight"), + }), + ) + .describe("3-5 most important findings from the analysis"), + quickActions: z + .array( + z.object({ + action: z + .string() + .describe("Specific action the user can take immediately"), + difficulty: z + .enum(["easy", "medium", "hard"]) + .describe("How difficult this action is to implement"), + impact: z + .enum(["high", "medium", "low"]) + .describe("Expected impact of this action"), + }), + ) + .describe("4-6 immediate actions the user can take"), +}); + +export const userPersonaSchema = z.object({ + professionalIdentity: z.object({ + persona: z.string().describe("Professional persona identification"), + supportingEvidence: z + .array(z.string()) + .describe("Evidence supporting this persona identification"), + }), + currentPriorities: z + .array(z.string()) + .describe("Current professional priorities based on email content"), +}); + +export const emailBehaviorSchema = z.object({ + timingPatterns: z.object({ + peakHours: z.array(z.string()).describe("Peak email activity hours"), + responsePreference: z.string().describe("Preferred response timing"), + frequency: z.string().describe("Overall email frequency"), + }), + contentPreferences: z.object({ + preferred: z + .array(z.string()) + .describe("Types of emails user engages with"), + avoided: z + .array(z.string()) + .describe("Types of emails user typically ignores"), + }), + engagementTriggers: z + .array(z.string()) + .describe("What prompts user to take action on emails"), +}); + +export const responsePatternsSchema = z.object({ + commonResponses: z.array( + z.object({ + pattern: z.string().describe("Description of the response pattern"), + example: z.string().describe("Example of this type of response"), + frequency: z + .number() + .describe("Percentage of responses using this pattern"), + triggers: z + .array(z.string()) + .describe("What types of emails trigger this response"), + }), + ), + suggestedTemplates: z.array( + z.object({ + templateName: z.string().describe("Name of the email template"), + template: z.string().describe("The actual email template text"), + useCase: z.string().describe("When to use this template"), + }), + ), + categoryOrganization: z.array( + z.object({ + category: z.string().describe("Email category name"), + description: z + .string() + .describe("What types of emails belong in this category"), + emailCount: z + .number() + .describe("Estimated number of emails in this category"), + priority: z + .enum(["high", "medium", "low"]) + .describe("Priority level for this category"), + }), + ), +}); + +export const labelAnalysisSchema = z.object({ + optimizationSuggestions: z.array( + z.object({ + type: z + .enum(["create", "consolidate", "rename", "delete"]) + .describe("Type of optimization"), + suggestion: z.string().describe("Specific suggestion"), + reason: z.string().describe("Reason for this suggestion"), + impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), + }), + ), +}); + +export const actionableRecommendationsSchema = z.object({ + immediateActions: z.array( + z.object({ + action: z.string().describe("Specific action to take"), + difficulty: z + .enum(["easy", "medium", "hard"]) + .describe("Implementation difficulty"), + impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), + timeRequired: z.string().describe("Time required (e.g., '5 minutes')"), + }), + ), + shortTermImprovements: z.array( + z.object({ + improvement: z.string().describe("Improvement to implement"), + timeline: z.string().describe("When to implement (e.g., 'This week')"), + expectedBenefit: z.string().describe("Expected benefit"), + }), + ), + longTermStrategy: z.array( + z.object({ + strategy: z.string().describe("Strategic initiative"), + description: z.string().describe("Detailed description"), + successMetrics: z.array(z.string()).describe("How to measure success"), + }), + ), +}); + +export type EmailSummary = z.infer; diff --git a/apps/web/app/(reports)/api/email-summaries/route.ts b/apps/web/app/(reports)/api/email-summaries/route.ts deleted file mode 100644 index 001259902a..0000000000 --- a/apps/web/app/(reports)/api/email-summaries/route.ts +++ /dev/null @@ -1,351 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; -import { getGmailClientWithRefresh } from "@/utils/gmail/client"; -import { getMessage, getMessages } from "@/utils/gmail/message"; -import { parseMessage } from "@/utils/mail"; -import { chatCompletionObject } from "@/utils/llms"; -import { redis } from "@/utils/redis"; -import { env } from "@/env"; -import prisma from "@/utils/prisma"; - -const EmailSummarySchema = z.object({ - summary: z.string().describe("Brief summary of the email content"), - sender: z.string().describe("Email sender"), - subject: z.string().describe("Email subject"), - category: z - .string() - .describe("Category of the email (work, personal, marketing, etc.)"), -}); - -type EmailSummary = z.infer; - -const SummarizeRequestSchema = z.object({ - userEmail: z.string().email(), - query: z.string().optional().default(""), // Gmail query (e.g., "from:me", "label:work") - count: z.number().min(1).max(200).optional().default(50), - forceRefresh: z.boolean().optional().default(false), - batchSize: z.number().min(10).max(50).optional().default(20), -}); - -const SummarizeResponseSchema = z.object({ - summaries: z.array(EmailSummarySchema), - totalEmails: z.number(), - cached: z.boolean(), - query: z.string(), - processingTime: z.number(), -}); - -const BATCH_SUMMARY_EXPIRATION = 60 * 60 * 24; - -/** - * Generate cache key for email batch - */ -function getBatchSummaryKey(query: string, count: number): string { - return `sandbox:email-summary:${query}:${count}`; -} - -/** - * Get cached batch summary from Redis - */ -async function getBatchSummary( - query: string, - count: number, -): Promise { - const key = getBatchSummaryKey(query, count); - return redis.get(key); -} - -/** - * Save batch summary to Redis cache - */ -async function saveBatchSummary( - summaries: EmailSummary[], - query: string, - count: number, -): Promise { - const key = getBatchSummaryKey(query, count); - await redis.set(key, summaries, { ex: BATCH_SUMMARY_EXPIRATION }); -} - -/** - * Fetch emails from Gmail based on query - */ -async function fetchEmailsByQuery( - gmail: any, - query: string, - count: number, -): Promise { - const emails: any[] = []; - let nextPageToken: string | undefined; - - while (emails.length < count) { - const response = await getMessages(gmail, { - query: query || undefined, - maxResults: Math.min(100, count - emails.length), - pageToken: nextPageToken, - }); - - if (!response.messages || response.messages.length === 0) { - break; - } - - // Get full message details for each email - const messagePromises = response.messages.map(async (message: any) => { - if (!message.id) return null; - try { - const messageWithPayload = await getMessage(message.id, gmail, "full"); - return parseMessage(messageWithPayload); - } catch (error) { - console.warn(`Failed to fetch message ${message.id}:`, error); - return null; - } - }); - - const messages = await Promise.all(messagePromises); - const validMessages = messages.filter((msg) => msg !== null); - - emails.push(...validMessages); - - nextPageToken = response.nextPageToken || undefined; - if (!nextPageToken) { - break; - } - } - - return emails; -} - -/** - * Summarize a batch of emails using AI - */ -async function summarizeEmailBatch( - emails: any[], - query: string, - forceRefresh = false, -): Promise { - // Check cache first (unless force refresh is requested) - if (!forceRefresh) { - const cachedSummaries = await getBatchSummary(query, emails.length); - if (cachedSummaries) { - return cachedSummaries; - } - } - - const emailTexts = emails.map((email) => { - const sender = email.headers?.from || "Unknown"; - const subject = email.headers?.subject || "No subject"; - const content = email.textPlain || email.textHtml || ""; - - return `From: ${sender}\nSubject: ${subject}\nContent: ${content.substring(0, 1000)}`; - }); - - const system = `You are an assistant that processes user emails to extract their core meaning for later analysis. - -For each email, write a **factual summary of 3–5 sentences** that clearly describes: -- The main topic or purpose of the email -- What the sender wants, requests, or informs -- Any relevant secondary detail (e.g., urgency, timing, sender role, or context) -- Optional: mention tools, platforms, or projects if they help clarify the email's purpose - -**Important Rules:** -- Be objective. Do **not** speculate, interpret intent, or invent details. -- Summarize only what is in the actual content of the email. -- Use professional and concise language. -- **Include** marketing/newsletter emails **only if** they reflect the user's professional interests (e.g., product updates, industry news, job boards). -- **Skip** irrelevant promotions, spam, or generic sales offers (e.g., holiday deals, coupon codes). - ---- -`; - - const prompt = ` -**Input Emails:** - -${emailTexts.join("\n\n---\n\n")} - -Return the analysis as a JSON array with objects containing: summary, sender, subject, category.`; - - const result = await chatCompletionObject({ - userAi: { - aiProvider: env.DEFAULT_LLM_PROVIDER as any, - aiModel: env.DEFAULT_LLM_MODEL || "gemini-2.0-flash-exp", - aiApiKey: env.GOOGLE_API_KEY || null, - }, - system, - prompt, - schema: z.array(EmailSummarySchema), - userEmail: "sandbox@inboxzero.com", - usageLabel: "sandbox-email-summary-generation", - }); - - const summaries = result.object; - - // Cache the results - await saveBatchSummary(summaries, query, emails.length); - - return summaries; -} - -/** - * Process emails in batches and summarize - */ -async function processEmailBatches( - emails: any[], - query: string, - batchSize: number, - forceRefresh: boolean, -): Promise { - const allSummaries: EmailSummary[] = []; - - for (let i = 0; i < emails.length; i += batchSize) { - const batch = emails.slice(i, i + batchSize); - const batchSummaries = await summarizeEmailBatch( - batch, - query, - forceRefresh, - ); - allSummaries.push(...batchSummaries); - } - - return allSummaries; -} - -export async function POST(request: NextRequest) { - const startTime = Date.now(); - const requestId = Math.random().toString(36).substring(7); - - console.log(`[${requestId}] Starting email summarization: ${request.url}`); - - try { - // Parse and validate request - const body = await request.json(); - const { userEmail, query, count, forceRefresh, batchSize } = - SummarizeRequestSchema.parse(body); - - console.log(`[${requestId}] Processing request:`, { - userEmail, - query, - count, - forceRefresh, - batchSize, - }); - - // Fetch user's email account - const emailAccount = await prisma.emailAccount.findFirst({ - where: { user: { email: userEmail } }, - include: { account: true }, - }); - - if (!emailAccount) { - console.log( - `[${requestId}] Error: Email account not found for ${userEmail}`, - ); - return NextResponse.json( - { error: "Email account not found" }, - { status: 404 }, - ); - } - - console.log(`[${requestId}] Found email account: ${emailAccount.email}`); - - // Get Gmail client - console.log(`[${requestId}] Initializing Gmail client...`); - const gmail = await getGmailClientWithRefresh({ - accessToken: emailAccount.account?.access_token!, - refreshToken: emailAccount.account?.refresh_token!, - expiresAt: emailAccount.account?.expires_at, - emailAccountId: emailAccount.id, - }); - console.log(`[${requestId}] Gmail client initialized successfully`); - - // Fetch emails based on query - console.log( - `[${requestId}] Fetching emails with query: "${query}", count: ${count}`, - ); - const emails = await fetchEmailsByQuery(gmail, query, count); - console.log(`[${requestId}] Fetched ${emails.length} emails`); - - if (emails.length === 0) { - console.log(`[${requestId}] No emails found for query: "${query}"`); - return NextResponse.json({ - summaries: [], - totalEmails: 0, - cached: false, - query, - processingTime: Date.now() - startTime, - }); - } - - // Check if we can use cached results - let cached = false; - if (!forceRefresh) { - console.log(`[${requestId}] Checking cache for query: "${query}"`); - const cachedSummaries = await getBatchSummary(query, emails.length); - if (cachedSummaries) { - cached = true; - console.log(`[${requestId}] Using cached results`); - } else { - console.log(`[${requestId}] No cache found, processing emails`); - } - } else { - console.log(`[${requestId}] Force refresh requested, ignoring cache`); - } - - // Process emails in batches - console.log( - `[${requestId}] Processing ${emails.length} emails in batches of ${batchSize}`, - ); - const summaries = await processEmailBatches( - emails, - query, - batchSize, - forceRefresh, - ); - console.log(`[${requestId}] Generated ${summaries.length} summaries`); - - const processingTime = Date.now() - startTime; - console.log( - `[${requestId}] Email summarization completed in ${processingTime}ms`, - ); - - return NextResponse.json({ - summaries, - totalEmails: emails.length, - cached, - query, - processingTime, - }); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - const errorStack = error instanceof Error ? error.stack : undefined; - - console.error(`[${requestId}] Sandbox email summarization error:`, { - error: errorMessage, - stack: errorStack, - processingTime: Date.now() - startTime, - url: request.url, - }); - - if (error instanceof z.ZodError) { - return NextResponse.json( - { - error: "Invalid request data", - details: error.errors, - requestId, - processingTime: Date.now() - startTime, - }, - { status: 400 }, - ); - } - - return NextResponse.json( - { - error: "Failed to summarize emails", - details: errorMessage, - requestId, - processingTime: Date.now() - startTime, - }, - { status: 500 }, - ); - } -} diff --git a/apps/web/app/(reports)/api/gmail-labels/route.ts b/apps/web/app/(reports)/api/gmail-labels/route.ts deleted file mode 100644 index 3b722acac7..0000000000 --- a/apps/web/app/(reports)/api/gmail-labels/route.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { type NextRequest, NextResponse } from "next/server"; -import { getGmailClientWithRefresh } from "@/utils/gmail/client"; -import prisma from "@/utils/prisma"; - -export async function POST(request: NextRequest) { - try { - const { userEmail } = await request.json(); - - if (!userEmail) { - return NextResponse.json( - { error: "Email address is required" }, - { status: 400 }, - ); - } - - const emailAccount = await prisma.emailAccount.findFirst({ - where: { user: { email: userEmail } }, - include: { account: true }, - }); - - if (!emailAccount) { - return NextResponse.json( - { error: "Email account not found" }, - { status: 404 }, - ); - } - - const gmail = await getGmailClientWithRefresh({ - accessToken: emailAccount.account.access_token!, - refreshToken: emailAccount.account.refresh_token!, - expiresAt: emailAccount.account.expires_at, - emailAccountId: emailAccount.id, - }); - - const response = await gmail.users.labels.list({ userId: "me" }); - - const userLabels = - response.data.labels?.filter( - (label: any) => - label.type === "user" && - !label.name.startsWith("CATEGORY_") && - !label.name.startsWith("CHAT"), - ) || []; - - const labelsWithCounts = await Promise.all( - userLabels.map(async (label: any) => { - try { - const labelDetail = await gmail.users.labels.get({ - userId: "me", - id: label.id, - }); - return { - ...label, - messagesTotal: labelDetail.data.messagesTotal || 0, - messagesUnread: labelDetail.data.messagesUnread || 0, - threadsTotal: labelDetail.data.threadsTotal || 0, - threadsUnread: labelDetail.data.threadsUnread || 0, - }; - } catch (error) { - console.warn(`Failed to get details for label ${label.name}:`, error); - return { - ...label, - messagesTotal: 0, - messagesUnread: 0, - threadsTotal: 0, - threadsUnread: 0, - }; - } - }), - ); - - const sortedLabels = labelsWithCounts.sort( - (a: any, b: any) => (b.messagesTotal || 0) - (a.messagesTotal || 0), - ); - - return NextResponse.json({ - labels: sortedLabels.map((label: any) => ({ - id: label.id, - name: label.name, - messagesTotal: label.messagesTotal || 0, - messagesUnread: label.messagesUnread || 0, - color: label.color || null, - type: label.type, - })), - totalLabels: sortedLabels.length, - }); - } catch (error) { - console.error("Gmail labels fetch error:", error); - return NextResponse.json( - { error: "Failed to fetch Gmail labels" }, - { status: 500 }, - ); - } -} From 7eed5dd01ada365944d858fe2751237385a137fc Mon Sep 17 00:00:00 2001 From: Eduardo Lelis Date: Fri, 25 Jul 2025 11:59:21 -0300 Subject: [PATCH 04/20] Improve typing --- .../app/(reports)/api/email-report/fetch.ts | 6 +- .../app/(reports)/api/email-report/prompts.ts | 51 +++++++++--- .../app/(reports)/api/email-report/route.ts | 77 +++++++++++-------- 3 files changed, 91 insertions(+), 43 deletions(-) diff --git a/apps/web/app/(reports)/api/email-report/fetch.ts b/apps/web/app/(reports)/api/email-report/fetch.ts index 44b6cef59e..a1836fce01 100644 --- a/apps/web/app/(reports)/api/email-report/fetch.ts +++ b/apps/web/app/(reports)/api/email-report/fetch.ts @@ -4,6 +4,7 @@ import { getMessages, getMessage } from "@/utils/gmail/message"; import { parseMessage } from "@/utils/mail"; import { createScopedLogger } from "@/utils/logger"; import type { ParsedMessage } from "@/utils/types"; +import type { EmailAccount, User, Account } from "@prisma/client"; const logger = createScopedLogger("email-report-fetch"); @@ -213,7 +214,10 @@ export interface EmailFetchResult { export async function fetchEmailsForReport({ emailAccount, }: { - emailAccount: any; + emailAccount: EmailAccount & { + account: Account; + user: Pick; + }; }): Promise { logger.info("fetchEmailsForReport started", { emailAccountId: emailAccount?.id, diff --git a/apps/web/app/(reports)/api/email-report/prompts.ts b/apps/web/app/(reports)/api/email-report/prompts.ts index 1c798fd989..7e3419a37b 100644 --- a/apps/web/app/(reports)/api/email-report/prompts.ts +++ b/apps/web/app/(reports)/api/email-report/prompts.ts @@ -2,6 +2,9 @@ import { z } from "zod"; import { createScopedLogger } from "@/utils/logger"; import { chatCompletionObject } from "@/utils/llms"; import type { EmailSummary } from "./schemas"; +import type { EmailAccount, User, Account } from "@prisma/client"; +import type { gmail_v1 } from "@googleapis/gmail"; +import type { ParsedMessage } from "@/utils/types"; import { executiveSummarySchema, userPersonaSchema, @@ -16,9 +19,12 @@ const logger = createScopedLogger("email-report-prompts"); export async function generateExecutiveSummary( emailSummaries: EmailSummary[], sentEmailSummaries: EmailSummary[], - gmailLabels: any[], + gmailLabels: gmail_v1.Schema$Label[], userEmail: string, - emailAccount: any, + emailAccount: EmailAccount & { + account: Account; + user: Pick; + }, ): Promise> { const system = `You are a professional persona identification expert. Your primary task is to accurately identify the user's professional role based on their email patterns. @@ -117,7 +123,10 @@ Generate: export async function buildUserPersona( emailSummaries: EmailSummary[], userEmail: string, - emailAccount: any, + emailAccount: EmailAccount & { + account: Account; + user: Pick; + }, sentEmailSummaries?: EmailSummary[], gmailSignature?: string, gmailTemplates?: string[], @@ -177,7 +186,10 @@ Analyze the data and identify: export async function analyzeEmailBehavior( emailSummaries: EmailSummary[], userEmail: string, - emailAccount: any, + emailAccount: EmailAccount & { + account: Account; + user: Pick; + }, sentEmailSummaries?: EmailSummary[], ): Promise> { const system = `You are an expert AI system that analyzes a user's email behavior to infer timing patterns, content preferences, and automation opportunities. @@ -221,7 +233,10 @@ Analyze the email patterns and identify: export async function analyzeResponsePatterns( emailSummaries: EmailSummary[], userEmail: string, - emailAccount: any, + emailAccount: EmailAccount & { + account: Account; + user: Pick; + }, sentEmailSummaries?: EmailSummary[], ): Promise> { const system = `You are an expert email behavior analyst. Your task is to identify common response patterns and suggest email categorization and templates based on the user's email activity. @@ -279,8 +294,11 @@ Only suggest categories that are meaningful and provide clear organizational val export async function analyzeLabelOptimization( emailSummaries: EmailSummary[], userEmail: string, - emailAccount: any, - gmailLabels: any[], + emailAccount: EmailAccount & { + account: Account; + user: Pick; + }, + gmailLabels: gmail_v1.Schema$Label[], ): Promise> { const system = `You are a Gmail organization expert. Analyze the user's current labels and email patterns to suggest specific optimizations that will improve their email organization and workflow efficiency. @@ -323,7 +341,10 @@ Each suggestion should include the reason and expected impact.`; export async function generateActionableRecommendations( emailSummaries: EmailSummary[], userEmail: string, - emailAccount: any, + emailAccount: EmailAccount & { + account: Account; + user: Pick; + }, userPersona: z.infer, emailBehavior: z.infer, ): Promise> { @@ -362,9 +383,12 @@ Focus on practical, implementable solutions that improve email organization and * Summarize emails for analysis */ export async function summarizeEmails( - emails: any[], + emails: ParsedMessage[], userEmail: string, - emailAccount: any, + emailAccount: EmailAccount & { + account: Account; + user: Pick; + }, ): Promise { logger.info("summarizeEmails started", { emailsCount: emails.length, @@ -423,9 +447,12 @@ export async function summarizeEmails( } async function processEmailBatch( - emails: any[], + emails: ParsedMessage[], userEmail: string, - emailAccount: any, + emailAccount: EmailAccount & { + account: Account; + user: Pick; + }, batchNumber: number, totalBatches: number, ): Promise { diff --git a/apps/web/app/(reports)/api/email-report/route.ts b/apps/web/app/(reports)/api/email-report/route.ts index 794d7f10fc..34911b1469 100644 --- a/apps/web/app/(reports)/api/email-report/route.ts +++ b/apps/web/app/(reports)/api/email-report/route.ts @@ -17,6 +17,7 @@ import { } from "./prompts"; import type { EmailSummary } from "./schemas"; import type { gmail_v1 } from "@googleapis/gmail"; +import type { EmailAccount, User, Account } from "@prisma/client"; const logger = createScopedLogger("email-report-api"); @@ -50,49 +51,59 @@ export const GET = withEmailAccount(async (request) => { } }); -async function fetchGmailLabels(gmail: any): Promise { +async function fetchGmailLabels( + gmail: gmail_v1.Gmail, +): Promise { try { const response = await gmail.users.labels.list({ userId: "me" }); const userLabels = response.data.labels?.filter( - (label: any) => + (label: gmail_v1.Schema$Label) => label.type === "user" && + label.name && !label.name.startsWith("CATEGORY_") && !label.name.startsWith("CHAT"), ) || []; const labelsWithCounts = await Promise.all( - userLabels.map(async (label: any) => { - try { - const labelDetail = await gmail.users.labels.get({ - userId: "me", - id: label.id, - }); - return { - ...label, - messagesTotal: labelDetail.data.messagesTotal || 0, - messagesUnread: labelDetail.data.messagesUnread || 0, - threadsTotal: labelDetail.data.threadsTotal || 0, - threadsUnread: labelDetail.data.threadsUnread || 0, - }; - } catch (error) { - logger.warn(`Failed to get details for label ${label.name}:`, { - error: error instanceof Error ? error.message : String(error), - }); - return { - ...label, - messagesTotal: 0, - messagesUnread: 0, - threadsTotal: 0, - threadsUnread: 0, - }; - } - }), + userLabels + .filter( + ( + label, + ): label is gmail_v1.Schema$Label & { id: string; name: string } => + Boolean(label.id && label.name), + ) + .map(async (label) => { + try { + const labelDetail = await gmail.users.labels.get({ + userId: "me", + id: label.id, + }); + return { + ...label, + messagesTotal: labelDetail.data.messagesTotal || 0, + messagesUnread: labelDetail.data.messagesUnread || 0, + threadsTotal: labelDetail.data.threadsTotal || 0, + threadsUnread: labelDetail.data.threadsUnread || 0, + }; + } catch (error) { + logger.warn(`Failed to get details for label ${label.name}:`, { + error: error instanceof Error ? error.message : String(error), + }); + return { + ...label, + messagesTotal: 0, + messagesUnread: 0, + threadsTotal: 0, + threadsUnread: 0, + }; + } + }), ); const sortedLabels = labelsWithCounts.sort( - (a: any, b: any) => (b.messagesTotal || 0) - (a.messagesTotal || 0), + (a, b) => (b.messagesTotal || 0) - (a.messagesTotal || 0), ); return sortedLabels; @@ -159,7 +170,12 @@ async function getEmailReportData({ totalSent: number; } | null = null; - let emailAccount: any; + let emailAccount: + | (EmailAccount & { + account: Account; + user: Pick; + }) + | null; try { emailAccount = await prisma.emailAccount.findFirst({ where: { user: { email: userEmail } }, @@ -167,6 +183,7 @@ async function getEmailReportData({ account: true, user: { select: { + email: true, aiProvider: true, aiModel: true, aiApiKey: true, From 578ebfba871825fb5558ef74390c9f60daa81f5f Mon Sep 17 00:00:00 2001 From: Eduardo Lelis Date: Fri, 25 Jul 2025 12:03:36 -0300 Subject: [PATCH 05/20] Remove unused layout --- apps/web/app/(reports)/layout.tsx | 131 ------------------------------ 1 file changed, 131 deletions(-) delete mode 100644 apps/web/app/(reports)/layout.tsx diff --git a/apps/web/app/(reports)/layout.tsx b/apps/web/app/(reports)/layout.tsx deleted file mode 100644 index d17c44dc4b..0000000000 --- a/apps/web/app/(reports)/layout.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import type { Metadata } from "next"; -import Link from "next/link"; -import { redirect } from "next/navigation"; -import { auth } from "@/app/api/auth/[...nextauth]/auth"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { User, LogIn, UserPlus, Shield } from "lucide-react"; -import { isAdmin } from "@/utils/admin"; -import { ErrorPage } from "@/components/ErrorPage"; - -export const metadata: Metadata = { - title: "Sandbox - Email Analysis", - description: "Email analysis sandbox environment", -}; - -async function AuthStatus() { - const session = await auth(); - - if (session?.user) { - const userIsAdmin = isAdmin({ email: session.user.email }); - - return ( -
-
- - {session.user.email} -
-
- - Authenticated - - {userIsAdmin && ( - - - Admin - - )} -
-
- ); - } - - return ( -
- - Not Authenticated - -
- - -
-
- ); -} - -export default async function SandboxLayout({ - children, -}: { - children: React.ReactNode; -}) { - const session = await auth(); - - // Require authentication - if (!session?.user.email) { - redirect("/login"); - } - - // Require admin access - if (!isAdmin({ email: session.user.email })) { - return ( -
- - Return to Home - - } - /> -
- ); - } - - return ( -
-
-
-
-
- -
- S -
-

- Email Intelligence Sandbox -

- - - Development Environment - -
- -
-
-
-
- {children} -
-
- ); -} From a33bc1eee319606bf53da1f390acc42d484ca03b Mon Sep 17 00:00:00 2001 From: Eduardo Lelis Date: Fri, 25 Jul 2025 12:32:37 -0300 Subject: [PATCH 06/20] Cleanup error messages --- .../app/(reports)/api/email-report/fetch.ts | 53 +++++-------------- 1 file changed, 12 insertions(+), 41 deletions(-) diff --git a/apps/web/app/(reports)/api/email-report/fetch.ts b/apps/web/app/(reports)/api/email-report/fetch.ts index a1836fce01..b49ca5907c 100644 --- a/apps/web/app/(reports)/api/email-report/fetch.ts +++ b/apps/web/app/(reports)/api/email-report/fetch.ts @@ -15,7 +15,7 @@ const logger = createScopedLogger("email-report-fetch"); * This approach fetches one message at a time with retry and backofflogic, which is slower but more * reliable than trying to fetch 100 messages at once. * - * getMessagesLargeBatch because it expects the messageIds + * Not usinggetMessagesLargeBatch because it expects the messageIds * queryBatchMessages is limited to 20 messages at a time */ async function fetchEmailsByQuery( @@ -116,22 +116,15 @@ async function fetchEmailsByQuery( return parsedMessage; } catch (error) { logger.warn("fetchEmailsByQuery: getMessage attempt failed", { + error, messageId: message.id, attempt: i + 1, - error: error instanceof Error ? error.message : String(error), - errorType: - error instanceof Error - ? error.constructor.name - : typeof error, }); if (i === 2) { logger.warn( `Failed to fetch message ${message.id} after 3 attempts:`, - { - error: - error instanceof Error ? error.message : String(error), - }, + { error }, ); return null; } @@ -174,19 +167,16 @@ async function fetchEmailsByQuery( } catch (error) { retryCount++; logger.error("fetchEmailsByQuery: main loop error", { + error, retryCount, maxRetries, - error: error instanceof Error ? error.message : String(error), - errorType: - error instanceof Error ? error.constructor.name : typeof error, - errorStack: error instanceof Error ? error.stack : undefined, currentEmailsCount: emails.length, targetCount: count, }); if (retryCount >= maxRetries) { logger.error(`Failed to fetch emails after ${maxRetries} attempts:`, { - error: error instanceof Error ? error.message : String(error), + error, }); break; } @@ -253,15 +243,8 @@ export async function fetchEmailsForReport({ logger.info("fetchEmailsForReport: Gmail client initialized successfully"); } catch (error) { - logger.error("Failed to initialize Gmail client", { - error: error instanceof Error ? error.message : String(error), - errorType: error instanceof Error ? error.constructor.name : typeof error, - errorStack: error instanceof Error ? error.stack : undefined, - emailAccountId: emailAccount.id, - }); - throw new Error( - `Failed to initialize Gmail client: ${error instanceof Error ? error.message : String(error)}`, - ); + logger.error("Failed to initialize Gmail client", { error }); + throw new Error("Failed to initialize Gmail client"); } let receivedEmails: ParsedMessage[]; @@ -278,12 +261,7 @@ export async function fetchEmailsForReport({ count: receivedEmails.length, }); } catch (error) { - logger.error("Failed to fetch received emails", { - error: error instanceof Error ? error.message : String(error), - errorType: error instanceof Error ? error.constructor.name : typeof error, - errorStack: error instanceof Error ? error.stack : undefined, - emailAccountId: emailAccount.id, - }); + logger.error("Failed to fetch received emails", { error }); throw new Error( `Failed to fetch received emails: ${error instanceof Error ? error.message : String(error)}`, ); @@ -302,15 +280,8 @@ export async function fetchEmailsForReport({ count: sentEmails.length, }); } catch (error) { - logger.error("Failed to fetch sent emails", { - error: error instanceof Error ? error.message : String(error), - errorType: error instanceof Error ? error.constructor.name : typeof error, - errorStack: error instanceof Error ? error.stack : undefined, - emailAccountId: emailAccount.id, - }); - throw new Error( - `Failed to fetch sent emails: ${error instanceof Error ? error.message : String(error)}`, - ); + logger.error("Failed to fetch sent emails", { error }); + throw new Error("Failed to fetch sent emails"); } logger.info("fetchEmailsForReport: preparing return result", { @@ -357,7 +328,7 @@ async function fetchReceivedEmails( emails.push(...sourceEmails); } catch (error) { logger.error(`Error fetching emails from ${source.name}`, { - error: error instanceof Error ? error.message : String(error), + error, query: source.query, maxResults: targetCount - emails.length, }); @@ -377,7 +348,7 @@ async function fetchSentEmails( return emails; } catch (error) { logger.error("Error fetching sent emails", { - error: error instanceof Error ? error.message : String(error), + error, }); return []; } From a5ac182f32c07486158bcc30ac8b7e82496ba6f7 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:37:03 +0300 Subject: [PATCH 07/20] clean up generate report --- .cursor/rules/llm.mdc | 6 +- .../app/(app)/[emailAccountId]/debug/page.tsx | 4 +- .../[emailAccountId]/debug/report/page.tsx | 1199 ++++++++--------- .../app/(landing)/components/test-action.ts | 3 +- .../app/(reports)/api/email-report/route.ts | 453 ------- .../app/(reports)/api/email-report/schemas.ts | 160 --- apps/web/utils/actions/report.ts | 230 ++++ .../email-report => utils/ai/report}/fetch.ts | 102 +- .../ai/report}/prompts.ts | 306 +++-- apps/web/utils/gmail/message.ts | 2 +- 10 files changed, 1015 insertions(+), 1450 deletions(-) delete mode 100644 apps/web/app/(reports)/api/email-report/route.ts delete mode 100644 apps/web/app/(reports)/api/email-report/schemas.ts create mode 100644 apps/web/utils/actions/report.ts rename apps/web/{app/(reports)/api/email-report => utils/ai/report}/fetch.ts (74%) rename apps/web/{app/(reports)/api/email-report => utils/ai/report}/prompts.ts (70%) diff --git a/.cursor/rules/llm.mdc b/.cursor/rules/llm.mdc index e96571bc40..79f4d1d82a 100644 --- a/.cursor/rules/llm.mdc +++ b/.cursor/rules/llm.mdc @@ -47,7 +47,7 @@ export const schema = z.object({ // 3. Create main function with typed options export async function featureFunction(options: { inputData: InputType; - user: UserEmailWithAI; + emailAccount: EmailAccountWithAI; }) { const { inputData, user } = options; @@ -67,14 +67,14 @@ export async function featureFunction(options: { ... -${user.about ? `${user.about}` : ""}`; +${emailAccount.about ? `${emailAccount.about}` : ""}`; // 7. Log inputs logger.trace("Input", { system, prompt }); // 8. Call LLM with proper configuration const result = await chatCompletionObject({ - userAi: user, + userAi: emailAccount.user, system, prompt, schema, diff --git a/apps/web/app/(app)/[emailAccountId]/debug/page.tsx b/apps/web/app/(app)/[emailAccountId]/debug/page.tsx index 23dc3194f8..f2daf5e549 100644 --- a/apps/web/app/(app)/[emailAccountId]/debug/page.tsx +++ b/apps/web/app/(app)/[emailAccountId]/debug/page.tsx @@ -27,9 +27,7 @@ export default async function DebugPage(props: {
diff --git a/apps/web/app/(app)/[emailAccountId]/debug/report/page.tsx b/apps/web/app/(app)/[emailAccountId]/debug/report/page.tsx index 8ab7538f11..31a3bb1852 100644 --- a/apps/web/app/(app)/[emailAccountId]/debug/report/page.tsx +++ b/apps/web/app/(app)/[emailAccountId]/debug/report/page.tsx @@ -1,11 +1,9 @@ "use client"; -import { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { useAction } from "next-safe-action/hooks"; import { Badge } from "@/components/ui/badge"; import { - Loader2, Mail, TrendingUp, Target, @@ -14,202 +12,46 @@ import { Clock, } from "lucide-react"; import { useParams } from "next/navigation"; -import { fetchWithAccount } from "@/utils/fetch"; - -// Types based on the email report API response -interface ExecutiveSummary { - userProfile: { - persona: string; - confidence: number; - }; - topInsights: Array<{ - insight: string; - priority: "high" | "medium" | "low"; - icon: string; - }>; - quickActions: Array<{ - action: string; - difficulty: "easy" | "medium" | "hard"; - impact: "high" | "medium" | "low"; - }>; -} - -interface EmailActivityOverview { - dataSources: { - inbox: number; - archived: number; - trash: number; - sent: number; - }; -} - -interface UserPersona { - professionalIdentity: { - persona: string; - supportingEvidence: string[]; - }; - currentPriorities: string[]; -} - -interface EmailBehavior { - timingPatterns: { - peakHours: string[]; - responsePreference: string; - frequency: string; - }; - contentPreferences: { - preferred: string[]; - avoided: string[]; - }; - engagementTriggers: string[]; -} - -interface ResponsePatterns { - commonResponses: Array<{ - pattern: string; - example: string; - frequency: number; - triggers: string[]; - }>; - suggestedTemplates: Array<{ - templateName: string; - template: string; - useCase: string; - }>; - categoryOrganization: Array<{ - category: string; - description: string; - emailCount: number; - priority: "high" | "medium" | "low"; - }>; -} - -interface LabelAnalysis { - currentLabels: Array<{ - name: string; - emailCount: number; - unreadCount: number; - threadCount: number; - unreadThreads: number; - color: string | null; - type: string; - }>; - optimizationSuggestions: Array<{ - type: "create" | "consolidate" | "rename" | "delete"; - suggestion: string; - reason: string; - impact: "high" | "medium" | "low"; - }>; -} - -interface ActionableRecommendations { - immediateActions: Array<{ - action: string; - difficulty: "easy" | "medium" | "hard"; - impact: "high" | "medium" | "low"; - timeRequired: string; - }>; - shortTermImprovements: Array<{ - improvement: string; - timeline: string; - expectedBenefit: string; - }>; - longTermStrategy: Array<{ - strategy: string; - description: string; - successMetrics: string[]; - }>; -} - -interface EmailReportData { - executiveSummary: ExecutiveSummary; - emailActivityOverview: EmailActivityOverview; - userPersona: UserPersona; - emailBehavior: EmailBehavior; - responsePatterns: ResponsePatterns; - labelAnalysis: LabelAnalysis; - actionableRecommendations: ActionableRecommendations; -} +import { Button } from "@/components/ui/button"; +import { LoadingContent } from "@/components/LoadingContent"; +import { + type EmailReportData, + generateReportAction, +} from "@/utils/actions/report"; +import { useState } from "react"; +import { toastError, toastSuccess } from "@/components/Toast"; export default function EmailReportPage() { const params = useParams(); - const emailAccountId = params.emailAccountId as string; + const emailAccountId = params.emailAccountId; - const [isLoading, setIsLoading] = useState(true); - const [report, setReport] = useState(null); - const [error, setError] = useState(null); + if (typeof emailAccountId !== "string") + throw new Error("Email account ID is required"); - useEffect(() => { - const generateReport = async () => { - if (!emailAccountId) return; - - try { - const response = await fetchWithAccount({ - url: "/api/email-report", - emailAccountId, - init: { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }, - }); + const [report, setReport] = useState(null); - if (!response.ok) { - throw new Error(`Failed to generate report: ${response.statusText}`); + const { executeAsync, isExecuting, result } = useAction( + generateReportAction.bind(null, emailAccountId), + { + onSuccess: () => { + if (result?.data) { + setReport(result.data); + toastSuccess({ description: "Report generated successfully" }); + } else { + toastError({ description: "Failed to generate report" }); } - - const data = await response.json(); - setReport(data); - } catch (error) { - console.error("Error generating report:", error); - setError( - error instanceof Error ? error.message : "Failed to generate report", - ); - } finally { - setIsLoading(false); - } - }; - - generateReport(); - }, [emailAccountId]); - - const getPriorityColor = (priority: "high" | "medium" | "low") => { - switch (priority) { - case "high": - return "bg-red-100 text-red-800"; - case "medium": - return "bg-yellow-100 text-yellow-800"; - case "low": - return "bg-green-100 text-green-800"; - } - }; - - const getDifficultyColor = (difficulty: "easy" | "medium" | "hard") => { - switch (difficulty) { - case "easy": - return "bg-green-100 text-green-800"; - case "medium": - return "bg-yellow-100 text-yellow-800"; - case "hard": - return "bg-red-100 text-red-800"; - } - }; - - const getImpactColor = (impact: "high" | "medium" | "low") => { - switch (impact) { - case "high": - return "bg-blue-100 text-blue-800"; - case "medium": - return "bg-purple-100 text-purple-800"; - case "low": - return "bg-gray-100 text-gray-800"; - } - }; + }, + onError: (result) => { + toastError({ + title: "Failed to generate report", + description: result.error.serverError || "Unknown error", + }); + }, + }, + ); return (
- {/* Header Section */} @@ -218,482 +60,563 @@ export default function EmailReportPage() { -

- Comprehensive analysis of your email patterns and personalized - recommendations. -

- {isLoading && ( -
- - Generating report... -
- )} - {error &&
Error: {error}
} -
-
- - {/* Report Display */} - {report && ( -
- {/* Executive Summary */} - - - - - Executive Summary - - - -
-
-

- Professional Persona -

-

- {report.executiveSummary.userProfile.persona} -

-

- Confidence: {report.executiveSummary.userProfile.confidence} - % -

-
-
-

Email Sources

-
-
- Inbox: - - {report.emailActivityOverview.dataSources.inbox} - -
-
- Archived: - - {report.emailActivityOverview.dataSources.archived} - -
-
- Sent: - - {report.emailActivityOverview.dataSources.sent} - -
-
-
-
-

Quick Actions

-
- {report.executiveSummary.quickActions - .slice(0, 3) - .map((action, index) => ( -
- - {action.difficulty} - - - {action.action} - -
- ))} -
-
-
- -
-

- Top Insights -

-
- {report.executiveSummary.topInsights.map((insight, index) => ( -
- {insight.icon} -
-
- - {insight.priority} - -
-

- {insight.insight} + + + +

+ Comprehensive analysis of your email patterns and personalized + recommendations. +

+ + {/* Report Display */} + {report && ( +
+ {/* Executive Summary */} + + + + + Executive Summary + + + +
+
+

+ Professional Persona +

+

+ {report.executiveSummary.userProfile.persona}

-
-
- ))} -
-
- - - - {/* User Persona */} - - - - - Professional Identity - - - -
-

- Professional Identity -

-

- {report.userPersona.professionalIdentity.persona} -

-
- {report.userPersona.professionalIdentity.supportingEvidence.map( - (evidence, index) => ( -

- - {evidence} -

- ), - )} -
-
- -
-

- Current Priorities -

-
- {report.userPersona.currentPriorities.map( - (priority, index) => ( - - {priority} - - ), - )} -
-
-
-
- - {/* Email Behavior */} - - - - - Email Behavior Patterns - - - -
-
-
- Timing Patterns -
-

- Peak hours:{" "} - {report.emailBehavior.timingPatterns.peakHours.join(", ")} -

-

- Response preference:{" "} - {report.emailBehavior.timingPatterns.responsePreference} -

-

- Frequency: {report.emailBehavior.timingPatterns.frequency} -

-
-
-
- Content Preferences -
-

- Preferred:{" "} - {report.emailBehavior.contentPreferences.preferred.join( - ", ", - )} -

-

- Avoided:{" "} - {report.emailBehavior.contentPreferences.avoided.join(", ")} -

-
-
-
- Engagement Triggers -
-
- {report.emailBehavior.engagementTriggers.map( - (trigger, index) => ( -

- • {trigger} -

- ), - )} -
-
-
-
-
- - {/* Response Patterns */} - - - - - Response Patterns & Categories - - - -
-

- Common Response Patterns -

-
- {report.responsePatterns.commonResponses.map( - (response, index) => ( -
-
-
- {response.pattern} -
- {response.frequency}% -
-

- "{response.example}" +

+ Confidence:{" "} + {report.executiveSummary.userProfile.confidence}%

-
- {response.triggers.map((trigger, triggerIndex) => ( - - {trigger} - - ))} +
+
+

+ Email Sources +

+
+
+ Inbox: + + {report.emailActivityOverview.dataSources.inbox} + +
+
+ Archived: + + { + report.emailActivityOverview.dataSources + .archived + } + +
+
+ Sent: + + {report.emailActivityOverview.dataSources.sent} + +
- ), - )} -
-
- -
-

- Email Categories -

-
- {report.responsePatterns.categoryOrganization.map( - (category, index) => ( -
-
-
- {category.category} -
- - {category.priority} - +
+

+ Quick Actions +

+
+ {report.executiveSummary.quickActions + .slice(0, 3) + .map((action, index) => ( +
+ + {action.difficulty} + + + {action.action} + +
+ ))}
-

- {category.description} -

-

- {category.emailCount} emails -

- ), - )} -
-
- - - - {/* Label Analysis */} - - - - - Label Analysis - - - -
-

- Current Labels -

-
- {report.labelAnalysis.currentLabels.map((label, index) => ( -
-

- {label.name} -

-

- {label.emailCount} -

-

- {label.unreadCount} unread -

-

- {label.threadCount} threads -

- ))} -
-
-
-

- Optimization Suggestions -

-
- {report.labelAnalysis.optimizationSuggestions.map( - (suggestion, index) => ( -
-
-
- +

+ Top Insights +

+
+ {report.executiveSummary.topInsights.map( + (insight, index) => ( +
- {suggestion.type} - -

- {suggestion.suggestion} + {insight.icon} +

+
+ + {insight.priority} + +
+

+ {insight.insight} +

+
+
+ ), + )} +
+
+ + + + {/* User Persona */} + + + + + Professional Identity + + + +
+

+ Professional Identity +

+

+ {report.userPersona.professionalIdentity.persona} +

+
+ {report.userPersona.professionalIdentity.supportingEvidence.map( + (evidence, index) => ( +

+ + {evidence}

-
-

- {suggestion.reason} -

-
- - {suggestion.impact} impact - + ), + )}
- ), - )} -
-
- - +
- {/* Actionable Recommendations */} - - - - - Actionable Recommendations - - - -
-

- Immediate Actions -

-
- {report.actionableRecommendations.immediateActions.map( - (action, index) => ( -
-
-

- {action.action} -

-

- Time required: {action.timeRequired} -

-
-
- - {action.difficulty} - - - {action.impact} impact - -
+
+

+ Current Priorities +

+
+ {report.userPersona.currentPriorities.map( + (priority, index) => ( + + {priority} + + ), + )}
- ), - )} -
-
- -
-

- Short-term Improvements -

-
- {report.actionableRecommendations.shortTermImprovements.map( - (improvement, index) => ( -
+
+ + + + {/* Email Behavior */} + + + + + Email Behavior Patterns + + + +
+
- {improvement.improvement} + Timing Patterns
-

- Timeline: {improvement.timeline} +

+ Peak hours:{" "} + {report.emailBehavior.timingPatterns.peakHours.join( + ", ", + )} +

+

+ Response preference:{" "} + { + report.emailBehavior.timingPatterns + .responsePreference + }

- {improvement.expectedBenefit} + Frequency:{" "} + {report.emailBehavior.timingPatterns.frequency}

- ), - )} -
-
- -
-

- Long-term Strategy -

-
- {report.actionableRecommendations.longTermStrategy.map( - (strategy, index) => ( -
+
- {strategy.strategy} + Content Preferences
-

- {strategy.description} +

+ Preferred:{" "} + {report.emailBehavior.contentPreferences.preferred.join( + ", ", + )}

-
- {strategy.successMetrics.map( - (metric, metricIndex) => ( - - {metric} - +

+ Avoided:{" "} + {report.emailBehavior.contentPreferences.avoided.join( + ", ", + )} +

+
+
+
+ Engagement Triggers +
+
+ {report.emailBehavior.engagementTriggers.map( + (trigger, index) => ( +

+ • {trigger} +

), )}
- ), - )} -
+
+ + + + {/* Response Patterns */} + + + + + Response Patterns & Categories + + + +
+

+ Common Response Patterns +

+
+ {report.responsePatterns.commonResponses.map( + (response, index) => ( +
+
+
+ {response.pattern} +
+ + {response.frequency}% + +
+

+ "{response.example}" +

+
+ {response.triggers.map( + (trigger, triggerIndex) => ( + + {trigger} + + ), + )} +
+
+ ), + )} +
+
+ +
+

+ Email Categories +

+
+ {report.responsePatterns.categoryOrganization.map( + (category, index) => ( +
+
+
+ {category.category} +
+ + {category.priority} + +
+

+ {category.description} +

+

+ {category.emailCount} emails +

+
+ ), + )} +
+
+
+
+ + {/* Label Analysis */} + + + + + Label Analysis + + + +
+

+ Current Labels +

+
+ {report.labelAnalysis.currentLabels.map( + (label, index) => ( +
+

+ {label.name} +

+

+ {label.emailCount} +

+

+ {label.unreadCount} unread +

+

+ {label.threadCount} threads +

+
+ ), + )} +
+
+ +
+

+ Optimization Suggestions +

+
+ {report.labelAnalysis.optimizationSuggestions.map( + (suggestion, index) => ( +
+
+
+ + {suggestion.type} + +

+ {suggestion.suggestion} +

+
+

+ {suggestion.reason} +

+
+ + {suggestion.impact} impact + +
+ ), + )} +
+
+
+
+ + {/* Actionable Recommendations */} + + + + + Actionable Recommendations + + + +
+

+ Immediate Actions +

+
+ {report.actionableRecommendations.immediateActions.map( + (action, index) => ( +
+
+

+ {action.action} +

+

+ Time required: {action.timeRequired} +

+
+
+ + {action.difficulty} + + + {action.impact} impact + +
+
+ ), + )} +
+
+ +
+

+ Short-term Improvements +

+
+ {report.actionableRecommendations.shortTermImprovements.map( + (improvement, index) => ( +
+
+ {improvement.improvement} +
+

+ Timeline: {improvement.timeline} +

+

+ {improvement.expectedBenefit} +

+
+ ), + )} +
+
+ +
+

+ Long-term Strategy +

+
+ {report.actionableRecommendations.longTermStrategy.map( + (strategy, index) => ( +
+
+ {strategy.strategy} +
+

+ {strategy.description} +

+
+ {strategy.successMetrics.map( + (metric, metricIndex) => ( + + {metric} + + ), + )} +
+
+ ), + )} +
+
+
+
- - -
- )} + )} + + +
); } + +const getPriorityColor = (priority: "high" | "medium" | "low") => { + switch (priority) { + case "high": + return "bg-red-100 text-red-800"; + case "medium": + return "bg-yellow-100 text-yellow-800"; + case "low": + return "bg-green-100 text-green-800"; + } +}; + +const getDifficultyColor = (difficulty: "easy" | "medium" | "hard") => { + switch (difficulty) { + case "easy": + return "bg-green-100 text-green-800"; + case "medium": + return "bg-yellow-100 text-yellow-800"; + case "hard": + return "bg-red-100 text-red-800"; + } +}; + +const getImpactColor = (impact: "high" | "medium" | "low") => { + switch (impact) { + case "high": + return "bg-blue-100 text-blue-800"; + case "medium": + return "bg-purple-100 text-purple-800"; + case "low": + return "bg-gray-100 text-gray-800"; + } +}; diff --git a/apps/web/app/(landing)/components/test-action.ts b/apps/web/app/(landing)/components/test-action.ts index 10e6b31773..74c2504513 100644 --- a/apps/web/app/(landing)/components/test-action.ts +++ b/apps/web/app/(landing)/components/test-action.ts @@ -1,6 +1,7 @@ "use server"; import { createScopedLogger } from "@/utils/logger"; +import { sleep } from "@/utils/sleep"; const logger = createScopedLogger("testAction"); @@ -8,7 +9,7 @@ export async function testAction() { logger.info("testAction started"); // sleep for 5 seconds - await new Promise((resolve) => setTimeout(resolve, 5000)); + await sleep(5000); logger.info("testAction completed"); diff --git a/apps/web/app/(reports)/api/email-report/route.ts b/apps/web/app/(reports)/api/email-report/route.ts deleted file mode 100644 index 34911b1469..0000000000 --- a/apps/web/app/(reports)/api/email-report/route.ts +++ /dev/null @@ -1,453 +0,0 @@ -import { NextResponse } from "next/server"; -import { withEmailAccount } from "@/utils/middleware"; -import { createScopedLogger } from "@/utils/logger"; -import { fetchEmailsForReport, fetchGmailTemplates } from "./fetch"; -import type { ParsedMessage } from "@/utils/types"; -import prisma from "@/utils/prisma"; -import { getGmailClientWithRefresh } from "@/utils/gmail/client"; - -import { - generateExecutiveSummary, - buildUserPersona, - analyzeEmailBehavior, - analyzeResponsePatterns, - analyzeLabelOptimization, - generateActionableRecommendations, - summarizeEmails, -} from "./prompts"; -import type { EmailSummary } from "./schemas"; -import type { gmail_v1 } from "@googleapis/gmail"; -import type { EmailAccount, User, Account } from "@prisma/client"; - -const logger = createScopedLogger("email-report-api"); - -export const GET = withEmailAccount(async (request) => { - const { emailAccountId, email } = request.auth; - - try { - const result = await getEmailReportData({ - emailAccountId, - userEmail: email, - }); - return NextResponse.json(result); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - const errorStack = error instanceof Error ? error.stack : undefined; - - logger.error("Error generating email report", { - error: errorMessage, - stack: errorStack, - emailAccountId, - }); - - return NextResponse.json( - { - error: "Failed to generate email report", - details: errorMessage, - emailAccountId, - }, - { status: 500 }, - ); - } -}); - -async function fetchGmailLabels( - gmail: gmail_v1.Gmail, -): Promise { - try { - const response = await gmail.users.labels.list({ userId: "me" }); - - const userLabels = - response.data.labels?.filter( - (label: gmail_v1.Schema$Label) => - label.type === "user" && - label.name && - !label.name.startsWith("CATEGORY_") && - !label.name.startsWith("CHAT"), - ) || []; - - const labelsWithCounts = await Promise.all( - userLabels - .filter( - ( - label, - ): label is gmail_v1.Schema$Label & { id: string; name: string } => - Boolean(label.id && label.name), - ) - .map(async (label) => { - try { - const labelDetail = await gmail.users.labels.get({ - userId: "me", - id: label.id, - }); - return { - ...label, - messagesTotal: labelDetail.data.messagesTotal || 0, - messagesUnread: labelDetail.data.messagesUnread || 0, - threadsTotal: labelDetail.data.threadsTotal || 0, - threadsUnread: labelDetail.data.threadsUnread || 0, - }; - } catch (error) { - logger.warn(`Failed to get details for label ${label.name}:`, { - error: error instanceof Error ? error.message : String(error), - }); - return { - ...label, - messagesTotal: 0, - messagesUnread: 0, - threadsTotal: 0, - threadsUnread: 0, - }; - } - }), - ); - - const sortedLabels = labelsWithCounts.sort( - (a, b) => (b.messagesTotal || 0) - (a.messagesTotal || 0), - ); - - return sortedLabels; - } catch (error) { - logger.warn("Failed to fetch Gmail labels:", { - error: error instanceof Error ? error.message : String(error), - }); - return []; - } -} - -async function fetchGmailSignature(gmail: gmail_v1.Gmail): Promise { - try { - const sendAsList = await gmail.users.settings.sendAs.list({ - userId: "me", - }); - - if (!sendAsList.data.sendAs || sendAsList.data.sendAs.length === 0) { - logger.warn("No sendAs settings found"); - return ""; - } - - const primarySendAs = sendAsList.data.sendAs[0]; - if (!primarySendAs.sendAsEmail) { - logger.warn("No primary sendAs email found"); - return ""; - } - - const signatureResponse = await gmail.users.settings.sendAs.get({ - userId: "me", - sendAsEmail: primarySendAs.sendAsEmail, - }); - - const signature = signatureResponse.data.signature; - logger.info("Gmail signature fetched successfully", { - hasSignature: !!signature, - sendAsEmail: primarySendAs.sendAsEmail, - }); - - return signature || ""; - } catch (error) { - logger.warn("Failed to fetch Gmail signature:", { - error: error instanceof Error ? error.message : String(error), - }); - return ""; - } -} - -async function getEmailReportData({ - emailAccountId, - userEmail, -}: { - emailAccountId: string; - userEmail: string; -}) { - logger.info("getEmailReportData started", { - emailAccountId, - userEmail, - }); - let emailData: { - receivedEmails: ParsedMessage[]; - sentEmails: ParsedMessage[]; - totalReceived: number; - totalSent: number; - } | null = null; - - let emailAccount: - | (EmailAccount & { - account: Account; - user: Pick; - }) - | null; - try { - emailAccount = await prisma.emailAccount.findFirst({ - where: { user: { email: userEmail } }, - include: { - account: true, - user: { - select: { - email: true, - aiProvider: true, - aiModel: true, - aiApiKey: true, - }, - }, - }, - }); - - if (!emailAccount) { - throw new Error("Email account not found"); - } - } catch (error) { - logger.error("Failed to fetch email account", { - error: error instanceof Error ? error.message : String(error), - userEmail, - emailAccountId, - }); - throw new Error( - `Failed to fetch email account: ${error instanceof Error ? error.message : String(error)}`, - ); - } - - try { - logger.info("getEmailReportData: about to call fetchEmailsForReport", { - emailAccountId: emailAccount.id, - userEmail: emailAccount.user?.email, - }); - - emailData = await fetchEmailsForReport({ - emailAccount, - }); - - logger.info( - "getEmailReportData: fetchEmailsForReport completed successfully", - { - receivedCount: emailData.totalReceived, - sentCount: emailData.totalSent, - }, - ); - } catch (error) { - logger.error("Failed to fetch emails for report", { - error: error instanceof Error ? error.message : String(error), - emailAccountId, - userEmail, - }); - throw new Error( - `Failed to fetch emails: ${error instanceof Error ? error.message : String(error)}`, - ); - } - - const { receivedEmails, sentEmails, totalReceived, totalSent } = emailData; - - let receivedEmailSummaries: EmailSummary[] = []; - let sentEmailSummaries: EmailSummary[] = []; - - try { - logger.info("Starting email summarization", { - receivedEmailsCount: receivedEmails.length, - sentEmailsCount: sentEmails.length, - }); - - logger.info("About to call summarizeEmails for received emails", { - receivedEmailsCount: receivedEmails.length, - firstEmailId: receivedEmails[0]?.id, - lastEmailId: receivedEmails[receivedEmails.length - 1]?.id, - }); - - const receivedSummaries = await summarizeEmails( - receivedEmails, - userEmail, - emailAccount, - ); - - logger.info("Received email summaries completed", { - summariesCount: receivedSummaries.length, - }); - - logger.info("About to call summarizeEmails for sent emails", { - sentEmailsCount: sentEmails.length, - firstEmailId: sentEmails[0]?.id, - lastEmailId: sentEmails[sentEmails.length - 1]?.id, - }); - - const sentSummaries = await summarizeEmails( - sentEmails, - userEmail, - emailAccount, - ); - - logger.info("Sent email summaries completed", { - summariesCount: sentSummaries.length, - }); - - receivedEmailSummaries = receivedSummaries; - sentEmailSummaries = sentSummaries; - } catch (error) { - logger.error("Failed to generate email summaries", { - error: error instanceof Error ? error.message : String(error), - errorType: error instanceof Error ? error.constructor.name : typeof error, - errorStack: error instanceof Error ? error.stack : undefined, - emailAccountId, - userEmail, - }); - throw new Error( - `Failed to generate email summaries: ${error instanceof Error ? error.message : String(error)}`, - ); - } - - if ( - !emailAccount?.account?.access_token || - !emailAccount?.account?.refresh_token - ) { - throw new Error("Missing Gmail tokens"); - } - - const gmail = await getGmailClientWithRefresh({ - accessToken: emailAccount.account.access_token, - refreshToken: emailAccount.account.refresh_token, - expiresAt: emailAccount.account.expires_at, - emailAccountId: emailAccount.id, - }); - - const gmailLabels = await fetchGmailLabels(gmail); - const gmailSignature = await fetchGmailSignature(gmail); - const gmailTemplates = await fetchGmailTemplates(gmail); - - let executiveSummary: any, - userPersona: any, - emailBehavior: any, - responsePatterns: any, - labelAnalysis: any, - actionableRecommendations: any; - - try { - executiveSummary = await generateExecutiveSummary( - receivedEmailSummaries, - sentEmailSummaries, - gmailLabels, - userEmail, - emailAccount, - ); - } catch (error) { - logger.error("Failed to generate executive summary", { - error: error instanceof Error ? error.message : String(error), - emailAccountId, - }); - throw new Error( - `Failed to generate executive summary: ${error instanceof Error ? error.message : String(error)}`, - ); - } - - try { - userPersona = await buildUserPersona( - receivedEmailSummaries, - userEmail, - emailAccount, - sentEmailSummaries, - gmailSignature, - gmailTemplates, - ); - } catch (error) { - logger.error("Failed to build user persona", { - error: error instanceof Error ? error.message : String(error), - emailAccountId, - }); - throw new Error( - `Failed to build user persona: ${error instanceof Error ? error.message : String(error)}`, - ); - } - - try { - emailBehavior = await analyzeEmailBehavior( - receivedEmailSummaries, - userEmail, - emailAccount, - sentEmailSummaries, - ); - } catch (error) { - logger.error("Failed to analyze email behavior", { - error: error instanceof Error ? error.message : String(error), - emailAccountId, - }); - throw new Error( - `Failed to analyze email behavior: ${error instanceof Error ? error.message : String(error)}`, - ); - } - - try { - responsePatterns = await analyzeResponsePatterns( - receivedEmailSummaries, - userEmail, - emailAccount, - sentEmailSummaries, - ); - } catch (error) { - logger.error("Failed to analyze response patterns", { - error: error instanceof Error ? error.message : String(error), - emailAccountId, - }); - throw new Error( - `Failed to analyze response patterns: ${error instanceof Error ? error.message : String(error)}`, - ); - } - - try { - labelAnalysis = await analyzeLabelOptimization( - receivedEmailSummaries, - userEmail, - emailAccount, - gmailLabels, - ); - } catch (error) { - logger.error("Failed to analyze label optimization", { - error: error instanceof Error ? error.message : String(error), - emailAccountId, - }); - throw new Error( - `Failed to analyze label optimization: ${error instanceof Error ? error.message : String(error)}`, - ); - } - - try { - actionableRecommendations = await generateActionableRecommendations( - receivedEmailSummaries, - userEmail, - emailAccount, - userPersona, - emailBehavior, - ); - } catch (error) { - logger.error("Failed to generate actionable recommendations", { - error: error instanceof Error ? error.message : String(error), - emailAccountId, - }); - throw new Error( - `Failed to generate actionable recommendations: ${error instanceof Error ? error.message : String(error)}`, - ); - } - - return { - executiveSummary, - emailActivityOverview: { - dataSources: { - inbox: totalReceived, - archived: 0, - trash: 0, - sent: totalSent, - }, - }, - userPersona, - emailBehavior, - responsePatterns, - labelAnalysis: { - currentLabels: gmailLabels.map((label) => ({ - name: label.name, - emailCount: label.messagesTotal || 0, - unreadCount: label.messagesUnread || 0, - threadCount: label.threadsTotal || 0, - unreadThreads: label.threadsUnread || 0, - color: label.color || null, - type: label.type, - })), - optimizationSuggestions: labelAnalysis.optimizationSuggestions, - }, - actionableRecommendations, - }; -} diff --git a/apps/web/app/(reports)/api/email-report/schemas.ts b/apps/web/app/(reports)/api/email-report/schemas.ts deleted file mode 100644 index 02fec639db..0000000000 --- a/apps/web/app/(reports)/api/email-report/schemas.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { z } from "zod"; - -export const emailSummarySchema = z.object({ - summary: z.string().describe("Brief summary of the email content"), - sender: z.string().describe("Email sender"), - subject: z.string().describe("Email subject"), - category: z - .string() - .describe("Category of the email (work, personal, marketing, etc.)"), -}); - -export const executiveSummarySchema = z.object({ - userProfile: z.object({ - persona: z - .string() - .describe( - "1-5 word persona identification (e.g., 'Tech Startup Founder')", - ), - confidence: z - .number() - .min(0) - .max(100) - .describe("Confidence level in persona identification (0-100)"), - }), - topInsights: z - .array( - z.object({ - insight: z.string().describe("Key insight about user's email behavior"), - priority: z - .enum(["high", "medium", "low"]) - .describe("Priority level of this insight"), - icon: z.string().describe("Single emoji representing this insight"), - }), - ) - .describe("3-5 most important findings from the analysis"), - quickActions: z - .array( - z.object({ - action: z - .string() - .describe("Specific action the user can take immediately"), - difficulty: z - .enum(["easy", "medium", "hard"]) - .describe("How difficult this action is to implement"), - impact: z - .enum(["high", "medium", "low"]) - .describe("Expected impact of this action"), - }), - ) - .describe("4-6 immediate actions the user can take"), -}); - -export const userPersonaSchema = z.object({ - professionalIdentity: z.object({ - persona: z.string().describe("Professional persona identification"), - supportingEvidence: z - .array(z.string()) - .describe("Evidence supporting this persona identification"), - }), - currentPriorities: z - .array(z.string()) - .describe("Current professional priorities based on email content"), -}); - -export const emailBehaviorSchema = z.object({ - timingPatterns: z.object({ - peakHours: z.array(z.string()).describe("Peak email activity hours"), - responsePreference: z.string().describe("Preferred response timing"), - frequency: z.string().describe("Overall email frequency"), - }), - contentPreferences: z.object({ - preferred: z - .array(z.string()) - .describe("Types of emails user engages with"), - avoided: z - .array(z.string()) - .describe("Types of emails user typically ignores"), - }), - engagementTriggers: z - .array(z.string()) - .describe("What prompts user to take action on emails"), -}); - -export const responsePatternsSchema = z.object({ - commonResponses: z.array( - z.object({ - pattern: z.string().describe("Description of the response pattern"), - example: z.string().describe("Example of this type of response"), - frequency: z - .number() - .describe("Percentage of responses using this pattern"), - triggers: z - .array(z.string()) - .describe("What types of emails trigger this response"), - }), - ), - suggestedTemplates: z.array( - z.object({ - templateName: z.string().describe("Name of the email template"), - template: z.string().describe("The actual email template text"), - useCase: z.string().describe("When to use this template"), - }), - ), - categoryOrganization: z.array( - z.object({ - category: z.string().describe("Email category name"), - description: z - .string() - .describe("What types of emails belong in this category"), - emailCount: z - .number() - .describe("Estimated number of emails in this category"), - priority: z - .enum(["high", "medium", "low"]) - .describe("Priority level for this category"), - }), - ), -}); - -export const labelAnalysisSchema = z.object({ - optimizationSuggestions: z.array( - z.object({ - type: z - .enum(["create", "consolidate", "rename", "delete"]) - .describe("Type of optimization"), - suggestion: z.string().describe("Specific suggestion"), - reason: z.string().describe("Reason for this suggestion"), - impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), - }), - ), -}); - -export const actionableRecommendationsSchema = z.object({ - immediateActions: z.array( - z.object({ - action: z.string().describe("Specific action to take"), - difficulty: z - .enum(["easy", "medium", "hard"]) - .describe("Implementation difficulty"), - impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), - timeRequired: z.string().describe("Time required (e.g., '5 minutes')"), - }), - ), - shortTermImprovements: z.array( - z.object({ - improvement: z.string().describe("Improvement to implement"), - timeline: z.string().describe("When to implement (e.g., 'This week')"), - expectedBenefit: z.string().describe("Expected benefit"), - }), - ), - longTermStrategy: z.array( - z.object({ - strategy: z.string().describe("Strategic initiative"), - description: z.string().describe("Detailed description"), - successMetrics: z.array(z.string()).describe("How to measure success"), - }), - ), -}); - -export type EmailSummary = z.infer; diff --git a/apps/web/utils/actions/report.ts b/apps/web/utils/actions/report.ts new file mode 100644 index 0000000000..27829ab9d1 --- /dev/null +++ b/apps/web/utils/actions/report.ts @@ -0,0 +1,230 @@ +"use server"; + +import { + fetchEmailsForReport, + fetchGmailTemplates, +} from "@/utils/ai/report/fetch"; +import { + generateExecutiveSummary, + buildUserPersona, + analyzeEmailBehavior, + analyzeResponsePatterns, + analyzeLabelOptimization, + generateActionableRecommendations, + summarizeEmails, +} from "@/utils/ai/report/prompts"; +import type { gmail_v1 } from "@googleapis/gmail"; +import { createScopedLogger } from "@/utils/logger"; +import { actionClient } from "@/utils/actions/safe-action"; +import { z } from "zod"; +import { getEmailAccountWithAi } from "@/utils/user/get"; +import { getGmailClientForEmail } from "@/utils/account"; + +const logger = createScopedLogger("actions/report"); + +export type EmailReportData = Awaited>; + +export const generateReportAction = actionClient + .metadata({ name: "generateReport" }) + .schema(z.object({})) + .action(async ({ ctx: { emailAccountId } }) => { + return getEmailReportData({ emailAccountId }); + }); + +async function fetchGmailLabels( + gmail: gmail_v1.Gmail, +): Promise { + try { + const response = await gmail.users.labels.list({ userId: "me" }); + + const userLabels = + response.data.labels?.filter( + (label: gmail_v1.Schema$Label) => + label.type === "user" && + label.name && + !label.name.startsWith("CATEGORY_") && + !label.name.startsWith("CHAT"), + ) || []; + + const labelsWithCounts = await Promise.all( + userLabels + .filter( + ( + label, + ): label is gmail_v1.Schema$Label & { id: string; name: string } => + Boolean(label.id && label.name), + ) + .map(async (label) => { + try { + const labelDetail = await gmail.users.labels.get({ + userId: "me", + id: label.id, + }); + return { + ...label, + messagesTotal: labelDetail.data.messagesTotal || 0, + messagesUnread: labelDetail.data.messagesUnread || 0, + threadsTotal: labelDetail.data.threadsTotal || 0, + threadsUnread: labelDetail.data.threadsUnread || 0, + }; + } catch (error) { + logger.warn(`Failed to get details for label ${label.name}:`, { + error: error instanceof Error ? error.message : String(error), + }); + return { + ...label, + messagesTotal: 0, + messagesUnread: 0, + threadsTotal: 0, + threadsUnread: 0, + }; + } + }), + ); + + const sortedLabels = labelsWithCounts.sort( + (a, b) => (b.messagesTotal || 0) - (a.messagesTotal || 0), + ); + + return sortedLabels; + } catch (error) { + logger.warn("Failed to fetch Gmail labels:", { + error: error instanceof Error ? error.message : String(error), + }); + return []; + } +} + +async function fetchGmailSignature(gmail: gmail_v1.Gmail): Promise { + try { + const sendAsList = await gmail.users.settings.sendAs.list({ + userId: "me", + }); + + if (!sendAsList.data.sendAs || sendAsList.data.sendAs.length === 0) { + logger.warn("No sendAs settings found"); + return ""; + } + + const primarySendAs = sendAsList.data.sendAs[0]; + if (!primarySendAs.sendAsEmail) { + logger.warn("No primary sendAs email found"); + return ""; + } + + const signatureResponse = await gmail.users.settings.sendAs.get({ + userId: "me", + sendAsEmail: primarySendAs.sendAsEmail, + }); + + const signature = signatureResponse.data.signature; + logger.info("Gmail signature fetched successfully", { + hasSignature: !!signature, + sendAsEmail: primarySendAs.sendAsEmail, + }); + + return signature || ""; + } catch (error) { + logger.warn("Failed to fetch Gmail signature:", { + error: error instanceof Error ? error.message : String(error), + }); + return ""; + } +} + +async function getEmailReportData({ + emailAccountId, +}: { + emailAccountId: string; +}) { + logger.info("getEmailReportData started", { emailAccountId }); + + const emailAccount = await getEmailAccountWithAi({ emailAccountId }); + + if (!emailAccount) { + logger.error("Email account not found", { emailAccountId }); + throw new Error("Email account not found"); + } + + const emailData = await fetchEmailsForReport({ emailAccount }); + + const { receivedEmails, sentEmails, totalReceived, totalSent } = emailData; + + const receivedSummaries = await summarizeEmails(receivedEmails, emailAccount); + const sentSummaries = await summarizeEmails(sentEmails, emailAccount); + + const gmail = await getGmailClientForEmail({ + emailAccountId: emailAccount.id, + }); + + const gmailLabels = await fetchGmailLabels(gmail); + const gmailSignature = await fetchGmailSignature(gmail); + const gmailTemplates = await fetchGmailTemplates(gmail); + + const executiveSummary = await generateExecutiveSummary( + receivedSummaries, + sentSummaries, + gmailLabels, + emailAccount, + ); + + const userPersona = await buildUserPersona( + receivedSummaries, + emailAccount, + sentSummaries, + gmailSignature, + gmailTemplates, + ); + + const emailBehavior = await analyzeEmailBehavior( + receivedSummaries, + emailAccount, + sentSummaries, + ); + + const responsePatterns = await analyzeResponsePatterns( + receivedSummaries, + emailAccount, + sentSummaries, + ); + + const labelAnalysis = await analyzeLabelOptimization( + receivedSummaries, + emailAccount, + gmailLabels, + ); + + const actionableRecommendations = await generateActionableRecommendations( + receivedSummaries, + emailAccount, + userPersona, + ); + + return { + executiveSummary, + emailActivityOverview: { + dataSources: { + inbox: totalReceived, + archived: 0, + trash: 0, + sent: totalSent, + }, + }, + userPersona, + emailBehavior, + responsePatterns, + labelAnalysis: { + currentLabels: gmailLabels.map((label) => ({ + name: label.name, + emailCount: label.messagesTotal || 0, + unreadCount: label.messagesUnread || 0, + threadCount: label.threadsTotal || 0, + unreadThreads: label.threadsUnread || 0, + color: label.color || null, + type: label.type, + })), + optimizationSuggestions: labelAnalysis.optimizationSuggestions, + }, + actionableRecommendations, + }; +} diff --git a/apps/web/app/(reports)/api/email-report/fetch.ts b/apps/web/utils/ai/report/fetch.ts similarity index 74% rename from apps/web/app/(reports)/api/email-report/fetch.ts rename to apps/web/utils/ai/report/fetch.ts index b49ca5907c..bed5aa600e 100644 --- a/apps/web/app/(reports)/api/email-report/fetch.ts +++ b/apps/web/utils/ai/report/fetch.ts @@ -1,10 +1,10 @@ import type { gmail_v1 } from "@googleapis/gmail"; -import { getGmailClientWithRefresh } from "@/utils/gmail/client"; -import { getMessages, getMessage } from "@/utils/gmail/message"; -import { parseMessage } from "@/utils/mail"; +import { getMessages, getMessage, parseMessage } from "@/utils/gmail/message"; import { createScopedLogger } from "@/utils/logger"; import type { ParsedMessage } from "@/utils/types"; -import type { EmailAccount, User, Account } from "@prisma/client"; +import type { EmailAccountWithAI } from "@/utils/llms/types"; +import { sleep } from "@/utils/sleep"; +import { getGmailClientForEmail } from "@/utils/account"; const logger = createScopedLogger("email-report-fetch"); @@ -181,7 +181,7 @@ async function fetchEmailsByQuery( break; } - await new Promise((resolve) => setTimeout(resolve, 2000 * retryCount)); + await sleep(2000 * retryCount); } } @@ -204,105 +204,31 @@ export interface EmailFetchResult { export async function fetchEmailsForReport({ emailAccount, }: { - emailAccount: EmailAccount & { - account: Account; - user: Pick; - }; + emailAccount: EmailAccountWithAI; }): Promise { logger.info("fetchEmailsForReport started", { - emailAccountId: emailAccount?.id, - userEmail: emailAccount?.user?.email, - hasAccessToken: !!emailAccount?.account?.access_token, - hasRefreshToken: !!emailAccount?.account?.refresh_token, + emailAccountId: emailAccount.id, }); - if ( - !emailAccount.account?.access_token || - !emailAccount.account?.refresh_token - ) { - logger.error("fetchEmailsForReport: missing Gmail tokens", { - hasAccessToken: !!emailAccount?.account?.access_token, - hasRefreshToken: !!emailAccount?.account?.refresh_token, - }); - throw new Error("Missing Gmail tokens"); - } - - let gmail: gmail_v1.Gmail; - try { - logger.info("fetchEmailsForReport: initializing Gmail client", { - emailAccountId: emailAccount.id, - expiresAt: emailAccount.account.expires_at, - }); - - gmail = await getGmailClientWithRefresh({ - accessToken: emailAccount.account.access_token, - refreshToken: emailAccount.account.refresh_token, - expiresAt: emailAccount.account.expires_at, - emailAccountId: emailAccount.id, - }); - - logger.info("fetchEmailsForReport: Gmail client initialized successfully"); - } catch (error) { - logger.error("Failed to initialize Gmail client", { error }); - throw new Error("Failed to initialize Gmail client"); - } - - let receivedEmails: ParsedMessage[]; - let sentEmails: ParsedMessage[]; - - try { - logger.info("fetchEmailsForReport: about to fetch received emails", { - targetCount: 200, - }); - - receivedEmails = await fetchReceivedEmails(gmail, 200); - - logger.info("fetchEmailsForReport: received emails fetched successfully", { - count: receivedEmails.length, - }); - } catch (error) { - logger.error("Failed to fetch received emails", { error }); - throw new Error( - `Failed to fetch received emails: ${error instanceof Error ? error.message : String(error)}`, - ); - } - - await new Promise((resolve) => setTimeout(resolve, 3000)); - - try { - logger.info("fetchEmailsForReport: about to fetch sent emails", { - targetCount: 50, - }); - - sentEmails = await fetchSentEmails(gmail, 50); + const gmail = await getGmailClientForEmail({ + emailAccountId: emailAccount.id, + }); - logger.info("fetchEmailsForReport: sent emails fetched successfully", { - count: sentEmails.length, - }); - } catch (error) { - logger.error("Failed to fetch sent emails", { error }); - throw new Error("Failed to fetch sent emails"); - } + const receivedEmails = await fetchReceivedEmails(gmail, 200); + await sleep(3000); + const sentEmails = await fetchSentEmails(gmail, 50); logger.info("fetchEmailsForReport: preparing return result", { receivedCount: receivedEmails.length, sentCount: sentEmails.length, }); - const result = { + return { receivedEmails, sentEmails, totalReceived: receivedEmails.length, totalSent: sentEmails.length, }; - - logger.info("fetchEmailsForReport: returning result", { - resultKeys: Object.keys(result), - receivedCount: result.totalReceived, - sentCount: result.totalSent, - }); - - return result; } async function fetchReceivedEmails( diff --git a/apps/web/app/(reports)/api/email-report/prompts.ts b/apps/web/utils/ai/report/prompts.ts similarity index 70% rename from apps/web/app/(reports)/api/email-report/prompts.ts rename to apps/web/utils/ai/report/prompts.ts index 7e3419a37b..346f08dea0 100644 --- a/apps/web/app/(reports)/api/email-report/prompts.ts +++ b/apps/web/utils/ai/report/prompts.ts @@ -1,30 +1,173 @@ import { z } from "zod"; import { createScopedLogger } from "@/utils/logger"; import { chatCompletionObject } from "@/utils/llms"; -import type { EmailSummary } from "./schemas"; -import type { EmailAccount, User, Account } from "@prisma/client"; import type { gmail_v1 } from "@googleapis/gmail"; import type { ParsedMessage } from "@/utils/types"; -import { - executiveSummarySchema, - userPersonaSchema, - emailBehaviorSchema, - responsePatternsSchema, - labelAnalysisSchema, - actionableRecommendationsSchema, -} from "./schemas"; +import type { EmailAccountWithAI } from "@/utils/llms/types"; +import { sleep } from "@/utils/sleep"; const logger = createScopedLogger("email-report-prompts"); +export type EmailSummary = { + summary: string; + sender: string; + subject: string; + category: string; +}; + +const executiveSummarySchema = z.object({ + userProfile: z.object({ + persona: z + .string() + .describe( + "1-5 word persona identification (e.g., 'Tech Startup Founder')", + ), + confidence: z + .number() + .min(0) + .max(100) + .describe("Confidence level in persona identification (0-100)"), + }), + topInsights: z + .array( + z.object({ + insight: z.string().describe("Key insight about user's email behavior"), + priority: z + .enum(["high", "medium", "low"]) + .describe("Priority level of this insight"), + icon: z.string().describe("Single emoji representing this insight"), + }), + ) + .describe("3-5 most important findings from the analysis"), + quickActions: z + .array( + z.object({ + action: z + .string() + .describe("Specific action the user can take immediately"), + difficulty: z + .enum(["easy", "medium", "hard"]) + .describe("How difficult this action is to implement"), + impact: z + .enum(["high", "medium", "low"]) + .describe("Expected impact of this action"), + }), + ) + .describe("4-6 immediate actions the user can take"), +}); + +const userPersonaSchema = z.object({ + professionalIdentity: z.object({ + persona: z.string().describe("Professional persona identification"), + supportingEvidence: z + .array(z.string()) + .describe("Evidence supporting this persona identification"), + }), + currentPriorities: z + .array(z.string()) + .describe("Current professional priorities based on email content"), +}); + +const emailBehaviorSchema = z.object({ + timingPatterns: z.object({ + peakHours: z.array(z.string()).describe("Peak email activity hours"), + responsePreference: z.string().describe("Preferred response timing"), + frequency: z.string().describe("Overall email frequency"), + }), + contentPreferences: z.object({ + preferred: z + .array(z.string()) + .describe("Types of emails user engages with"), + avoided: z + .array(z.string()) + .describe("Types of emails user typically ignores"), + }), + engagementTriggers: z + .array(z.string()) + .describe("What prompts user to take action on emails"), +}); + +const responsePatternsSchema = z.object({ + commonResponses: z.array( + z.object({ + pattern: z.string().describe("Description of the response pattern"), + example: z.string().describe("Example of this type of response"), + frequency: z + .number() + .describe("Percentage of responses using this pattern"), + triggers: z + .array(z.string()) + .describe("What types of emails trigger this response"), + }), + ), + suggestedTemplates: z.array( + z.object({ + templateName: z.string().describe("Name of the email template"), + template: z.string().describe("The actual email template text"), + useCase: z.string().describe("When to use this template"), + }), + ), + categoryOrganization: z.array( + z.object({ + category: z.string().describe("Email category name"), + description: z + .string() + .describe("What types of emails belong in this category"), + emailCount: z + .number() + .describe("Estimated number of emails in this category"), + priority: z + .enum(["high", "medium", "low"]) + .describe("Priority level for this category"), + }), + ), +}); + +const labelAnalysisSchema = z.object({ + optimizationSuggestions: z.array( + z.object({ + type: z + .enum(["create", "consolidate", "rename", "delete"]) + .describe("Type of optimization"), + suggestion: z.string().describe("Specific suggestion"), + reason: z.string().describe("Reason for this suggestion"), + impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), + }), + ), +}); + +const actionableRecommendationsSchema = z.object({ + immediateActions: z.array( + z.object({ + action: z.string().describe("Specific action to take"), + difficulty: z + .enum(["easy", "medium", "hard"]) + .describe("Implementation difficulty"), + impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), + timeRequired: z.string().describe("Time required (e.g., '5 minutes')"), + }), + ), + shortTermImprovements: z.array( + z.object({ + improvement: z.string().describe("Improvement to implement"), + timeline: z.string().describe("When to implement (e.g., 'This week')"), + expectedBenefit: z.string().describe("Expected benefit"), + }), + ), + longTermStrategy: z.array( + z.object({ + strategy: z.string().describe("Strategic initiative"), + description: z.string().describe("Detailed description"), + successMetrics: z.array(z.string()).describe("How to measure success"), + }), + ), +}); + export async function generateExecutiveSummary( emailSummaries: EmailSummary[], sentEmailSummaries: EmailSummary[], gmailLabels: gmail_v1.Schema$Label[], - userEmail: string, - emailAccount: EmailAccount & { - account: Account; - user: Pick; - }, + emailAccount: EmailAccountWithAI, ): Promise> { const system = `You are a professional persona identification expert. Your primary task is to accurately identify the user's professional role based on their email patterns. @@ -113,7 +256,7 @@ Generate: system, prompt, schema: executiveSummarySchema, - userEmail: userEmail, + userEmail: emailAccount.email, usageLabel: "email-report-executive-summary", }); @@ -122,11 +265,7 @@ Generate: export async function buildUserPersona( emailSummaries: EmailSummary[], - userEmail: string, - emailAccount: EmailAccount & { - account: Account; - user: Pick; - }, + emailAccount: EmailAccountWithAI, sentEmailSummaries?: EmailSummary[], gmailSignature?: string, gmailTemplates?: string[], @@ -176,7 +315,7 @@ Analyze the data and identify: system, prompt, schema: userPersonaSchema, - userEmail: userEmail, + userEmail: emailAccount.email, usageLabel: "email-report-user-persona", }); @@ -185,11 +324,7 @@ Analyze the data and identify: export async function analyzeEmailBehavior( emailSummaries: EmailSummary[], - userEmail: string, - emailAccount: EmailAccount & { - account: Account; - user: Pick; - }, + emailAccount: EmailAccountWithAI, sentEmailSummaries?: EmailSummary[], ): Promise> { const system = `You are an expert AI system that analyzes a user's email behavior to infer timing patterns, content preferences, and automation opportunities. @@ -223,7 +358,7 @@ Analyze the email patterns and identify: system, prompt, schema: emailBehaviorSchema, - userEmail: userEmail, + userEmail: emailAccount.email, usageLabel: "email-report-email-behavior", }); @@ -232,11 +367,7 @@ Analyze the email patterns and identify: export async function analyzeResponsePatterns( emailSummaries: EmailSummary[], - userEmail: string, - emailAccount: EmailAccount & { - account: Account; - user: Pick; - }, + emailAccount: EmailAccountWithAI, sentEmailSummaries?: EmailSummary[], ): Promise> { const system = `You are an expert email behavior analyst. Your task is to identify common response patterns and suggest email categorization and templates based on the user's email activity. @@ -284,7 +415,7 @@ Only suggest categories that are meaningful and provide clear organizational val system, prompt, schema: responsePatternsSchema, - userEmail: userEmail, + userEmail: emailAccount.email, usageLabel: "email-report-response-patterns", }); @@ -293,11 +424,7 @@ Only suggest categories that are meaningful and provide clear organizational val export async function analyzeLabelOptimization( emailSummaries: EmailSummary[], - userEmail: string, - emailAccount: EmailAccount & { - account: Account; - user: Pick; - }, + emailAccount: EmailAccountWithAI, gmailLabels: gmail_v1.Schema$Label[], ): Promise> { const system = `You are a Gmail organization expert. Analyze the user's current labels and email patterns to suggest specific optimizations that will improve their email organization and workflow efficiency. @@ -331,7 +458,7 @@ Each suggestion should include the reason and expected impact.`; system, prompt, schema: labelAnalysisSchema, - userEmail: userEmail, + userEmail: emailAccount.email, usageLabel: "email-report-label-analysis", }); @@ -340,13 +467,8 @@ Each suggestion should include the reason and expected impact.`; export async function generateActionableRecommendations( emailSummaries: EmailSummary[], - userEmail: string, - emailAccount: EmailAccount & { - account: Account; - user: Pick; - }, + emailAccount: EmailAccountWithAI, userPersona: z.infer, - emailBehavior: z.infer, ): Promise> { const system = `You are an email productivity consultant. Based on the comprehensive email analysis, create specific, actionable recommendations that the user can implement to improve their email workflow. @@ -372,7 +494,7 @@ Focus on practical, implementable solutions that improve email organization and system, prompt, schema: actionableRecommendationsSchema, - userEmail: userEmail, + userEmail: emailAccount.email, usageLabel: "email-report-actionable-recommendations", }); @@ -384,15 +506,10 @@ Focus on practical, implementable solutions that improve email organization and */ export async function summarizeEmails( emails: ParsedMessage[], - userEmail: string, - emailAccount: EmailAccount & { - account: Account; - user: Pick; - }, + emailAccount: EmailAccountWithAI, ): Promise { logger.info("summarizeEmails started", { emailsCount: emails.length, - userEmail, hasEmailAccount: !!emailAccount, }); @@ -404,7 +521,7 @@ export async function summarizeEmails( } if (!emailAccount) { - logger.warn("Email account not found for summarization", { userEmail }); + logger.warn("Email account not found for summarization"); return []; } @@ -426,7 +543,6 @@ export async function summarizeEmails( const batchResults = await processEmailBatch( batch, - userEmail, emailAccount, batchNumber, totalBatches, @@ -434,7 +550,7 @@ export async function summarizeEmails( results.push(...batchResults); if (i + batchSize < emails.length) { - await new Promise((resolve) => setTimeout(resolve, 1000)); + await sleep(1000); } } @@ -448,11 +564,7 @@ export async function summarizeEmails( async function processEmailBatch( emails: ParsedMessage[], - userEmail: string, - emailAccount: EmailAccount & { - account: Account; - user: Pick; - }, + emailAccount: EmailAccountWithAI, batchNumber: number, totalBatches: number, ): Promise { @@ -511,51 +623,39 @@ Return the analysis as a JSON array with objects containing: summary, sender, su batchNumber, promptLength: prompt.length, systemLength: system.length, - userEmail, }); - try { - logger.info("processEmailBatch: calling chatCompletionObject", { - batchNumber, - userAiProvider: emailAccount.user?.aiProvider, - userAiModel: emailAccount.user?.aiModel, - hasUserAiApiKey: !!emailAccount.user?.aiApiKey, - }); + logger.info("processEmailBatch: calling chatCompletionObject", { + batchNumber, + userAiProvider: emailAccount.user?.aiProvider, + userAiModel: emailAccount.user?.aiModel, + hasUserAiApiKey: !!emailAccount.user?.aiApiKey, + }); - const result = await chatCompletionObject({ - userAi: emailAccount.user, - system, - prompt, - schema: z.array( - z.object({ - summary: z.string().describe("Brief summary of the email content"), - sender: z.string().describe("Email sender"), - subject: z.string().describe("Email subject"), - category: z - .string() - .describe( - "Category of the email (work, personal, marketing, etc.)", - ), - }), - ), - userEmail: userEmail, - usageLabel: "email-report-summary-generation", - }); + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: z.array( + z.object({ + summary: z.string().describe("Brief summary of the email content"), + sender: z.string().describe("Email sender"), + subject: z.string().describe("Email subject"), + category: z + .string() + .describe("Category of the email (work, personal, marketing, etc.)"), + }), + ), + userEmail: emailAccount.email, + usageLabel: "email-report-summary-generation", + }); - logger.info("processEmailBatch: chatCompletionObject completed", { - batchNumber, - resultType: typeof result, - hasObject: !!result.object, - objectLength: result.object?.length || 0, - }); + logger.info("processEmailBatch: chatCompletionObject completed", { + batchNumber, + resultType: typeof result, + hasObject: !!result.object, + objectLength: result.object?.length || 0, + }); - return result.object; - } catch (error) { - logger.error("processEmailBatch: failed to summarize batch", { - batchNumber, - error, - userEmail, - }); - return []; - } + return result.object; } diff --git a/apps/web/utils/gmail/message.ts b/apps/web/utils/gmail/message.ts index 54c2ea0fc4..bfa0bc8737 100644 --- a/apps/web/utils/gmail/message.ts +++ b/apps/web/utils/gmail/message.ts @@ -356,7 +356,7 @@ export async function getMessagesLargeBatch({ // Wait 2 seconds between batches, except after the last batch if (i + batchSize < messageIds.length) { - await new Promise((resolve) => setTimeout(resolve, 2000)); + await sleep(2000); } } From 53219703cc641fb46eba8df258b977bf5cffb473 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:39:48 +0300 Subject: [PATCH 08/20] logs --- .cursor/rules/llm.mdc | 2 +- apps/web/utils/ai/knowledge/writing-style.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.cursor/rules/llm.mdc b/.cursor/rules/llm.mdc index 79f4d1d82a..f05b3831da 100644 --- a/.cursor/rules/llm.mdc +++ b/.cursor/rules/llm.mdc @@ -83,7 +83,7 @@ ${emailAccount.about ? `${emailAccount.about}` : ""}`; }); // 9. Log outputs - logger.trace("Output", { result }); + logger.trace("Output", result.object); // 10. Return validated result return result.object; diff --git a/apps/web/utils/ai/knowledge/writing-style.ts b/apps/web/utils/ai/knowledge/writing-style.ts index 723a7100a7..ff27ea212d 100644 --- a/apps/web/utils/ai/knowledge/writing-style.ts +++ b/apps/web/utils/ai/knowledge/writing-style.ts @@ -87,7 +87,7 @@ ${ usageLabel: "Writing Style Analysis", }); - logger.trace("Output", { result }); + logger.trace("Output", result.object); return result.object; } From 0bd02910cd9029d8d2f7fca217ee4c2edce8068a Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:42:59 +0300 Subject: [PATCH 09/20] catch errors --- apps/web/utils/actions/report.ts | 64 ++++++++++++++++---------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/apps/web/utils/actions/report.ts b/apps/web/utils/actions/report.ts index 27829ab9d1..e4ce97cc86 100644 --- a/apps/web/utils/actions/report.ts +++ b/apps/web/utils/actions/report.ts @@ -161,44 +161,42 @@ async function getEmailReportData({ const gmailSignature = await fetchGmailSignature(gmail); const gmailTemplates = await fetchGmailTemplates(gmail); - const executiveSummary = await generateExecutiveSummary( - receivedSummaries, - sentSummaries, - gmailLabels, - emailAccount, - ); - - const userPersona = await buildUserPersona( - receivedSummaries, - emailAccount, - sentSummaries, - gmailSignature, - gmailTemplates, - ); - - const emailBehavior = await analyzeEmailBehavior( - receivedSummaries, - emailAccount, - sentSummaries, - ); - - const responsePatterns = await analyzeResponsePatterns( - receivedSummaries, - emailAccount, - sentSummaries, - ); - - const labelAnalysis = await analyzeLabelOptimization( - receivedSummaries, - emailAccount, - gmailLabels, - ); + const [ + executiveSummary, + userPersona, + emailBehavior, + responsePatterns, + labelAnalysis, + ] = await Promise.all([ + generateExecutiveSummary( + receivedSummaries, + sentSummaries, + gmailLabels, + emailAccount, + ), + buildUserPersona( + receivedSummaries, + emailAccount, + sentSummaries, + gmailSignature, + gmailTemplates, + ), + analyzeEmailBehavior(receivedSummaries, emailAccount, sentSummaries), + analyzeResponsePatterns(receivedSummaries, emailAccount, sentSummaries), + analyzeLabelOptimization(receivedSummaries, emailAccount, gmailLabels), + ]).catch((error) => { + logger.error("Error generating report", { error }); + throw error; + }); const actionableRecommendations = await generateActionableRecommendations( receivedSummaries, emailAccount, userPersona, - ); + ).catch((error) => { + logger.error("Error generating actionable recommendations", { error }); + throw error; + }); return { executiveSummary, From 4dba4d1faedf3f29002224c7b6dc9ed4544b833c Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:46:49 +0300 Subject: [PATCH 10/20] catch errors better --- apps/web/utils/actions/report.ts | 54 +++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/apps/web/utils/actions/report.ts b/apps/web/utils/actions/report.ts index e4ce97cc86..02ce82d757 100644 --- a/apps/web/utils/actions/report.ts +++ b/apps/web/utils/actions/report.ts @@ -173,30 +173,48 @@ async function getEmailReportData({ sentSummaries, gmailLabels, emailAccount, - ), + ).catch((error) => { + logger.error("Error generating executive summary", { error }); + }), buildUserPersona( receivedSummaries, emailAccount, sentSummaries, gmailSignature, gmailTemplates, + ).catch((error) => { + logger.error("Error generating user persona", { error }); + }), + analyzeEmailBehavior(receivedSummaries, emailAccount, sentSummaries).catch( + (error) => { + logger.error("Error generating email behavior", { error }); + }, ), - analyzeEmailBehavior(receivedSummaries, emailAccount, sentSummaries), - analyzeResponsePatterns(receivedSummaries, emailAccount, sentSummaries), - analyzeLabelOptimization(receivedSummaries, emailAccount, gmailLabels), - ]).catch((error) => { - logger.error("Error generating report", { error }); - throw error; - }); - - const actionableRecommendations = await generateActionableRecommendations( - receivedSummaries, - emailAccount, - userPersona, - ).catch((error) => { - logger.error("Error generating actionable recommendations", { error }); - throw error; - }); + analyzeResponsePatterns( + receivedSummaries, + emailAccount, + sentSummaries, + ).catch((error) => { + logger.error("Error generating response patterns", { error }); + }), + analyzeLabelOptimization( + receivedSummaries, + emailAccount, + gmailLabels, + ).catch((error) => { + logger.error("Error generating label optimization", { error }); + }), + ]); + + const actionableRecommendations = userPersona + ? await generateActionableRecommendations( + receivedSummaries, + emailAccount, + userPersona, + ).catch((error) => { + logger.error("Error generating actionable recommendations", { error }); + }) + : null; return { executiveSummary, @@ -221,7 +239,7 @@ async function getEmailReportData({ color: label.color || null, type: label.type, })), - optimizationSuggestions: labelAnalysis.optimizationSuggestions, + optimizationSuggestions: labelAnalysis?.optimizationSuggestions || [], }, actionableRecommendations, }; From ee672e12d4297cd2921632e602f78605cb95a77a Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:51:31 +0300 Subject: [PATCH 11/20] cut down logs --- .../[emailAccountId]/debug/report/page.tsx | 40 ++++++----- apps/web/utils/ai/report/prompts.ts | 70 ++----------------- 2 files changed, 25 insertions(+), 85 deletions(-) diff --git a/apps/web/app/(app)/[emailAccountId]/debug/report/page.tsx b/apps/web/app/(app)/[emailAccountId]/debug/report/page.tsx index 31a3bb1852..aea810fe1f 100644 --- a/apps/web/app/(app)/[emailAccountId]/debug/report/page.tsx +++ b/apps/web/app/(app)/[emailAccountId]/debug/report/page.tsx @@ -60,7 +60,9 @@ export default function EmailReportPage() { - +

- {report.executiveSummary.userProfile.persona} + {report.executiveSummary?.userProfile.persona}

Confidence:{" "} - {report.executiveSummary.userProfile.confidence}% + {report.executiveSummary?.userProfile.confidence}%

@@ -131,7 +133,7 @@ export default function EmailReportPage() { Quick Actions
- {report.executiveSummary.quickActions + {report.executiveSummary?.quickActions .slice(0, 3) .map((action, index) => (
- {report.executiveSummary.topInsights.map( + {report.executiveSummary?.topInsights.map( (insight, index) => (

- {report.userPersona.professionalIdentity.persona} + {report.userPersona?.professionalIdentity.persona}

- {report.userPersona.professionalIdentity.supportingEvidence.map( + {report.userPersona?.professionalIdentity.supportingEvidence.map( (evidence, index) => (

- {report.userPersona.currentPriorities.map( + {report.userPersona?.currentPriorities.map( (priority, index) => ( {priority} @@ -252,20 +254,20 @@ export default function EmailReportPage() {

Peak hours:{" "} - {report.emailBehavior.timingPatterns.peakHours.join( + {report.emailBehavior?.timingPatterns.peakHours.join( ", ", )}

Response preference:{" "} { - report.emailBehavior.timingPatterns + report.emailBehavior?.timingPatterns .responsePreference }

Frequency:{" "} - {report.emailBehavior.timingPatterns.frequency} + {report.emailBehavior?.timingPatterns.frequency}

@@ -274,13 +276,13 @@ export default function EmailReportPage() {

Preferred:{" "} - {report.emailBehavior.contentPreferences.preferred.join( + {report.emailBehavior?.contentPreferences.preferred.join( ", ", )}

Avoided:{" "} - {report.emailBehavior.contentPreferences.avoided.join( + {report.emailBehavior?.contentPreferences.avoided.join( ", ", )}

@@ -290,7 +292,7 @@ export default function EmailReportPage() { Engagement Triggers
- {report.emailBehavior.engagementTriggers.map( + {report.emailBehavior?.engagementTriggers.map( (trigger, index) => (

• {trigger} @@ -317,7 +319,7 @@ export default function EmailReportPage() { Common Response Patterns

- {report.responsePatterns.commonResponses.map( + {report.responsePatterns?.commonResponses.map( (response, index) => (
- {report.responsePatterns.categoryOrganization.map( + {report.responsePatterns?.categoryOrganization.map( (category, index) => (
- {report.actionableRecommendations.immediateActions.map( + {report.actionableRecommendations?.immediateActions.map( (action, index) => (
- {report.actionableRecommendations.shortTermImprovements.map( + {report.actionableRecommendations?.shortTermImprovements.map( (improvement, index) => (
- {report.actionableRecommendations.longTermStrategy.map( + {report.actionableRecommendations?.longTermStrategy.map( (strategy, index) => (
{ - logger.info("summarizeEmails started", { - emailsCount: emails.length, - hasEmailAccount: !!emailAccount, - }); - if (emails.length === 0) { - logger.info( - "summarizeEmails: no emails to summarize, returning empty array", - ); - return []; - } - - if (!emailAccount) { - logger.warn("Email account not found for summarization"); + logger.warn("No emails to summarize, returning empty array"); return []; } @@ -533,14 +521,6 @@ export async function summarizeEmails( const batchNumber = Math.floor(i / batchSize) + 1; const totalBatches = Math.ceil(emails.length / batchSize); - logger.info("summarizeEmails: processing batch", { - batchNumber, - totalBatches, - batchSize: batch.length, - startIndex: i, - endIndex: Math.min(i + batchSize, emails.length), - }); - const batchResults = await processEmailBatch( batch, emailAccount, @@ -554,11 +534,6 @@ export async function summarizeEmails( } } - logger.info("summarizeEmails: all batches completed", { - totalResults: results.length, - originalEmailCount: emails.length, - }); - return results; } @@ -568,35 +543,14 @@ async function processEmailBatch( batchNumber: number, totalBatches: number, ): Promise { - logger.info("processEmailBatch: preparing email texts", { - batchNumber, - totalBatches, - emailsCount: emails.length, - }); - - const emailTexts = emails.map((email, index) => { + const emailTexts = emails.map((email) => { const sender = email.headers?.from || "Unknown"; const subject = email.headers?.subject || "No subject"; const content = email.textPlain || email.textHtml || ""; - logger.info("processEmailBatch: processing email", { - batchNumber, - index, - emailId: email.id, - sender, - subjectLength: subject.length, - contentLength: content.length, - }); - return `From: ${sender}\nSubject: ${subject}\nContent: ${content.substring(0, 1000)}`; }); - logger.info("processEmailBatch: email texts prepared", { - batchNumber, - emailTextsCount: emailTexts.length, - totalTextLength: emailTexts.join("\n\n---\n\n").length, - }); - const system = `You are an assistant that processes user emails to extract their core meaning for later analysis. For each email, write a **factual summary of 3–5 sentences** that clearly describes: @@ -619,18 +573,7 @@ ${emailTexts.join("\n\n---\n\n")} Return the analysis as a JSON array with objects containing: summary, sender, subject, category.`; - logger.info("processEmailBatch: about to call chatCompletionObject", { - batchNumber, - promptLength: prompt.length, - systemLength: system.length, - }); - - logger.info("processEmailBatch: calling chatCompletionObject", { - batchNumber, - userAiProvider: emailAccount.user?.aiProvider, - userAiModel: emailAccount.user?.aiModel, - hasUserAiApiKey: !!emailAccount.user?.aiApiKey, - }); + logger.trace("Input", { system, prompt }); const result = await chatCompletionObject({ userAi: emailAccount.user, @@ -650,12 +593,7 @@ Return the analysis as a JSON array with objects containing: summary, sender, su usageLabel: "email-report-summary-generation", }); - logger.info("processEmailBatch: chatCompletionObject completed", { - batchNumber, - resultType: typeof result, - hasObject: !!result.object, - objectLength: result.object?.length || 0, - }); + logger.trace("processEmailBatch: result", { response: result.object }); return result.object; } From a9d1da5e3f1296898278940dfdd473418f0452a3 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:53:41 +0300 Subject: [PATCH 12/20] remove logs --- apps/web/utils/ai/report/fetch.ts | 62 +------------------------------ 1 file changed, 2 insertions(+), 60 deletions(-) diff --git a/apps/web/utils/ai/report/fetch.ts b/apps/web/utils/ai/report/fetch.ts index bed5aa600e..42fabd4256 100644 --- a/apps/web/utils/ai/report/fetch.ts +++ b/apps/web/utils/ai/report/fetch.ts @@ -36,37 +36,17 @@ async function fetchEmailsByQuery( while (emails.length < count && retryCount < maxRetries) { try { - logger.info("fetchEmailsByQuery: calling getMessages", { - query, - maxResults: Math.min(100, count - emails.length), - hasPageToken: !!nextPageToken, - currentEmailsCount: emails.length, - retryCount, - }); - const response = await getMessages(gmail, { query: query || undefined, maxResults: Math.min(100, count - emails.length), pageToken: nextPageToken, }); - logger.info("fetchEmailsByQuery: getMessages response received", { - hasMessages: !!response.messages, - messagesCount: response.messages?.length || 0, - hasNextPageToken: !!response.nextPageToken, - responseKeys: Object.keys(response), - }); - if (!response.messages || response.messages.length === 0) { - logger.info("fetchEmailsByQuery: no messages found, breaking"); + logger.warn("No messages found, breaking"); break; } - logger.info("fetchEmailsByQuery: starting to fetch individual messages", { - messageIdsCount: response.messages?.length || 0, - messageIds: response.messages?.map((m: any) => m.id).slice(0, 5) || [], - }); - const messagePromises = (response.messages || []).map( async (message: any, index: number) => { if (!message.id) { @@ -77,41 +57,15 @@ async function fetchEmailsByQuery( return null; } - logger.info("fetchEmailsByQuery: fetching individual message", { - messageId: message.id, - index, - totalMessages: response.messages?.length || 0, - }); - for (let i = 0; i < 3; i++) { try { - logger.info("fetchEmailsByQuery: calling getMessage", { - messageId: message.id, - attempt: i + 1, - format: "full", - }); - const messageWithPayload = await getMessage( message.id, gmail, "full", ); - logger.info("fetchEmailsByQuery: getMessage successful", { - messageId: message.id, - hasPayload: !!messageWithPayload, - payloadKeys: messageWithPayload - ? Object.keys(messageWithPayload) - : [], - }); - const parsedMessage = parseMessage(messageWithPayload); - logger.info("fetchEmailsByQuery: message parsed successfully", { - messageId: message.id, - hasHeaders: !!parsedMessage.headers, - hasTextPlain: !!parsedMessage.textPlain, - hasTextHtml: !!parsedMessage.textHtml, - }); return parsedMessage; } catch (error) { @@ -128,19 +82,13 @@ async function fetchEmailsByQuery( ); return null; } - await new Promise((resolve) => - setTimeout(resolve, 1000 * (i + 1)), - ); + await sleep(1000 * (i + 1)); } } return null; }, ); - logger.info("fetchEmailsByQuery: waiting for all message promises", { - promisesCount: messagePromises.length, - }); - const messages = await Promise.all(messagePromises); const validMessages = messages.filter((msg) => msg !== null); @@ -154,16 +102,10 @@ async function fetchEmailsByQuery( nextPageToken = response.nextPageToken || undefined; if (!nextPageToken) { - logger.info("fetchEmailsByQuery: no next page token, breaking"); break; } retryCount = 0; - logger.info("fetchEmailsByQuery: successful iteration completed", { - currentEmailsCount: emails.length, - targetCount: count, - hasNextPageToken: !!nextPageToken, - }); } catch (error) { retryCount++; logger.error("fetchEmailsByQuery: main loop error", { From 361422daf68f7f83fc073b4137d2969c11cdd9cd Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:02:04 +0300 Subject: [PATCH 13/20] clean up type --- apps/web/utils/ai/report/prompts.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/apps/web/utils/ai/report/prompts.ts b/apps/web/utils/ai/report/prompts.ts index d5801f857d..7b955a7c50 100644 --- a/apps/web/utils/ai/report/prompts.ts +++ b/apps/web/utils/ai/report/prompts.ts @@ -8,12 +8,15 @@ import { sleep } from "@/utils/sleep"; const logger = createScopedLogger("email-report-prompts"); -export type EmailSummary = { - summary: string; - sender: string; - subject: string; - category: string; -}; +const emailSummarySchema = z.object({ + summary: z.string().describe("Brief summary of the email content"), + sender: z.string().describe("Email sender"), + subject: z.string().describe("Email subject"), + category: z + .string() + .describe("Category of the email (work, personal, marketing, etc.)"), +}); +export type EmailSummary = z.infer; const executiveSummarySchema = z.object({ userProfile: z.object({ @@ -579,16 +582,7 @@ Return the analysis as a JSON array with objects containing: summary, sender, su userAi: emailAccount.user, system, prompt, - schema: z.array( - z.object({ - summary: z.string().describe("Brief summary of the email content"), - sender: z.string().describe("Email sender"), - subject: z.string().describe("Email subject"), - category: z - .string() - .describe("Category of the email (work, personal, marketing, etc.)"), - }), - ), + schema: z.array(emailSummarySchema), userEmail: emailAccount.email, usageLabel: "email-report-summary-generation", }); From e250fca647496da6cb895ca5015a1786b37bd504 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:22:48 +0300 Subject: [PATCH 14/20] use email for llm converter --- apps/web/utils/actions/report.ts | 22 ++++++++++++++-------- apps/web/utils/ai/report/fetch.ts | 14 ++------------ apps/web/utils/ai/report/prompts.ts | 19 ++++++------------- apps/web/utils/get-email-from-message.ts | 1 + apps/web/utils/types.ts | 1 + 5 files changed, 24 insertions(+), 33 deletions(-) diff --git a/apps/web/utils/actions/report.ts b/apps/web/utils/actions/report.ts index 02ce82d757..09812282a5 100644 --- a/apps/web/utils/actions/report.ts +++ b/apps/web/utils/actions/report.ts @@ -1,5 +1,7 @@ "use server"; +import type { gmail_v1 } from "@googleapis/gmail"; +import { z } from "zod"; import { fetchEmailsForReport, fetchGmailTemplates, @@ -13,12 +15,11 @@ import { generateActionableRecommendations, summarizeEmails, } from "@/utils/ai/report/prompts"; -import type { gmail_v1 } from "@googleapis/gmail"; import { createScopedLogger } from "@/utils/logger"; import { actionClient } from "@/utils/actions/safe-action"; -import { z } from "zod"; import { getEmailAccountWithAi } from "@/utils/user/get"; import { getGmailClientForEmail } from "@/utils/account"; +import { getEmailForLLM } from "@/utils/get-email-from-message"; const logger = createScopedLogger("actions/report"); @@ -146,12 +147,17 @@ async function getEmailReportData({ throw new Error("Email account not found"); } - const emailData = await fetchEmailsForReport({ emailAccount }); - - const { receivedEmails, sentEmails, totalReceived, totalSent } = emailData; - - const receivedSummaries = await summarizeEmails(receivedEmails, emailAccount); - const sentSummaries = await summarizeEmails(sentEmails, emailAccount); + const { receivedEmails, sentEmails, totalReceived, totalSent } = + await fetchEmailsForReport({ emailAccount }); + + const receivedSummaries = await summarizeEmails( + receivedEmails.map((message) => getEmailForLLM(message)), + emailAccount, + ); + const sentSummaries = await summarizeEmails( + sentEmails.map((message) => getEmailForLLM(message)), + emailAccount, + ); const gmail = await getGmailClientForEmail({ emailAccountId: emailAccount.id, diff --git a/apps/web/utils/ai/report/fetch.ts b/apps/web/utils/ai/report/fetch.ts index 42fabd4256..2712e9fc5e 100644 --- a/apps/web/utils/ai/report/fetch.ts +++ b/apps/web/utils/ai/report/fetch.ts @@ -136,18 +136,11 @@ async function fetchEmailsByQuery( return emails; } -export interface EmailFetchResult { - receivedEmails: ParsedMessage[]; - sentEmails: ParsedMessage[]; - totalReceived: number; - totalSent: number; -} - export async function fetchEmailsForReport({ emailAccount, }: { emailAccount: EmailAccountWithAI; -}): Promise { +}) { logger.info("fetchEmailsForReport started", { emailAccountId: emailAccount.id, }); @@ -198,7 +191,6 @@ async function fetchReceivedEmails( logger.error(`Error fetching emails from ${source.name}`, { error, query: source.query, - maxResults: targetCount - emails.length, }); } } @@ -215,9 +207,7 @@ async function fetchSentEmails( return emails; } catch (error) { - logger.error("Error fetching sent emails", { - error, - }); + logger.error("Error fetching sent emails", { error }); return []; } } diff --git a/apps/web/utils/ai/report/prompts.ts b/apps/web/utils/ai/report/prompts.ts index 7b955a7c50..ba6447add5 100644 --- a/apps/web/utils/ai/report/prompts.ts +++ b/apps/web/utils/ai/report/prompts.ts @@ -2,9 +2,10 @@ import { z } from "zod"; import { createScopedLogger } from "@/utils/logger"; import { chatCompletionObject } from "@/utils/llms"; import type { gmail_v1 } from "@googleapis/gmail"; -import type { ParsedMessage } from "@/utils/types"; +import type { EmailForLLM } from "@/utils/types"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import { sleep } from "@/utils/sleep"; +import { stringifyEmail } from "@/utils/stringify-email"; const logger = createScopedLogger("email-report-prompts"); @@ -508,7 +509,7 @@ Focus on practical, implementable solutions that improve email organization and * Summarize emails for analysis */ export async function summarizeEmails( - emails: ParsedMessage[], + emails: EmailForLLM[], emailAccount: EmailAccountWithAI, ): Promise { if (emails.length === 0) { @@ -541,19 +542,11 @@ export async function summarizeEmails( } async function processEmailBatch( - emails: ParsedMessage[], + emails: EmailForLLM[], emailAccount: EmailAccountWithAI, batchNumber: number, totalBatches: number, ): Promise { - const emailTexts = emails.map((email) => { - const sender = email.headers?.from || "Unknown"; - const subject = email.headers?.subject || "No subject"; - const content = email.textPlain || email.textHtml || ""; - - return `From: ${sender}\nSubject: ${subject}\nContent: ${content.substring(0, 1000)}`; - }); - const system = `You are an assistant that processes user emails to extract their core meaning for later analysis. For each email, write a **factual summary of 3–5 sentences** that clearly describes: @@ -572,9 +565,9 @@ For each email, write a **factual summary of 3–5 sentences** that clearly desc const prompt = ` **Input Emails (Batch ${batchNumber} of ${totalBatches}):** -${emailTexts.join("\n\n---\n\n")} +${emails.map((email) => `${stringifyEmail(email, 2000)}`).join("\n")} -Return the analysis as a JSON array with objects containing: summary, sender, subject, category.`; +Return the analysis as a JSON array of objects.`; logger.trace("Input", { system, prompt }); diff --git a/apps/web/utils/get-email-from-message.ts b/apps/web/utils/get-email-from-message.ts index 498cad4cbb..7e9c9944fe 100644 --- a/apps/web/utils/get-email-from-message.ts +++ b/apps/web/utils/get-email-from-message.ts @@ -2,6 +2,7 @@ import type { ParsedMessage, EmailForLLM } from "@/utils/types"; import { emailToContent, type EmailToContentOptions } from "@/utils/mail"; import { internalDateToDate } from "@/utils/date"; +// Convert a ParsedMessage to an EmailForLLM export function getEmailForLLM( message: ParsedMessage, contentOptions?: EmailToContentOptions, diff --git a/apps/web/utils/types.ts b/apps/web/utils/types.ts index 4afc12efc1..ee3663ef69 100644 --- a/apps/web/utils/types.ts +++ b/apps/web/utils/types.ts @@ -113,6 +113,7 @@ export interface ParsedMessageHeaders { "list-unsubscribe"?: string; } +// Note: use `getEmailForLLM(message)` to convert a `ParsedMessage` to an `EmailForLLM` export type EmailForLLM = { id: string; from: string; From 831b94aa52ad858ae6a49acf6083591dda6bbcad Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:36:27 +0300 Subject: [PATCH 15/20] split up ai report calls into many files --- apps/web/utils/actions/report.ts | 32 +- .../utils/ai/report/analyze-email-behavior.ts | 66 ++ .../ai/report/analyze-label-optimization.ts | 61 ++ .../web/utils/ai/report/build-user-persona.ts | 76 +++ .../generate-actionable-recommendations.ts | 68 ++ .../ai/report/generate-executive-summary.ts | 146 +++++ apps/web/utils/ai/report/prompts.ts | 586 ------------------ apps/web/utils/ai/report/response-patterns.ts | 97 +++ apps/web/utils/ai/report/summarize-emails.ts | 96 +++ 9 files changed, 625 insertions(+), 603 deletions(-) create mode 100644 apps/web/utils/ai/report/analyze-email-behavior.ts create mode 100644 apps/web/utils/ai/report/analyze-label-optimization.ts create mode 100644 apps/web/utils/ai/report/build-user-persona.ts create mode 100644 apps/web/utils/ai/report/generate-actionable-recommendations.ts create mode 100644 apps/web/utils/ai/report/generate-executive-summary.ts delete mode 100644 apps/web/utils/ai/report/prompts.ts create mode 100644 apps/web/utils/ai/report/response-patterns.ts create mode 100644 apps/web/utils/ai/report/summarize-emails.ts diff --git a/apps/web/utils/actions/report.ts b/apps/web/utils/actions/report.ts index 09812282a5..c430be5dac 100644 --- a/apps/web/utils/actions/report.ts +++ b/apps/web/utils/actions/report.ts @@ -6,15 +6,13 @@ import { fetchEmailsForReport, fetchGmailTemplates, } from "@/utils/ai/report/fetch"; -import { - generateExecutiveSummary, - buildUserPersona, - analyzeEmailBehavior, - analyzeResponsePatterns, - analyzeLabelOptimization, - generateActionableRecommendations, - summarizeEmails, -} from "@/utils/ai/report/prompts"; +import { aiSummarizeEmails } from "@/utils/ai/report/summarize-emails"; +import { aiGenerateExecutiveSummary } from "@/utils/ai/report/generate-executive-summary"; +import { aiBuildUserPersona } from "@/utils/ai/report/build-user-persona"; +import { aiAnalyzeEmailBehavior } from "@/utils/ai/report/analyze-email-behavior"; +import { aiAnalyzeResponsePatterns } from "@/utils/ai/report/response-patterns"; +import { aiAnalyzeLabelOptimization } from "@/utils/ai/report/analyze-label-optimization"; +import { aiGenerateActionableRecommendations } from "@/utils/ai/report/generate-actionable-recommendations"; import { createScopedLogger } from "@/utils/logger"; import { actionClient } from "@/utils/actions/safe-action"; import { getEmailAccountWithAi } from "@/utils/user/get"; @@ -150,11 +148,11 @@ async function getEmailReportData({ const { receivedEmails, sentEmails, totalReceived, totalSent } = await fetchEmailsForReport({ emailAccount }); - const receivedSummaries = await summarizeEmails( + const receivedSummaries = await aiSummarizeEmails( receivedEmails.map((message) => getEmailForLLM(message)), emailAccount, ); - const sentSummaries = await summarizeEmails( + const sentSummaries = await aiSummarizeEmails( sentEmails.map((message) => getEmailForLLM(message)), emailAccount, ); @@ -174,7 +172,7 @@ async function getEmailReportData({ responsePatterns, labelAnalysis, ] = await Promise.all([ - generateExecutiveSummary( + aiGenerateExecutiveSummary( receivedSummaries, sentSummaries, gmailLabels, @@ -182,7 +180,7 @@ async function getEmailReportData({ ).catch((error) => { logger.error("Error generating executive summary", { error }); }), - buildUserPersona( + aiBuildUserPersona( receivedSummaries, emailAccount, sentSummaries, @@ -191,19 +189,19 @@ async function getEmailReportData({ ).catch((error) => { logger.error("Error generating user persona", { error }); }), - analyzeEmailBehavior(receivedSummaries, emailAccount, sentSummaries).catch( + aiAnalyzeEmailBehavior(receivedSummaries, emailAccount, sentSummaries).catch( (error) => { logger.error("Error generating email behavior", { error }); }, ), - analyzeResponsePatterns( + aiAnalyzeResponsePatterns( receivedSummaries, emailAccount, sentSummaries, ).catch((error) => { logger.error("Error generating response patterns", { error }); }), - analyzeLabelOptimization( + aiAnalyzeLabelOptimization( receivedSummaries, emailAccount, gmailLabels, @@ -213,7 +211,7 @@ async function getEmailReportData({ ]); const actionableRecommendations = userPersona - ? await generateActionableRecommendations( + ? await aiGenerateActionableRecommendations( receivedSummaries, emailAccount, userPersona, diff --git a/apps/web/utils/ai/report/analyze-email-behavior.ts b/apps/web/utils/ai/report/analyze-email-behavior.ts new file mode 100644 index 0000000000..b88c617456 --- /dev/null +++ b/apps/web/utils/ai/report/analyze-email-behavior.ts @@ -0,0 +1,66 @@ +import { z } from "zod"; +import { chatCompletionObject } from "@/utils/llms"; +import type { EmailAccountWithAI } from "@/utils/llms/types"; +import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; + +const emailBehaviorSchema = z.object({ + timingPatterns: z.object({ + peakHours: z.array(z.string()).describe("Peak email activity hours"), + responsePreference: z.string().describe("Preferred response timing"), + frequency: z.string().describe("Overall email frequency"), + }), + contentPreferences: z.object({ + preferred: z + .array(z.string()) + .describe("Types of emails user engages with"), + avoided: z + .array(z.string()) + .describe("Types of emails user typically ignores"), + }), + engagementTriggers: z + .array(z.string()) + .describe("What prompts user to take action on emails"), +}); + +export async function aiAnalyzeEmailBehavior( + emailSummaries: EmailSummary[], + emailAccount: EmailAccountWithAI, + sentEmailSummaries?: EmailSummary[], +) { + const system = `You are an expert AI system that analyzes a user's email behavior to infer timing patterns, content preferences, and automation opportunities. + +Focus on identifying patterns that can be automated and providing specific, actionable automation rules that would save time and improve email management efficiency.`; + + const prompt = `### Email Analysis Data + +**Received Emails:** +${emailSummaries.map((email, i) => `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`).join("\n")} + +${ + sentEmailSummaries && sentEmailSummaries.length > 0 + ? ` +**Sent Emails:** +${sentEmailSummaries.map((email, i) => `${i + 1}. To: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`).join("\n")} +` + : "" +} + +--- + +Analyze the email patterns and identify: +1. Timing patterns (when emails are most active, response preferences) +2. Content preferences (what types of emails they engage with vs avoid) +3. Engagement triggers (what prompts them to take action) +4. Specific automation opportunities with estimated time savings`; + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: emailBehaviorSchema, + userEmail: emailAccount.email, + usageLabel: "email-report-email-behavior", + }); + + return result.object; +} diff --git a/apps/web/utils/ai/report/analyze-label-optimization.ts b/apps/web/utils/ai/report/analyze-label-optimization.ts new file mode 100644 index 0000000000..3026cb498d --- /dev/null +++ b/apps/web/utils/ai/report/analyze-label-optimization.ts @@ -0,0 +1,61 @@ +import { z } from "zod"; +import { chatCompletionObject } from "@/utils/llms"; +import type { gmail_v1 } from "@googleapis/gmail"; +import type { EmailAccountWithAI } from "@/utils/llms/types"; +import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; + +const labelAnalysisSchema = z.object({ + optimizationSuggestions: z.array( + z.object({ + type: z + .enum(["create", "consolidate", "rename", "delete"]) + .describe("Type of optimization"), + suggestion: z.string().describe("Specific suggestion"), + reason: z.string().describe("Reason for this suggestion"), + impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), + }), + ), +}); + +export async function aiAnalyzeLabelOptimization( + emailSummaries: EmailSummary[], + emailAccount: EmailAccountWithAI, + gmailLabels: gmail_v1.Schema$Label[], +): Promise> { + const system = `You are a Gmail organization expert. Analyze the user's current labels and email patterns to suggest specific optimizations that will improve their email organization and workflow efficiency. + +Focus on practical suggestions that will reduce email management time and improve organization.`; + + const prompt = `### Current Gmail Labels +${gmailLabels.map((label) => `- ${label.name}: ${label.messagesTotal || 0} emails, ${label.messagesUnread || 0} unread`).join("\n")} + +### Email Content Analysis +${emailSummaries + .slice(0, 30) + .map( + (email, i) => + `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, + ) + .join("\n")} + +--- + +Based on the current labels and email content, suggest specific optimizations: +1. Labels to create based on email patterns +2. Labels to consolidate that have overlapping purposes +3. Labels to rename for better clarity +4. Labels to delete that are unused or redundant + +Each suggestion should include the reason and expected impact.`; + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: labelAnalysisSchema, + userEmail: emailAccount.email, + usageLabel: "email-report-label-analysis", + }); + + return result.object; +} diff --git a/apps/web/utils/ai/report/build-user-persona.ts b/apps/web/utils/ai/report/build-user-persona.ts new file mode 100644 index 0000000000..b1aea2353d --- /dev/null +++ b/apps/web/utils/ai/report/build-user-persona.ts @@ -0,0 +1,76 @@ +import { z } from "zod"; +import { chatCompletionObject } from "@/utils/llms"; +import type { EmailAccountWithAI } from "@/utils/llms/types"; +import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; + +const userPersonaSchema = z.object({ + professionalIdentity: z.object({ + persona: z.string().describe("Professional persona identification"), + supportingEvidence: z + .array(z.string()) + .describe("Evidence supporting this persona identification"), + }), + currentPriorities: z + .array(z.string()) + .describe("Current professional priorities based on email content"), +}); +export type UserPersona = z.infer; + +export async function aiBuildUserPersona( + emailSummaries: EmailSummary[], + emailAccount: EmailAccountWithAI, + sentEmailSummaries?: EmailSummary[], + gmailSignature?: string, + gmailTemplates?: string[], +): Promise> { + const system = `You are a highly skilled AI analyst tasked with generating a focused professional persona of a user based on their email activity. + +Analyze the email summaries, signatures, and templates to identify: +1. Professional identity with supporting evidence +2. Current professional priorities based on email content + +Focus on understanding the user's role and what they're currently focused on professionally.`; + + const prompt = `### Input Data + +**Received Email Summaries:** +${emailSummaries.map((summary, index) => `Email ${index + 1} Summary: ${summary.summary} (Category: ${summary.category})`).join("\n")} + +${ + sentEmailSummaries && sentEmailSummaries.length > 0 + ? ` +**Sent Email Summaries:** +${sentEmailSummaries.map((summary, index) => `Sent ${index + 1} Summary: ${summary.summary} (Category: ${summary.category})`).join("\n")} +` + : "" +} + +**User's Signature:** +${gmailSignature || "[No signature data available – analyze based on email content only]"} + +${ + gmailTemplates && gmailTemplates.length > 0 + ? ` +**User's Gmail Templates:** +${gmailTemplates.map((template, index) => `Template ${index + 1}: ${template}`).join("\n")} +` + : "" +} + +--- + +Analyze the data and identify: +1. **Professional Identity**: What is their role and what evidence supports this? +2. **Current Priorities**: What are they focused on professionally based on email content?`; + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: userPersonaSchema, + userEmail: emailAccount.email, + usageLabel: "email-report-user-persona", + }); + + return result.object; +} diff --git a/apps/web/utils/ai/report/generate-actionable-recommendations.ts b/apps/web/utils/ai/report/generate-actionable-recommendations.ts new file mode 100644 index 0000000000..55633a5c45 --- /dev/null +++ b/apps/web/utils/ai/report/generate-actionable-recommendations.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; +import { chatCompletionObject } from "@/utils/llms"; +import type { EmailAccountWithAI } from "@/utils/llms/types"; +import type { UserPersona } from "@/utils/ai/report/build-user-persona"; +import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; + +const actionableRecommendationsSchema = z.object({ + immediateActions: z.array( + z.object({ + action: z.string().describe("Specific action to take"), + difficulty: z + .enum(["easy", "medium", "hard"]) + .describe("Implementation difficulty"), + impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), + timeRequired: z.string().describe("Time required (e.g., '5 minutes')"), + }), + ), + shortTermImprovements: z.array( + z.object({ + improvement: z.string().describe("Improvement to implement"), + timeline: z.string().describe("When to implement (e.g., 'This week')"), + expectedBenefit: z.string().describe("Expected benefit"), + }), + ), + longTermStrategy: z.array( + z.object({ + strategy: z.string().describe("Strategic initiative"), + description: z.string().describe("Detailed description"), + successMetrics: z.array(z.string()).describe("How to measure success"), + }), + ), +}); + +export async function aiGenerateActionableRecommendations( + emailSummaries: EmailSummary[], + emailAccount: EmailAccountWithAI, + userPersona: UserPersona, +): Promise> { + const system = `You are an email productivity consultant. Based on the comprehensive email analysis, create specific, actionable recommendations that the user can implement to improve their email workflow. + +Organize recommendations by timeline (immediate, short-term, long-term) and include specific implementation details and expected benefits.`; + + const prompt = `### Analysis Summary + +**User Persona:** ${userPersona.professionalIdentity.persona} +**Current Priorities:** ${userPersona.currentPriorities.join(", ")} +**Email Volume:** ${emailSummaries.length} emails analyzed + +--- + +Create actionable recommendations in three categories: +1. **Immediate Actions** (can be done today): 4-6 specific actions with time requirements +2. **Short-term Improvements** (this week): 3-4 improvements with timelines and benefits +3. **Long-term Strategy** (ongoing): 2-3 strategic initiatives with success metrics + +Focus on practical, implementable solutions that improve email organization and workflow efficiency.`; + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: actionableRecommendationsSchema, + userEmail: emailAccount.email, + usageLabel: "email-report-actionable-recommendations", + }); + + return result.object; +} diff --git a/apps/web/utils/ai/report/generate-executive-summary.ts b/apps/web/utils/ai/report/generate-executive-summary.ts new file mode 100644 index 0000000000..43547e5b52 --- /dev/null +++ b/apps/web/utils/ai/report/generate-executive-summary.ts @@ -0,0 +1,146 @@ +import { z } from "zod"; +import { chatCompletionObject } from "@/utils/llms"; +import type { gmail_v1 } from "@googleapis/gmail"; +import type { EmailAccountWithAI } from "@/utils/llms/types"; +import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; + +const executiveSummarySchema = z.object({ + userProfile: z.object({ + persona: z + .string() + .describe( + "1-5 word persona identification (e.g., 'Tech Startup Founder')", + ), + confidence: z + .number() + .min(0) + .max(100) + .describe("Confidence level in persona identification (0-100)"), + }), + topInsights: z + .array( + z.object({ + insight: z.string().describe("Key insight about user's email behavior"), + priority: z + .enum(["high", "medium", "low"]) + .describe("Priority level of this insight"), + icon: z.string().describe("Single emoji representing this insight"), + }), + ) + .describe("3-5 most important findings from the analysis"), + quickActions: z + .array( + z.object({ + action: z + .string() + .describe("Specific action the user can take immediately"), + difficulty: z + .enum(["easy", "medium", "hard"]) + .describe("How difficult this action is to implement"), + impact: z + .enum(["high", "medium", "low"]) + .describe("Expected impact of this action"), + }), + ) + .describe("4-6 immediate actions the user can take"), +}); + +export async function aiGenerateExecutiveSummary( + emailSummaries: EmailSummary[], + sentEmailSummaries: EmailSummary[], + gmailLabels: gmail_v1.Schema$Label[], + emailAccount: EmailAccountWithAI, +): Promise> { + const system = `You are a professional persona identification expert. Your primary task is to accurately identify the user's professional role based on their email patterns. + +CRITICAL: The persona must be a specific, recognizable professional role that clearly identifies what this person does for work. + +Examples of GOOD personas: +- "Startup Founder" +- "Software Developer" +- "Real Estate Agent" +- "Marketing Manager" +- "Sales Executive" +- "Product Manager" +- "Consultant" +- "Teacher" +- "Lawyer" +- "Doctor" +- "Influencer" +- "Freelance Designer" + +Examples of BAD personas (too vague): +- "Professional" +- "Business Person" +- "Tech Worker" +- "Knowledge Worker" + +Focus on identifying the PRIMARY professional role based on email content, senders, and communication patterns.`; + + const prompt = `### Email Analysis Data + +**Received Emails (${emailSummaries.length} emails):** +${emailSummaries + .slice(0, 30) + .map( + (email, i) => + `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, + ) + .join("\n")} + +**Sent Emails (${sentEmailSummaries.length} emails):** +${sentEmailSummaries + .slice(0, 15) + .map( + (email, i) => + `${i + 1}. To: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, + ) + .join("\n")} + +**Current Gmail Labels:** +${gmailLabels.map((label) => `- ${label.name} (${label.messagesTotal || 0} emails)`).join("\n")} + +--- + +**PERSONA IDENTIFICATION INSTRUCTIONS:** + +Analyze the email patterns to identify the user's PRIMARY professional role: + +1. **Look for role indicators:** + - Who do they email? (clients, team members, investors, customers, etc.) + - What topics dominate? (code reviews, property listings, campaign metrics, etc.) + - What language/terminology is used? (technical terms, industry jargon, etc.) + - What responsibilities are evident? (managing teams, closing deals, creating content, etc.) + +2. **Common professional patterns:** + - **Founder/CEO**: Investor emails, team management, strategic decisions, fundraising + - **Developer**: Code reviews, technical discussions, GitHub notifications, deployment issues + - **Sales**: CRM notifications, client outreach, deal discussions, quota tracking + - **Marketing**: Campaign metrics, content creation, social media, analytics + - **Real Estate**: Property listings, client communications, MLS notifications + - **Consultant**: Client projects, proposals, expertise sharing, industry updates + - **Teacher**: Student communications, educational content, institutional emails + +3. **Confidence level:** + - 90-100%: Very clear indicators, consistent patterns + - 70-89%: Strong indicators, some ambiguity + - 50-69%: Mixed signals, multiple possible roles + - Below 50%: Unclear or insufficient data + +Generate: +1. **Specific professional persona** (1-3 words max, e.g., "Software Developer", "Real Estate Agent") +2. **Confidence level** based on clarity of evidence +3. **Top insights** about their email behavior +4. **Quick actions** for immediate improvement`; + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: executiveSummarySchema, + userEmail: emailAccount.email, + usageLabel: "email-report-executive-summary", + }); + + return result.object; +} diff --git a/apps/web/utils/ai/report/prompts.ts b/apps/web/utils/ai/report/prompts.ts deleted file mode 100644 index ba6447add5..0000000000 --- a/apps/web/utils/ai/report/prompts.ts +++ /dev/null @@ -1,586 +0,0 @@ -import { z } from "zod"; -import { createScopedLogger } from "@/utils/logger"; -import { chatCompletionObject } from "@/utils/llms"; -import type { gmail_v1 } from "@googleapis/gmail"; -import type { EmailForLLM } from "@/utils/types"; -import type { EmailAccountWithAI } from "@/utils/llms/types"; -import { sleep } from "@/utils/sleep"; -import { stringifyEmail } from "@/utils/stringify-email"; - -const logger = createScopedLogger("email-report-prompts"); - -const emailSummarySchema = z.object({ - summary: z.string().describe("Brief summary of the email content"), - sender: z.string().describe("Email sender"), - subject: z.string().describe("Email subject"), - category: z - .string() - .describe("Category of the email (work, personal, marketing, etc.)"), -}); -export type EmailSummary = z.infer; - -const executiveSummarySchema = z.object({ - userProfile: z.object({ - persona: z - .string() - .describe( - "1-5 word persona identification (e.g., 'Tech Startup Founder')", - ), - confidence: z - .number() - .min(0) - .max(100) - .describe("Confidence level in persona identification (0-100)"), - }), - topInsights: z - .array( - z.object({ - insight: z.string().describe("Key insight about user's email behavior"), - priority: z - .enum(["high", "medium", "low"]) - .describe("Priority level of this insight"), - icon: z.string().describe("Single emoji representing this insight"), - }), - ) - .describe("3-5 most important findings from the analysis"), - quickActions: z - .array( - z.object({ - action: z - .string() - .describe("Specific action the user can take immediately"), - difficulty: z - .enum(["easy", "medium", "hard"]) - .describe("How difficult this action is to implement"), - impact: z - .enum(["high", "medium", "low"]) - .describe("Expected impact of this action"), - }), - ) - .describe("4-6 immediate actions the user can take"), -}); - -const userPersonaSchema = z.object({ - professionalIdentity: z.object({ - persona: z.string().describe("Professional persona identification"), - supportingEvidence: z - .array(z.string()) - .describe("Evidence supporting this persona identification"), - }), - currentPriorities: z - .array(z.string()) - .describe("Current professional priorities based on email content"), -}); - -const emailBehaviorSchema = z.object({ - timingPatterns: z.object({ - peakHours: z.array(z.string()).describe("Peak email activity hours"), - responsePreference: z.string().describe("Preferred response timing"), - frequency: z.string().describe("Overall email frequency"), - }), - contentPreferences: z.object({ - preferred: z - .array(z.string()) - .describe("Types of emails user engages with"), - avoided: z - .array(z.string()) - .describe("Types of emails user typically ignores"), - }), - engagementTriggers: z - .array(z.string()) - .describe("What prompts user to take action on emails"), -}); - -const responsePatternsSchema = z.object({ - commonResponses: z.array( - z.object({ - pattern: z.string().describe("Description of the response pattern"), - example: z.string().describe("Example of this type of response"), - frequency: z - .number() - .describe("Percentage of responses using this pattern"), - triggers: z - .array(z.string()) - .describe("What types of emails trigger this response"), - }), - ), - suggestedTemplates: z.array( - z.object({ - templateName: z.string().describe("Name of the email template"), - template: z.string().describe("The actual email template text"), - useCase: z.string().describe("When to use this template"), - }), - ), - categoryOrganization: z.array( - z.object({ - category: z.string().describe("Email category name"), - description: z - .string() - .describe("What types of emails belong in this category"), - emailCount: z - .number() - .describe("Estimated number of emails in this category"), - priority: z - .enum(["high", "medium", "low"]) - .describe("Priority level for this category"), - }), - ), -}); - -const labelAnalysisSchema = z.object({ - optimizationSuggestions: z.array( - z.object({ - type: z - .enum(["create", "consolidate", "rename", "delete"]) - .describe("Type of optimization"), - suggestion: z.string().describe("Specific suggestion"), - reason: z.string().describe("Reason for this suggestion"), - impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), - }), - ), -}); - -const actionableRecommendationsSchema = z.object({ - immediateActions: z.array( - z.object({ - action: z.string().describe("Specific action to take"), - difficulty: z - .enum(["easy", "medium", "hard"]) - .describe("Implementation difficulty"), - impact: z.enum(["high", "medium", "low"]).describe("Expected impact"), - timeRequired: z.string().describe("Time required (e.g., '5 minutes')"), - }), - ), - shortTermImprovements: z.array( - z.object({ - improvement: z.string().describe("Improvement to implement"), - timeline: z.string().describe("When to implement (e.g., 'This week')"), - expectedBenefit: z.string().describe("Expected benefit"), - }), - ), - longTermStrategy: z.array( - z.object({ - strategy: z.string().describe("Strategic initiative"), - description: z.string().describe("Detailed description"), - successMetrics: z.array(z.string()).describe("How to measure success"), - }), - ), -}); - -export async function generateExecutiveSummary( - emailSummaries: EmailSummary[], - sentEmailSummaries: EmailSummary[], - gmailLabels: gmail_v1.Schema$Label[], - emailAccount: EmailAccountWithAI, -): Promise> { - const system = `You are a professional persona identification expert. Your primary task is to accurately identify the user's professional role based on their email patterns. - -CRITICAL: The persona must be a specific, recognizable professional role that clearly identifies what this person does for work. - -Examples of GOOD personas: -- "Startup Founder" -- "Software Developer" -- "Real Estate Agent" -- "Marketing Manager" -- "Sales Executive" -- "Product Manager" -- "Consultant" -- "Teacher" -- "Lawyer" -- "Doctor" -- "Influencer" -- "Freelance Designer" - -Examples of BAD personas (too vague): -- "Professional" -- "Business Person" -- "Tech Worker" -- "Knowledge Worker" - -Focus on identifying the PRIMARY professional role based on email content, senders, and communication patterns.`; - - const prompt = `### Email Analysis Data - -**Received Emails (${emailSummaries.length} emails):** -${emailSummaries - .slice(0, 30) - .map( - (email, i) => - `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, - ) - .join("\n")} - -**Sent Emails (${sentEmailSummaries.length} emails):** -${sentEmailSummaries - .slice(0, 15) - .map( - (email, i) => - `${i + 1}. To: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, - ) - .join("\n")} - -**Current Gmail Labels:** -${gmailLabels.map((label) => `- ${label.name} (${label.messagesTotal || 0} emails)`).join("\n")} - ---- - -**PERSONA IDENTIFICATION INSTRUCTIONS:** - -Analyze the email patterns to identify the user's PRIMARY professional role: - -1. **Look for role indicators:** - - Who do they email? (clients, team members, investors, customers, etc.) - - What topics dominate? (code reviews, property listings, campaign metrics, etc.) - - What language/terminology is used? (technical terms, industry jargon, etc.) - - What responsibilities are evident? (managing teams, closing deals, creating content, etc.) - -2. **Common professional patterns:** - - **Founder/CEO**: Investor emails, team management, strategic decisions, fundraising - - **Developer**: Code reviews, technical discussions, GitHub notifications, deployment issues - - **Sales**: CRM notifications, client outreach, deal discussions, quota tracking - - **Marketing**: Campaign metrics, content creation, social media, analytics - - **Real Estate**: Property listings, client communications, MLS notifications - - **Consultant**: Client projects, proposals, expertise sharing, industry updates - - **Teacher**: Student communications, educational content, institutional emails - -3. **Confidence level:** - - 90-100%: Very clear indicators, consistent patterns - - 70-89%: Strong indicators, some ambiguity - - 50-69%: Mixed signals, multiple possible roles - - Below 50%: Unclear or insufficient data - -Generate: -1. **Specific professional persona** (1-3 words max, e.g., "Software Developer", "Real Estate Agent") -2. **Confidence level** based on clarity of evidence -3. **Top insights** about their email behavior -4. **Quick actions** for immediate improvement`; - - const result = await chatCompletionObject({ - userAi: emailAccount.user, - system, - prompt, - schema: executiveSummarySchema, - userEmail: emailAccount.email, - usageLabel: "email-report-executive-summary", - }); - - return result.object; -} - -export async function buildUserPersona( - emailSummaries: EmailSummary[], - emailAccount: EmailAccountWithAI, - sentEmailSummaries?: EmailSummary[], - gmailSignature?: string, - gmailTemplates?: string[], -): Promise> { - const system = `You are a highly skilled AI analyst tasked with generating a focused professional persona of a user based on their email activity. - -Analyze the email summaries, signatures, and templates to identify: -1. Professional identity with supporting evidence -2. Current professional priorities based on email content - -Focus on understanding the user's role and what they're currently focused on professionally.`; - - const prompt = `### Input Data - -**Received Email Summaries:** -${emailSummaries.map((summary, index) => `Email ${index + 1} Summary: ${summary.summary} (Category: ${summary.category})`).join("\n")} - -${ - sentEmailSummaries && sentEmailSummaries.length > 0 - ? ` -**Sent Email Summaries:** -${sentEmailSummaries.map((summary, index) => `Sent ${index + 1} Summary: ${summary.summary} (Category: ${summary.category})`).join("\n")} -` - : "" -} - -**User's Signature:** -${gmailSignature || "[No signature data available – analyze based on email content only]"} - -${ - gmailTemplates && gmailTemplates.length > 0 - ? ` -**User's Gmail Templates:** -${gmailTemplates.map((template, index) => `Template ${index + 1}: ${template}`).join("\n")} -` - : "" -} - ---- - -Analyze the data and identify: -1. **Professional Identity**: What is their role and what evidence supports this? -2. **Current Priorities**: What are they focused on professionally based on email content?`; - - const result = await chatCompletionObject({ - userAi: emailAccount.user, - system, - prompt, - schema: userPersonaSchema, - userEmail: emailAccount.email, - usageLabel: "email-report-user-persona", - }); - - return result.object; -} - -export async function analyzeEmailBehavior( - emailSummaries: EmailSummary[], - emailAccount: EmailAccountWithAI, - sentEmailSummaries?: EmailSummary[], -): Promise> { - const system = `You are an expert AI system that analyzes a user's email behavior to infer timing patterns, content preferences, and automation opportunities. - -Focus on identifying patterns that can be automated and providing specific, actionable automation rules that would save time and improve email management efficiency.`; - - const prompt = `### Email Analysis Data - -**Received Emails:** -${emailSummaries.map((email, i) => `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`).join("\n")} - -${ - sentEmailSummaries && sentEmailSummaries.length > 0 - ? ` -**Sent Emails:** -${sentEmailSummaries.map((email, i) => `${i + 1}. To: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`).join("\n")} -` - : "" -} - ---- - -Analyze the email patterns and identify: -1. Timing patterns (when emails are most active, response preferences) -2. Content preferences (what types of emails they engage with vs avoid) -3. Engagement triggers (what prompts them to take action) -4. Specific automation opportunities with estimated time savings`; - - const result = await chatCompletionObject({ - userAi: emailAccount.user, - system, - prompt, - schema: emailBehaviorSchema, - userEmail: emailAccount.email, - usageLabel: "email-report-email-behavior", - }); - - return result.object; -} - -export async function analyzeResponsePatterns( - emailSummaries: EmailSummary[], - emailAccount: EmailAccountWithAI, - sentEmailSummaries?: EmailSummary[], -): Promise> { - const system = `You are an expert email behavior analyst. Your task is to identify common response patterns and suggest email categorization and templates based on the user's email activity. - -Focus on practical, actionable insights for email management including reusable templates and smart categorization. - -IMPORTANT: When creating email categories, avoid meaningless or generic categories such as: -- "Other", "Unknown", "Unclear", "Miscellaneous" -- "Personal" (too generic and meaningless) -- "Unclear Content/HTML Code", "HTML Content", "Raw Content" -- "General", "Random", "Various" - -Only suggest categories that are meaningful and provide clear organizational value. If an email doesn't fit into a meaningful category, don't create a category for it.`; - - const prompt = `### Input Data - -**Received Email Summaries:** -${emailSummaries.map((summary, index) => `Email ${index + 1}: ${summary.summary} (Category: ${summary.category})`).join("\n")} - -${ - sentEmailSummaries && sentEmailSummaries.length > 0 - ? ` -**Sent Email Summaries (User's Response Patterns):** -${sentEmailSummaries.map((summary, index) => `Sent ${index + 1}: ${summary.summary} (Category: ${summary.category})`).join("\n")} -` - : "" -} - ---- - -Analyze the data and identify: -1. Common response patterns the user uses with examples and frequency -2. Suggested email templates that would save time -3. Email categorization strategy with volume estimates and priorities - -For email categorization, create simple, practical categories based on actual email content. Examples of good categories: -- "Work", "Finance", "Meetings", "Marketing", "Support", "Sales" -- "Projects", "Billing", "Team", "Clients", "Products", "Services" -- "Administrative", "Technical", "Legal", "HR", "Operations" - -Only suggest categories that are meaningful and provide clear organizational value. If emails don't fit into meaningful categories, don't create categories for them.`; - - const result = await chatCompletionObject({ - userAi: emailAccount.user, - system, - prompt, - schema: responsePatternsSchema, - userEmail: emailAccount.email, - usageLabel: "email-report-response-patterns", - }); - - return result.object; -} - -export async function analyzeLabelOptimization( - emailSummaries: EmailSummary[], - emailAccount: EmailAccountWithAI, - gmailLabels: gmail_v1.Schema$Label[], -): Promise> { - const system = `You are a Gmail organization expert. Analyze the user's current labels and email patterns to suggest specific optimizations that will improve their email organization and workflow efficiency. - -Focus on practical suggestions that will reduce email management time and improve organization.`; - - const prompt = `### Current Gmail Labels -${gmailLabels.map((label) => `- ${label.name}: ${label.messagesTotal || 0} emails, ${label.messagesUnread || 0} unread`).join("\n")} - -### Email Content Analysis -${emailSummaries - .slice(0, 30) - .map( - (email, i) => - `${i + 1}. From: ${email.sender} | Subject: ${email.subject} | Category: ${email.category} | Summary: ${email.summary}`, - ) - .join("\n")} - ---- - -Based on the current labels and email content, suggest specific optimizations: -1. Labels to create based on email patterns -2. Labels to consolidate that have overlapping purposes -3. Labels to rename for better clarity -4. Labels to delete that are unused or redundant - -Each suggestion should include the reason and expected impact.`; - - const result = await chatCompletionObject({ - userAi: emailAccount.user, - system, - prompt, - schema: labelAnalysisSchema, - userEmail: emailAccount.email, - usageLabel: "email-report-label-analysis", - }); - - return result.object; -} - -export async function generateActionableRecommendations( - emailSummaries: EmailSummary[], - emailAccount: EmailAccountWithAI, - userPersona: z.infer, -): Promise> { - const system = `You are an email productivity consultant. Based on the comprehensive email analysis, create specific, actionable recommendations that the user can implement to improve their email workflow. - -Organize recommendations by timeline (immediate, short-term, long-term) and include specific implementation details and expected benefits.`; - - const prompt = `### Analysis Summary - -**User Persona:** ${userPersona.professionalIdentity.persona} -**Current Priorities:** ${userPersona.currentPriorities.join(", ")} -**Email Volume:** ${emailSummaries.length} emails analyzed - ---- - -Create actionable recommendations in three categories: -1. **Immediate Actions** (can be done today): 4-6 specific actions with time requirements -2. **Short-term Improvements** (this week): 3-4 improvements with timelines and benefits -3. **Long-term Strategy** (ongoing): 2-3 strategic initiatives with success metrics - -Focus on practical, implementable solutions that improve email organization and workflow efficiency.`; - - const result = await chatCompletionObject({ - userAi: emailAccount.user, - system, - prompt, - schema: actionableRecommendationsSchema, - userEmail: emailAccount.email, - usageLabel: "email-report-actionable-recommendations", - }); - - return result.object; -} - -/** - * Summarize emails for analysis - */ -export async function summarizeEmails( - emails: EmailForLLM[], - emailAccount: EmailAccountWithAI, -): Promise { - if (emails.length === 0) { - logger.warn("No emails to summarize, returning empty array"); - return []; - } - - const batchSize = 15; - const results: EmailSummary[] = []; - - for (let i = 0; i < emails.length; i += batchSize) { - const batch = emails.slice(i, i + batchSize); - const batchNumber = Math.floor(i / batchSize) + 1; - const totalBatches = Math.ceil(emails.length / batchSize); - - const batchResults = await processEmailBatch( - batch, - emailAccount, - batchNumber, - totalBatches, - ); - results.push(...batchResults); - - if (i + batchSize < emails.length) { - await sleep(1000); - } - } - - return results; -} - -async function processEmailBatch( - emails: EmailForLLM[], - emailAccount: EmailAccountWithAI, - batchNumber: number, - totalBatches: number, -): Promise { - const system = `You are an assistant that processes user emails to extract their core meaning for later analysis. - -For each email, write a **factual summary of 3–5 sentences** that clearly describes: -- The main topic or purpose of the email -- What the sender wants, requests, or informs -- Any relevant secondary detail (e.g., urgency, timing, sender role, or context) -- Optional: mention tools, platforms, or projects if they help clarify the email's purpose - -**Important Rules:** -- Be objective. Do **not** speculate, interpret intent, or invent details. -- Summarize only what is in the actual content of the email. -- Use professional and concise language. -- **Include** marketing/newsletter emails **only if** they reflect the user's professional interests (e.g., product updates, industry news, job boards). -- **Skip** irrelevant promotions, spam, or generic sales offers (e.g., holiday deals, coupon codes).`; - - const prompt = ` -**Input Emails (Batch ${batchNumber} of ${totalBatches}):** - -${emails.map((email) => `${stringifyEmail(email, 2000)}`).join("\n")} - -Return the analysis as a JSON array of objects.`; - - logger.trace("Input", { system, prompt }); - - const result = await chatCompletionObject({ - userAi: emailAccount.user, - system, - prompt, - schema: z.array(emailSummarySchema), - userEmail: emailAccount.email, - usageLabel: "email-report-summary-generation", - }); - - logger.trace("processEmailBatch: result", { response: result.object }); - - return result.object; -} diff --git a/apps/web/utils/ai/report/response-patterns.ts b/apps/web/utils/ai/report/response-patterns.ts new file mode 100644 index 0000000000..b5927ac3ff --- /dev/null +++ b/apps/web/utils/ai/report/response-patterns.ts @@ -0,0 +1,97 @@ +import { z } from "zod"; +import { chatCompletionObject } from "@/utils/llms"; +import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; +import type { EmailAccountWithAI } from "@/utils/llms/types"; + +const responsePatternsSchema = z.object({ + commonResponses: z.array( + z.object({ + pattern: z.string().describe("Description of the response pattern"), + example: z.string().describe("Example of this type of response"), + frequency: z + .number() + .describe("Percentage of responses using this pattern"), + triggers: z + .array(z.string()) + .describe("What types of emails trigger this response"), + }), + ), + suggestedTemplates: z.array( + z.object({ + templateName: z.string().describe("Name of the email template"), + template: z.string().describe("The actual email template text"), + useCase: z.string().describe("When to use this template"), + }), + ), + categoryOrganization: z.array( + z.object({ + category: z.string().describe("Email category name"), + description: z + .string() + .describe("What types of emails belong in this category"), + emailCount: z + .number() + .describe("Estimated number of emails in this category"), + priority: z + .enum(["high", "medium", "low"]) + .describe("Priority level for this category"), + }), + ), +}); + +export async function aiAnalyzeResponsePatterns( + emailSummaries: EmailSummary[], + emailAccount: EmailAccountWithAI, + sentEmailSummaries?: EmailSummary[], +): Promise> { + const system = `You are an expert email behavior analyst. Your task is to identify common response patterns and suggest email categorization and templates based on the user's email activity. + +Focus on practical, actionable insights for email management including reusable templates and smart categorization. + +IMPORTANT: When creating email categories, avoid meaningless or generic categories such as: +- "Other", "Unknown", "Unclear", "Miscellaneous" +- "Personal" (too generic and meaningless) +- "Unclear Content/HTML Code", "HTML Content", "Raw Content" +- "General", "Random", "Various" + +Only suggest categories that are meaningful and provide clear organizational value. If an email doesn't fit into a meaningful category, don't create a category for it.`; + + const prompt = `### Input Data + +**Received Email Summaries:** +${emailSummaries.map((summary, index) => `Email ${index + 1}: ${summary.summary} (Category: ${summary.category})`).join("\n")} + +${ + sentEmailSummaries && sentEmailSummaries.length > 0 + ? ` +**Sent Email Summaries (User's Response Patterns):** +${sentEmailSummaries.map((summary, index) => `Sent ${index + 1}: ${summary.summary} (Category: ${summary.category})`).join("\n")} +` + : "" +} + +--- + +Analyze the data and identify: +1. Common response patterns the user uses with examples and frequency +2. Suggested email templates that would save time +3. Email categorization strategy with volume estimates and priorities + +For email categorization, create simple, practical categories based on actual email content. Examples of good categories: +- "Work", "Finance", "Meetings", "Marketing", "Support", "Sales" +- "Projects", "Billing", "Team", "Clients", "Products", "Services" +- "Administrative", "Technical", "Legal", "HR", "Operations" + +Only suggest categories that are meaningful and provide clear organizational value. If emails don't fit into meaningful categories, don't create categories for them.`; + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: responsePatternsSchema, + userEmail: emailAccount.email, + usageLabel: "email-report-response-patterns", + }); + + return result.object; +} diff --git a/apps/web/utils/ai/report/summarize-emails.ts b/apps/web/utils/ai/report/summarize-emails.ts new file mode 100644 index 0000000000..a6caa01075 --- /dev/null +++ b/apps/web/utils/ai/report/summarize-emails.ts @@ -0,0 +1,96 @@ +import { z } from "zod"; +import { createScopedLogger } from "@/utils/logger"; +import { chatCompletionObject } from "@/utils/llms"; +import type { EmailForLLM } from "@/utils/types"; +import type { EmailAccountWithAI } from "@/utils/llms/types"; +import { sleep } from "@/utils/sleep"; +import { stringifyEmail } from "@/utils/stringify-email"; + +const logger = createScopedLogger("email-report-prompts"); + +const emailSummarySchema = z.object({ + summary: z.string().describe("Brief summary of the email content"), + sender: z.string().describe("Email sender"), + subject: z.string().describe("Email subject"), + category: z + .string() + .describe("Category of the email (work, personal, marketing, etc.)"), +}); +export type EmailSummary = z.infer; + +export async function aiSummarizeEmails( + emails: EmailForLLM[], + emailAccount: EmailAccountWithAI, +): Promise { + if (emails.length === 0) { + logger.warn("No emails to summarize, returning empty array"); + return []; + } + + const batchSize = 15; + const results: EmailSummary[] = []; + + for (let i = 0; i < emails.length; i += batchSize) { + const batch = emails.slice(i, i + batchSize); + const batchNumber = Math.floor(i / batchSize) + 1; + const totalBatches = Math.ceil(emails.length / batchSize); + + const batchResults = await processEmailBatch( + batch, + emailAccount, + batchNumber, + totalBatches, + ); + results.push(...batchResults); + + if (i + batchSize < emails.length) { + await sleep(1000); + } + } + + return results; +} + +async function processEmailBatch( + emails: EmailForLLM[], + emailAccount: EmailAccountWithAI, + batchNumber: number, + totalBatches: number, +): Promise { + const system = `You are an assistant that processes user emails to extract their core meaning for later analysis. + +For each email, write a **factual summary of 3–5 sentences** that clearly describes: +- The main topic or purpose of the email +- What the sender wants, requests, or informs +- Any relevant secondary detail (e.g., urgency, timing, sender role, or context) +- Optional: mention tools, platforms, or projects if they help clarify the email's purpose + +**Important Rules:** +- Be objective. Do **not** speculate, interpret intent, or invent details. +- Summarize only what is in the actual content of the email. +- Use professional and concise language. +- **Include** marketing/newsletter emails **only if** they reflect the user's professional interests (e.g., product updates, industry news, job boards). +- **Skip** irrelevant promotions, spam, or generic sales offers (e.g., holiday deals, coupon codes).`; + + const prompt = ` +**Input Emails (Batch ${batchNumber} of ${totalBatches}):** + +${emails.map((email) => `${stringifyEmail(email, 2000)}`).join("\n")} + +Return the analysis as a JSON array of objects.`; + + logger.trace("Input", { system, prompt }); + + const result = await chatCompletionObject({ + userAi: emailAccount.user, + system, + prompt, + schema: z.array(emailSummarySchema), + userEmail: emailAccount.email, + usageLabel: "email-report-summary-generation", + }); + + logger.trace("Output", { result: result.object }); + + return result.object; +} From 05a8082e4888b14eadf54a4cf2c26aba27f7698e Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:38:28 +0300 Subject: [PATCH 16/20] reorder --- apps/web/utils/actions/report.ts | 216 ++++++++++++++++--------------- 1 file changed, 110 insertions(+), 106 deletions(-) diff --git a/apps/web/utils/actions/report.ts b/apps/web/utils/actions/report.ts index c430be5dac..b5891a527f 100644 --- a/apps/web/utils/actions/report.ts +++ b/apps/web/utils/actions/report.ts @@ -30,107 +30,6 @@ export const generateReportAction = actionClient return getEmailReportData({ emailAccountId }); }); -async function fetchGmailLabels( - gmail: gmail_v1.Gmail, -): Promise { - try { - const response = await gmail.users.labels.list({ userId: "me" }); - - const userLabels = - response.data.labels?.filter( - (label: gmail_v1.Schema$Label) => - label.type === "user" && - label.name && - !label.name.startsWith("CATEGORY_") && - !label.name.startsWith("CHAT"), - ) || []; - - const labelsWithCounts = await Promise.all( - userLabels - .filter( - ( - label, - ): label is gmail_v1.Schema$Label & { id: string; name: string } => - Boolean(label.id && label.name), - ) - .map(async (label) => { - try { - const labelDetail = await gmail.users.labels.get({ - userId: "me", - id: label.id, - }); - return { - ...label, - messagesTotal: labelDetail.data.messagesTotal || 0, - messagesUnread: labelDetail.data.messagesUnread || 0, - threadsTotal: labelDetail.data.threadsTotal || 0, - threadsUnread: labelDetail.data.threadsUnread || 0, - }; - } catch (error) { - logger.warn(`Failed to get details for label ${label.name}:`, { - error: error instanceof Error ? error.message : String(error), - }); - return { - ...label, - messagesTotal: 0, - messagesUnread: 0, - threadsTotal: 0, - threadsUnread: 0, - }; - } - }), - ); - - const sortedLabels = labelsWithCounts.sort( - (a, b) => (b.messagesTotal || 0) - (a.messagesTotal || 0), - ); - - return sortedLabels; - } catch (error) { - logger.warn("Failed to fetch Gmail labels:", { - error: error instanceof Error ? error.message : String(error), - }); - return []; - } -} - -async function fetchGmailSignature(gmail: gmail_v1.Gmail): Promise { - try { - const sendAsList = await gmail.users.settings.sendAs.list({ - userId: "me", - }); - - if (!sendAsList.data.sendAs || sendAsList.data.sendAs.length === 0) { - logger.warn("No sendAs settings found"); - return ""; - } - - const primarySendAs = sendAsList.data.sendAs[0]; - if (!primarySendAs.sendAsEmail) { - logger.warn("No primary sendAs email found"); - return ""; - } - - const signatureResponse = await gmail.users.settings.sendAs.get({ - userId: "me", - sendAsEmail: primarySendAs.sendAsEmail, - }); - - const signature = signatureResponse.data.signature; - logger.info("Gmail signature fetched successfully", { - hasSignature: !!signature, - sendAsEmail: primarySendAs.sendAsEmail, - }); - - return signature || ""; - } catch (error) { - logger.warn("Failed to fetch Gmail signature:", { - error: error instanceof Error ? error.message : String(error), - }); - return ""; - } -} - async function getEmailReportData({ emailAccountId, }: { @@ -189,11 +88,13 @@ async function getEmailReportData({ ).catch((error) => { logger.error("Error generating user persona", { error }); }), - aiAnalyzeEmailBehavior(receivedSummaries, emailAccount, sentSummaries).catch( - (error) => { - logger.error("Error generating email behavior", { error }); - }, - ), + aiAnalyzeEmailBehavior( + receivedSummaries, + emailAccount, + sentSummaries, + ).catch((error) => { + logger.error("Error generating email behavior", { error }); + }), aiAnalyzeResponsePatterns( receivedSummaries, emailAccount, @@ -248,3 +149,106 @@ async function getEmailReportData({ actionableRecommendations, }; } + +// TODO: should be able to import this functionality from elsewhere +async function fetchGmailLabels( + gmail: gmail_v1.Gmail, +): Promise { + try { + const response = await gmail.users.labels.list({ userId: "me" }); + + const userLabels = + response.data.labels?.filter( + (label: gmail_v1.Schema$Label) => + label.type === "user" && + label.name && + !label.name.startsWith("CATEGORY_") && + !label.name.startsWith("CHAT"), + ) || []; + + const labelsWithCounts = await Promise.all( + userLabels + .filter( + ( + label, + ): label is gmail_v1.Schema$Label & { id: string; name: string } => + Boolean(label.id && label.name), + ) + .map(async (label) => { + try { + const labelDetail = await gmail.users.labels.get({ + userId: "me", + id: label.id, + }); + return { + ...label, + messagesTotal: labelDetail.data.messagesTotal || 0, + messagesUnread: labelDetail.data.messagesUnread || 0, + threadsTotal: labelDetail.data.threadsTotal || 0, + threadsUnread: labelDetail.data.threadsUnread || 0, + }; + } catch (error) { + logger.warn(`Failed to get details for label ${label.name}:`, { + error: error instanceof Error ? error.message : String(error), + }); + return { + ...label, + messagesTotal: 0, + messagesUnread: 0, + threadsTotal: 0, + threadsUnread: 0, + }; + } + }), + ); + + const sortedLabels = labelsWithCounts.sort( + (a, b) => (b.messagesTotal || 0) - (a.messagesTotal || 0), + ); + + return sortedLabels; + } catch (error) { + logger.warn("Failed to fetch Gmail labels:", { + error: error instanceof Error ? error.message : String(error), + }); + return []; + } +} + +// TODO: should be able to import this functionality from elsewhere +async function fetchGmailSignature(gmail: gmail_v1.Gmail): Promise { + try { + const sendAsList = await gmail.users.settings.sendAs.list({ + userId: "me", + }); + + if (!sendAsList.data.sendAs || sendAsList.data.sendAs.length === 0) { + logger.warn("No sendAs settings found"); + return ""; + } + + const primarySendAs = sendAsList.data.sendAs[0]; + if (!primarySendAs.sendAsEmail) { + logger.warn("No primary sendAs email found"); + return ""; + } + + const signatureResponse = await gmail.users.settings.sendAs.get({ + userId: "me", + sendAsEmail: primarySendAs.sendAsEmail, + }); + + const signature = signatureResponse.data.signature; + logger.info("Gmail signature fetched successfully", { + hasSignature: !!signature, + sendAsEmail: primarySendAs.sendAsEmail, + }); + + return signature || ""; + } catch (error) { + logger.warn("Failed to fetch Gmail signature:", { + error: error instanceof Error ? error.message : String(error), + }); + return ""; + } +} From d40f06926e80a83935327de37bd740b399f63e51 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:03:48 +0300 Subject: [PATCH 17/20] adjust schema to avoid errors. send less info for summary --- apps/web/utils/actions/report.ts | 26 ++++++++++++++------ apps/web/utils/ai/report/summarize-emails.ts | 11 ++++++--- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/apps/web/utils/actions/report.ts b/apps/web/utils/actions/report.ts index b5891a527f..6f1ea74f3b 100644 --- a/apps/web/utils/actions/report.ts +++ b/apps/web/utils/actions/report.ts @@ -47,14 +47,24 @@ async function getEmailReportData({ const { receivedEmails, sentEmails, totalReceived, totalSent } = await fetchEmailsForReport({ emailAccount }); - const receivedSummaries = await aiSummarizeEmails( - receivedEmails.map((message) => getEmailForLLM(message)), - emailAccount, - ); - const sentSummaries = await aiSummarizeEmails( - sentEmails.map((message) => getEmailForLLM(message)), - emailAccount, - ); + const [receivedSummaries, sentSummaries] = await Promise.all([ + aiSummarizeEmails( + receivedEmails.map((message) => + getEmailForLLM(message, { maxLength: 1000 }), + ), + emailAccount, + ).catch((error) => { + logger.error("Error summarizing received emails", { error }); + return []; + }), + aiSummarizeEmails( + sentEmails.map((message) => getEmailForLLM(message, { maxLength: 1000 })), + emailAccount, + ).catch((error) => { + logger.error("Error summarizing sent emails", { error }); + return []; + }), + ]); const gmail = await getGmailClientForEmail({ emailAccountId: emailAccount.id, diff --git a/apps/web/utils/ai/report/summarize-emails.ts b/apps/web/utils/ai/report/summarize-emails.ts index a6caa01075..daa4e69581 100644 --- a/apps/web/utils/ai/report/summarize-emails.ts +++ b/apps/web/utils/ai/report/summarize-emails.ts @@ -85,12 +85,17 @@ Return the analysis as a JSON array of objects.`; userAi: emailAccount.user, system, prompt, - schema: z.array(emailSummarySchema), + schema: z.object({ + summaries: z + .array(emailSummarySchema) + .describe("Summaries of the emails"), + }), userEmail: emailAccount.email, usageLabel: "email-report-summary-generation", + modelType: "economy", }); - logger.trace("Output", { result: result.object }); + logger.trace("Output", { result: result.object.summaries }); - return result.object; + return result.object.summaries; } From 19e4d87be3786478c7fb7e86e5c7744f14439755 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:36:17 +0300 Subject: [PATCH 18/20] trace logs --- apps/web/utils/ai/report/analyze-email-behavior.ts | 7 +++++++ apps/web/utils/ai/report/analyze-label-optimization.ts | 7 +++++++ apps/web/utils/ai/report/build-user-persona.ts | 7 +++++++ .../ai/report/generate-actionable-recommendations.ts | 7 +++++++ apps/web/utils/ai/report/generate-executive-summary.ts | 7 +++++++ apps/web/utils/ai/report/response-patterns.ts | 9 ++++++++- apps/web/utils/ai/report/summarize-emails.ts | 2 +- 7 files changed, 44 insertions(+), 2 deletions(-) diff --git a/apps/web/utils/ai/report/analyze-email-behavior.ts b/apps/web/utils/ai/report/analyze-email-behavior.ts index b88c617456..ca316e7656 100644 --- a/apps/web/utils/ai/report/analyze-email-behavior.ts +++ b/apps/web/utils/ai/report/analyze-email-behavior.ts @@ -2,6 +2,9 @@ import { z } from "zod"; import { chatCompletionObject } from "@/utils/llms"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; +import { createScopedLogger } from "@/utils/logger"; + +const logger = createScopedLogger("email-report-email-behavior"); const emailBehaviorSchema = z.object({ timingPatterns: z.object({ @@ -53,6 +56,8 @@ Analyze the email patterns and identify: 3. Engagement triggers (what prompts them to take action) 4. Specific automation opportunities with estimated time savings`; + logger.trace("Input", { system, prompt }); + const result = await chatCompletionObject({ userAi: emailAccount.user, system, @@ -62,5 +67,7 @@ Analyze the email patterns and identify: usageLabel: "email-report-email-behavior", }); + logger.trace("Output", result.object); + return result.object; } diff --git a/apps/web/utils/ai/report/analyze-label-optimization.ts b/apps/web/utils/ai/report/analyze-label-optimization.ts index 3026cb498d..42e2ddc947 100644 --- a/apps/web/utils/ai/report/analyze-label-optimization.ts +++ b/apps/web/utils/ai/report/analyze-label-optimization.ts @@ -3,6 +3,9 @@ import { chatCompletionObject } from "@/utils/llms"; import type { gmail_v1 } from "@googleapis/gmail"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; +import { createScopedLogger } from "@/utils/logger"; + +const logger = createScopedLogger("email-report-label-analysis"); const labelAnalysisSchema = z.object({ optimizationSuggestions: z.array( @@ -48,6 +51,8 @@ Based on the current labels and email content, suggest specific optimizations: Each suggestion should include the reason and expected impact.`; + logger.trace("Input", { system, prompt }); + const result = await chatCompletionObject({ userAi: emailAccount.user, system, @@ -57,5 +62,7 @@ Each suggestion should include the reason and expected impact.`; usageLabel: "email-report-label-analysis", }); + logger.trace("Output", result.object); + return result.object; } diff --git a/apps/web/utils/ai/report/build-user-persona.ts b/apps/web/utils/ai/report/build-user-persona.ts index b1aea2353d..3b6b376bb3 100644 --- a/apps/web/utils/ai/report/build-user-persona.ts +++ b/apps/web/utils/ai/report/build-user-persona.ts @@ -2,6 +2,9 @@ import { z } from "zod"; import { chatCompletionObject } from "@/utils/llms"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; +import { createScopedLogger } from "@/utils/logger"; + +const logger = createScopedLogger("email-report-user-persona"); const userPersonaSchema = z.object({ professionalIdentity: z.object({ @@ -63,6 +66,8 @@ Analyze the data and identify: 1. **Professional Identity**: What is their role and what evidence supports this? 2. **Current Priorities**: What are they focused on professionally based on email content?`; + logger.trace("Input", { system, prompt }); + const result = await chatCompletionObject({ userAi: emailAccount.user, system, @@ -72,5 +77,7 @@ Analyze the data and identify: usageLabel: "email-report-user-persona", }); + logger.trace("Output", result.object); + return result.object; } diff --git a/apps/web/utils/ai/report/generate-actionable-recommendations.ts b/apps/web/utils/ai/report/generate-actionable-recommendations.ts index 55633a5c45..fab7f937cb 100644 --- a/apps/web/utils/ai/report/generate-actionable-recommendations.ts +++ b/apps/web/utils/ai/report/generate-actionable-recommendations.ts @@ -3,6 +3,9 @@ import { chatCompletionObject } from "@/utils/llms"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import type { UserPersona } from "@/utils/ai/report/build-user-persona"; import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; +import { createScopedLogger } from "@/utils/logger"; + +const logger = createScopedLogger("email-report-actionable-recommendations"); const actionableRecommendationsSchema = z.object({ immediateActions: z.array( @@ -55,6 +58,8 @@ Create actionable recommendations in three categories: Focus on practical, implementable solutions that improve email organization and workflow efficiency.`; + logger.trace("Input", { system, prompt }); + const result = await chatCompletionObject({ userAi: emailAccount.user, system, @@ -64,5 +69,7 @@ Focus on practical, implementable solutions that improve email organization and usageLabel: "email-report-actionable-recommendations", }); + logger.trace("Output", result.object); + return result.object; } diff --git a/apps/web/utils/ai/report/generate-executive-summary.ts b/apps/web/utils/ai/report/generate-executive-summary.ts index 43547e5b52..a7becea3bc 100644 --- a/apps/web/utils/ai/report/generate-executive-summary.ts +++ b/apps/web/utils/ai/report/generate-executive-summary.ts @@ -3,6 +3,9 @@ import { chatCompletionObject } from "@/utils/llms"; import type { gmail_v1 } from "@googleapis/gmail"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; +import { createScopedLogger } from "@/utils/logger"; + +const logger = createScopedLogger("email-report-executive-summary"); const executiveSummarySchema = z.object({ userProfile: z.object({ @@ -133,6 +136,8 @@ Generate: 3. **Top insights** about their email behavior 4. **Quick actions** for immediate improvement`; + logger.trace("Input", { system, prompt }); + const result = await chatCompletionObject({ userAi: emailAccount.user, system, @@ -142,5 +147,7 @@ Generate: usageLabel: "email-report-executive-summary", }); + logger.trace("Output", result.object); + return result.object; } diff --git a/apps/web/utils/ai/report/response-patterns.ts b/apps/web/utils/ai/report/response-patterns.ts index b5927ac3ff..837543d97c 100644 --- a/apps/web/utils/ai/report/response-patterns.ts +++ b/apps/web/utils/ai/report/response-patterns.ts @@ -2,6 +2,9 @@ import { z } from "zod"; import { chatCompletionObject } from "@/utils/llms"; import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; import type { EmailAccountWithAI } from "@/utils/llms/types"; +import { createScopedLogger } from "@/utils/logger"; + +const logger = createScopedLogger("email-report-response-patterns"); const responsePatternsSchema = z.object({ commonResponses: z.array( @@ -43,7 +46,7 @@ export async function aiAnalyzeResponsePatterns( emailSummaries: EmailSummary[], emailAccount: EmailAccountWithAI, sentEmailSummaries?: EmailSummary[], -): Promise> { +) { const system = `You are an expert email behavior analyst. Your task is to identify common response patterns and suggest email categorization and templates based on the user's email activity. Focus on practical, actionable insights for email management including reusable templates and smart categorization. @@ -84,6 +87,8 @@ For email categorization, create simple, practical categories based on actual em Only suggest categories that are meaningful and provide clear organizational value. If emails don't fit into meaningful categories, don't create categories for them.`; + logger.trace("Input", { system, prompt }); + const result = await chatCompletionObject({ userAi: emailAccount.user, system, @@ -93,5 +98,7 @@ Only suggest categories that are meaningful and provide clear organizational val usageLabel: "email-report-response-patterns", }); + logger.trace("Output", result.object); + return result.object; } diff --git a/apps/web/utils/ai/report/summarize-emails.ts b/apps/web/utils/ai/report/summarize-emails.ts index daa4e69581..0a69c49705 100644 --- a/apps/web/utils/ai/report/summarize-emails.ts +++ b/apps/web/utils/ai/report/summarize-emails.ts @@ -6,7 +6,7 @@ import type { EmailAccountWithAI } from "@/utils/llms/types"; import { sleep } from "@/utils/sleep"; import { stringifyEmail } from "@/utils/stringify-email"; -const logger = createScopedLogger("email-report-prompts"); +const logger = createScopedLogger("email-report-summarize-emails"); const emailSummarySchema = z.object({ summary: z.string().describe("Brief summary of the email content"), From edccb7321d764a319a8cfe1e92901a11f44fdedf Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Fri, 15 Aug 2025 01:06:33 +0300 Subject: [PATCH 19/20] docs --- .cursor/rules/llm.mdc | 4 ++-- apps/web/utils/ai/assistant/chat.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.cursor/rules/llm.mdc b/.cursor/rules/llm.mdc index c0350176fd..ab141c2bdc 100644 --- a/.cursor/rules/llm.mdc +++ b/.cursor/rules/llm.mdc @@ -27,11 +27,11 @@ Follow this standard structure for LLM-related functions: import { z } from "zod"; import { createScopedLogger } from "@/utils/logger"; import { chatCompletionObject } from "@/utils/llms"; -import type { UserEmailWithAI } from "@/utils/llms/types"; +import type { EmailAccountWithAI } from "@/utils/llms/types"; export async function featureFunction(options: { inputData: InputType; - user: UserEmailWithAI; + emailAccount: EmailAccountWithAI; }) { const { inputData, user } = options; diff --git a/apps/web/utils/ai/assistant/chat.ts b/apps/web/utils/ai/assistant/chat.ts index 5d83c8c415..e8132df8a2 100644 --- a/apps/web/utils/ai/assistant/chat.ts +++ b/apps/web/utils/ai/assistant/chat.ts @@ -460,9 +460,9 @@ const updateRuleActionsTool = ({ subject: action.fields?.subject ?? null, content: action.fields?.content ?? null, webhookUrl: action.fields?.webhookUrl ?? null, - folderName: isMicrosoftProvider(provider) - ? (action.fields?.folderName ?? null) - : undefined, + ...(isMicrosoftProvider(provider) && { + folderName: action.fields?.folderName ?? null, + }), }, delayInMinutes: action.delayInMinutes ?? null, })), From 01e0a02fea0ce5136dba9fc4ddcd0597e184802c Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Fri, 15 Aug 2025 01:14:33 +0300 Subject: [PATCH 20/20] fix up ai calls --- .cursor/rules/llm.mdc | 18 +++++++++++++----- .../utils/ai/report/analyze-email-behavior.ts | 17 ++++++++++++----- .../ai/report/analyze-label-optimization.ts | 17 ++++++++++++----- apps/web/utils/ai/report/build-user-persona.ts | 17 ++++++++++++----- .../generate-actionable-recommendations.ts | 17 ++++++++++++----- .../ai/report/generate-executive-summary.ts | 17 ++++++++++++----- apps/web/utils/ai/report/response-patterns.ts | 17 ++++++++++++----- apps/web/utils/ai/report/summarize-emails.ts | 18 ++++++++++++------ 8 files changed, 97 insertions(+), 41 deletions(-) diff --git a/.cursor/rules/llm.mdc b/.cursor/rules/llm.mdc index 438fae23ca..e188052eda 100644 --- a/.cursor/rules/llm.mdc +++ b/.cursor/rules/llm.mdc @@ -28,6 +28,7 @@ import { z } from "zod"; import { createScopedLogger } from "@/utils/logger"; import { chatCompletionObject } from "@/utils/llms"; import type { EmailAccountWithAI } from "@/utils/llms/types"; +import { createGenerateObject } from "@/utils/llms"; export async function featureFunction(options: { inputData: InputType; @@ -53,9 +54,18 @@ ${emailAccount.about ? `${emailAccount.about}` : ""}`; // 7. Log inputs logger.trace("Input", { system, prompt }); - // 8. Call LLM with proper configuration - const result = await chatCompletionObject({ - userAi: emailAccount.user, + // 8. Set up and call LLM + const modelOptions = getModel(emailAccount.user); + + const generateObject = createGenerateObject({ + userEmail: emailAccount.email, + label: "Feature Name", + modelOptions, + }); + + + const result = await generateObject({ + ...modelOptions, system, prompt, schema: z.object({ @@ -66,8 +76,6 @@ ${emailAccount.about ? `${emailAccount.about}` : ""}`; }), array_field: z.array(z.string()), }), - userEmail: user.email, - usageLabel: "Feature Name", }); // 9. Log outputs diff --git a/apps/web/utils/ai/report/analyze-email-behavior.ts b/apps/web/utils/ai/report/analyze-email-behavior.ts index ca316e7656..dbdc9f6518 100644 --- a/apps/web/utils/ai/report/analyze-email-behavior.ts +++ b/apps/web/utils/ai/report/analyze-email-behavior.ts @@ -1,8 +1,9 @@ import { z } from "zod"; -import { chatCompletionObject } from "@/utils/llms"; +import { createGenerateObject } from "@/utils/llms"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; import { createScopedLogger } from "@/utils/logger"; +import { getModel } from "@/utils/llms/model"; const logger = createScopedLogger("email-report-email-behavior"); @@ -58,13 +59,19 @@ Analyze the email patterns and identify: logger.trace("Input", { system, prompt }); - const result = await chatCompletionObject({ - userAi: emailAccount.user, + const modelOptions = getModel(emailAccount.user, "economy"); + + const generateObject = createGenerateObject({ + userEmail: emailAccount.email, + label: "email-report-email-behavior", + modelOptions, + }); + + const result = await generateObject({ + ...modelOptions, system, prompt, schema: emailBehaviorSchema, - userEmail: emailAccount.email, - usageLabel: "email-report-email-behavior", }); logger.trace("Output", result.object); diff --git a/apps/web/utils/ai/report/analyze-label-optimization.ts b/apps/web/utils/ai/report/analyze-label-optimization.ts index 42e2ddc947..cf5c8e5962 100644 --- a/apps/web/utils/ai/report/analyze-label-optimization.ts +++ b/apps/web/utils/ai/report/analyze-label-optimization.ts @@ -1,9 +1,10 @@ import { z } from "zod"; -import { chatCompletionObject } from "@/utils/llms"; +import { createGenerateObject } from "@/utils/llms"; import type { gmail_v1 } from "@googleapis/gmail"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; import { createScopedLogger } from "@/utils/logger"; +import { getModel } from "@/utils/llms/model"; const logger = createScopedLogger("email-report-label-analysis"); @@ -53,13 +54,19 @@ Each suggestion should include the reason and expected impact.`; logger.trace("Input", { system, prompt }); - const result = await chatCompletionObject({ - userAi: emailAccount.user, + const modelOptions = getModel(emailAccount.user, "economy"); + + const generateObject = createGenerateObject({ + userEmail: emailAccount.email, + label: "email-report-label-analysis", + modelOptions, + }); + + const result = await generateObject({ + ...modelOptions, system, prompt, schema: labelAnalysisSchema, - userEmail: emailAccount.email, - usageLabel: "email-report-label-analysis", }); logger.trace("Output", result.object); diff --git a/apps/web/utils/ai/report/build-user-persona.ts b/apps/web/utils/ai/report/build-user-persona.ts index 3b6b376bb3..d379857852 100644 --- a/apps/web/utils/ai/report/build-user-persona.ts +++ b/apps/web/utils/ai/report/build-user-persona.ts @@ -1,8 +1,9 @@ import { z } from "zod"; -import { chatCompletionObject } from "@/utils/llms"; +import { createGenerateObject } from "@/utils/llms"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; import { createScopedLogger } from "@/utils/logger"; +import { getModel } from "@/utils/llms/model"; const logger = createScopedLogger("email-report-user-persona"); @@ -68,13 +69,19 @@ Analyze the data and identify: logger.trace("Input", { system, prompt }); - const result = await chatCompletionObject({ - userAi: emailAccount.user, + const modelOptions = getModel(emailAccount.user); + + const generateObject = createGenerateObject({ + userEmail: emailAccount.email, + label: "email-report-user-persona", + modelOptions, + }); + + const result = await generateObject({ + ...modelOptions, system, prompt, schema: userPersonaSchema, - userEmail: emailAccount.email, - usageLabel: "email-report-user-persona", }); logger.trace("Output", result.object); diff --git a/apps/web/utils/ai/report/generate-actionable-recommendations.ts b/apps/web/utils/ai/report/generate-actionable-recommendations.ts index fab7f937cb..52da88e2be 100644 --- a/apps/web/utils/ai/report/generate-actionable-recommendations.ts +++ b/apps/web/utils/ai/report/generate-actionable-recommendations.ts @@ -1,9 +1,10 @@ import { z } from "zod"; -import { chatCompletionObject } from "@/utils/llms"; +import { createGenerateObject } from "@/utils/llms"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import type { UserPersona } from "@/utils/ai/report/build-user-persona"; import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; import { createScopedLogger } from "@/utils/logger"; +import { getModel } from "@/utils/llms/model"; const logger = createScopedLogger("email-report-actionable-recommendations"); @@ -60,13 +61,19 @@ Focus on practical, implementable solutions that improve email organization and logger.trace("Input", { system, prompt }); - const result = await chatCompletionObject({ - userAi: emailAccount.user, + const modelOptions = getModel(emailAccount.user); + + const generateObject = createGenerateObject({ + userEmail: emailAccount.email, + label: "email-report-actionable-recommendations", + modelOptions, + }); + + const result = await generateObject({ + ...modelOptions, system, prompt, schema: actionableRecommendationsSchema, - userEmail: emailAccount.email, - usageLabel: "email-report-actionable-recommendations", }); logger.trace("Output", result.object); diff --git a/apps/web/utils/ai/report/generate-executive-summary.ts b/apps/web/utils/ai/report/generate-executive-summary.ts index a7becea3bc..7d9ec38cca 100644 --- a/apps/web/utils/ai/report/generate-executive-summary.ts +++ b/apps/web/utils/ai/report/generate-executive-summary.ts @@ -1,9 +1,10 @@ import { z } from "zod"; -import { chatCompletionObject } from "@/utils/llms"; +import { createGenerateObject } from "@/utils/llms"; import type { gmail_v1 } from "@googleapis/gmail"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; import { createScopedLogger } from "@/utils/logger"; +import { getModel } from "@/utils/llms/model"; const logger = createScopedLogger("email-report-executive-summary"); @@ -138,13 +139,19 @@ Generate: logger.trace("Input", { system, prompt }); - const result = await chatCompletionObject({ - userAi: emailAccount.user, + const modelOptions = getModel(emailAccount.user); + + const generateObject = createGenerateObject({ + userEmail: emailAccount.email, + label: "email-report-executive-summary", + modelOptions, + }); + + const result = await generateObject({ + ...modelOptions, system, prompt, schema: executiveSummarySchema, - userEmail: emailAccount.email, - usageLabel: "email-report-executive-summary", }); logger.trace("Output", result.object); diff --git a/apps/web/utils/ai/report/response-patterns.ts b/apps/web/utils/ai/report/response-patterns.ts index 837543d97c..a9fa67fb77 100644 --- a/apps/web/utils/ai/report/response-patterns.ts +++ b/apps/web/utils/ai/report/response-patterns.ts @@ -1,8 +1,9 @@ import { z } from "zod"; -import { chatCompletionObject } from "@/utils/llms"; +import { createGenerateObject } from "@/utils/llms"; import type { EmailSummary } from "@/utils/ai/report/summarize-emails"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import { createScopedLogger } from "@/utils/logger"; +import { getModel } from "@/utils/llms/model"; const logger = createScopedLogger("email-report-response-patterns"); @@ -89,13 +90,19 @@ Only suggest categories that are meaningful and provide clear organizational val logger.trace("Input", { system, prompt }); - const result = await chatCompletionObject({ - userAi: emailAccount.user, + const modelOptions = getModel(emailAccount.user); + + const generateObject = createGenerateObject({ + userEmail: emailAccount.email, + label: "email-report-response-patterns", + modelOptions, + }); + + const result = await generateObject({ + ...modelOptions, system, prompt, schema: responsePatternsSchema, - userEmail: emailAccount.email, - usageLabel: "email-report-response-patterns", }); logger.trace("Output", result.object); diff --git a/apps/web/utils/ai/report/summarize-emails.ts b/apps/web/utils/ai/report/summarize-emails.ts index 0a69c49705..1a5d340780 100644 --- a/apps/web/utils/ai/report/summarize-emails.ts +++ b/apps/web/utils/ai/report/summarize-emails.ts @@ -1,10 +1,11 @@ import { z } from "zod"; import { createScopedLogger } from "@/utils/logger"; -import { chatCompletionObject } from "@/utils/llms"; +import { createGenerateObject } from "@/utils/llms"; import type { EmailForLLM } from "@/utils/types"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import { sleep } from "@/utils/sleep"; import { stringifyEmail } from "@/utils/stringify-email"; +import { getModel } from "@/utils/llms/model"; const logger = createScopedLogger("email-report-summarize-emails"); @@ -81,8 +82,16 @@ Return the analysis as a JSON array of objects.`; logger.trace("Input", { system, prompt }); - const result = await chatCompletionObject({ - userAi: emailAccount.user, + const modelOptions = getModel(emailAccount.user, "economy"); + + const generateObject = createGenerateObject({ + userEmail: emailAccount.email, + label: "email-report-summary-generation", + modelOptions, + }); + + const result = await generateObject({ + ...modelOptions, system, prompt, schema: z.object({ @@ -90,9 +99,6 @@ Return the analysis as a JSON array of objects.`; .array(emailSummarySchema) .describe("Summaries of the emails"), }), - userEmail: emailAccount.email, - usageLabel: "email-report-summary-generation", - modelType: "economy", }); logger.trace("Output", { result: result.object.summaries });