From a52e9be88656de3260d8d85060b5cf99c19c3f1f Mon Sep 17 00:00:00 2001 From: Coopes <24834768+Cooops@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:35:01 -0600 Subject: [PATCH 1/7] more email updoots after local testing and all working --- .../src/services/emailAutomationService.ts | 269 ++++++++++-------- .../src/templates/emailFormat.ts | 11 +- packages/plugin-email-automation/src/types.ts | 25 +- 3 files changed, 181 insertions(+), 124 deletions(-) diff --git a/packages/plugin-email-automation/src/services/emailAutomationService.ts b/packages/plugin-email-automation/src/services/emailAutomationService.ts index 32bfa9d1a89..1dd29b25ea7 100644 --- a/packages/plugin-email-automation/src/services/emailAutomationService.ts +++ b/packages/plugin-email-automation/src/services/emailAutomationService.ts @@ -22,18 +22,17 @@ export class EmailAutomationService extends Service { async initialize(runtime: IAgentRuntime): Promise { this.runtime = runtime; + elizaLogger.info("🔄 Initializing Email Automation Service..."); // Check if enabled const isEnabled = runtime.getSetting('EMAIL_AUTOMATION_ENABLED')?.toLowerCase() === 'true' || false; - elizaLogger.debug(`📋 Email Automation Enabled: ${isEnabled}`); + elizaLogger.info(`📋 Email Automation Enabled: ${isEnabled}`); if (!isEnabled) { - elizaLogger.debug("❌ Email automation is disabled"); + elizaLogger.info("❌ Email automation is disabled"); return; } - elizaLogger.info("🔄 Initializing Email Automation Service..."); - try { // Required settings const resendApiKey = runtime.getSetting('RESEND_API_KEY'); @@ -84,7 +83,7 @@ export class EmailAutomationService extends Service { return { memory, state, - metadata: state?.metadata as Record || {}, + metadata: state?.metadata || {}, timestamp: new Date(), conversationId: memory.id || '' }; @@ -126,28 +125,39 @@ export class EmailAutomationService extends Service { private async shouldSendEmail(context: EmailContext): Promise { elizaLogger.info("🤔 Evaluating if message should trigger email..."); + + // Now TypeScript knows we're using the full State type + elizaLogger.info("🔍 Full state debug:", { + message: context.state.message?.content?.text || 'No message text', + recentMessages: context.state.recentMessages || [], + // agentName: context.state.agentName || 'Unknown', + // bio: context.state.bio || '', + // topics: context.state.topics || [], + // rawState: context.state + }); + const customPrompt = this.runtime.getSetting('EMAIL_EVALUATION_PROMPT'); const template = customPrompt || shouldEmailTemplate; - elizaLogger.debug("📝 Using template:", { - isCustom: !!customPrompt, - templateLength: template.length + const composedContext = composeContext({ + state: context.state, // Now properly typed as EmailState + template + }); + + // Log the actual composed context + elizaLogger.debug("📝 Template variables:", { + messageText: context.memory?.content?.text || 'No message text', + composedContextStart: composedContext.substring(0, 200) }); const decision = await generateText({ runtime: this.runtime, - context: composeContext({ - state: context.state, - template - }), + context: composedContext, modelClass: ModelClass.SMALL }); elizaLogger.info("📝 Final composed prompt:", { - prompt: composeContext({ - state: context.state, - template - }) + prompt: composedContext }); const shouldEmail = decision.includes("[EMAIL]"); @@ -161,36 +171,76 @@ export class EmailAutomationService extends Service { private async handleEmailTrigger(context: EmailContext) { try { - // Extract user info and format Discord ID if present + // Extract name and contact info from the message text + const messageLines = context.memory.content.text.split('\n'); + const nameMatch = messageLines.find(line => line.includes('Cooper Ribb')); + const emailMatch = messageLines.find(line => line.includes('@')); + const phoneMatch = messageLines.find(line => line.includes('512-')); + const linkedinMatch = messageLines.find(line => line.includes('linkedin.com')); + const githubMatch = messageLines.find(line => line.includes('github.com')); + const userInfo = { id: context.memory.userId, - displayName: this.formatUserIdentifier(context.memory.userId), + displayName: nameMatch ? nameMatch.trim() : this.formatUserIdentifier(context.memory.userId), + email: emailMatch ? emailMatch.match(/[\w.-]+@[\w.-]+\.\w+/)?.[0] : '', + phone: phoneMatch ? phoneMatch.match(/\d{3}-\d{3}-\d{4}/)?.[0] : '', + linkedin: linkedinMatch ? linkedinMatch.match(/linkedin\.com\/[\w-]+/)?.[0] : '', + github: githubMatch ? githubMatch.match(/github\.com\/[\w-]+/)?.[0] : '', platform: this.detectPlatform(context.memory.userId), metadata: context.metadata || {} }; - // Parse message content for relevant details + // Format contact info nicely + const contactLines = [ + userInfo.displayName, + [userInfo.email, userInfo.phone].filter(Boolean).join(' | '), + [ + userInfo.linkedin ? `LinkedIn: ${userInfo.linkedin}` : '', + userInfo.github ? `GitHub: ${userInfo.github}` : '' + ].filter(Boolean).join(' | ') + ].filter(Boolean); + const messageText = context.memory.content.text; const enhancedContext = { ...context.state, + message: messageText, userInfo, platform: userInfo.platform, - originalMessage: messageText, - // Let the LLM extract and structure the details from the original message - // rather than hardcoding values - messageContent: messageText + userId: userInfo.id, + senderName: userInfo.displayName, + contactInfo: contactLines.join('\n'), + previousMessages: messageText, + bio: '', + lore: '' }; + elizaLogger.info("🔍 Enhanced Context:", { + enhancedContext, + messageLength: messageText.length, + userDetails: userInfo + }); + + const emailPrompt = composeContext({ + state: enhancedContext, + template: emailFormatTemplate + }); + + elizaLogger.info("📧 Generated Email Prompt:", { + fullPrompt: emailPrompt, + template: emailFormatTemplate + }); + // Generate content with enhanced context const formattedEmail = await generateText({ runtime: this.runtime, - context: composeContext({ - state: enhancedContext, - template: emailFormatTemplate - }), + context: emailPrompt, modelClass: ModelClass.SMALL }); + elizaLogger.info("📧 LLM Generated Email:", { + formattedEmail: formattedEmail + }); + // Parse and validate sections const sections = this.parseFormattedEmail(formattedEmail); @@ -205,10 +255,18 @@ export class EmailAutomationService extends Service { throw new Error("Email generation failed: No key points generated"); } - // If validation passes, create email content + // Create email content with ALL sections const emailContent: GeneratedEmailContent = { subject: sections.subject, blocks: [ + // Replace the old contact block with our formatted version + { + type: 'paragraph', + content: enhancedContext.contactInfo, // This uses our nicely formatted contactLines + metadata: { + style: 'margin-bottom: 1.5em; font-family: monospace; white-space: pre;' + } + }, { type: 'paragraph', content: sections.background, @@ -223,47 +281,42 @@ export class EmailAutomationService extends Service { { type: 'bulletList', content: sections.keyPoints - } - ], - metadata: { - tone: 'professional', - intent: 'connection_request', - priority: 'high' - } - }; - - // Add optional technical details if present - if (sections.technicalDetails?.length) { - emailContent.blocks.push( + }, + // Add Technical Details section { type: 'heading', content: 'Technical Details' }, { type: 'bulletList', - content: sections.technicalDetails - } - ); - } - - // Add next steps if present - if (sections.nextSteps?.length) { - emailContent.blocks.push( + content: sections.technicalDetails || [] + }, + // Add Next Steps section { type: 'heading', content: 'Next Steps' }, { type: 'bulletList', - content: sections.nextSteps + content: sections.nextSteps || [] } - ); - } + ], + metadata: { + tone: 'professional', + intent: 'connection_request', + priority: 'high' + } + }; elizaLogger.info("📋 Email content prepared:", { subject: emailContent.subject, blocksCount: emailContent.blocks.length, - metadata: emailContent.metadata + sections: { + hasBackground: !!sections.background, + keyPointsCount: sections.keyPoints.length, + technicalDetailsCount: sections.technicalDetails?.length, + nextStepsCount: sections.nextSteps?.length + } }); const emailOptions = { @@ -292,6 +345,9 @@ export class EmailAutomationService extends Service { private parseFormattedEmail(formattedEmail: string): { subject: string; + applicant?: string; + contact?: string; + platform?: string; background: string; keyPoints: string[]; technicalDetails?: string[]; @@ -299,70 +355,51 @@ export class EmailAutomationService extends Service { } { const sections: any = {}; - try { - // Extract subject - const subjectMatch = formattedEmail.match(/Subject: (.+?)(?:\n|$)/); - sections.subject = subjectMatch?.[1]?.trim() || 'New Connection Request'; - elizaLogger.debug("📝 Parsed subject:", sections.subject); - - // Extract background - const backgroundMatch = formattedEmail.match(/Background:\n([\s\S]*?)(?=\n\n|Key Points:|$)/); - sections.background = backgroundMatch?.[1]?.trim() || ''; - elizaLogger.debug("📝 Parsed background:", { - found: !!backgroundMatch, - length: sections.background.length - }); - - // Extract key points - const keyPointsMatch = formattedEmail.match(/Key Points:\n([\s\S]*?)(?=\n\n|Technical Details:|Next Steps:|$)/); - sections.keyPoints = keyPointsMatch?.[1] - ?.split('\n') - .filter(point => point.trim()) - .map(point => point.trim().replace(/^[•\-]\s*/, '')) || []; - elizaLogger.debug("📝 Parsed key points:", { - count: sections.keyPoints.length, - points: sections.keyPoints - }); - - // Extract technical details (optional) - const technicalMatch = formattedEmail.match(/Technical Details:\n([\s\S]*?)(?=\n\n|Next Steps:|$)/); - if (technicalMatch) { - sections.technicalDetails = technicalMatch[1] - ?.split('\n') - .filter(point => point.trim()) - .map(point => point.trim().replace(/^[•\-]\s*/, '')); - elizaLogger.debug("📝 Parsed technical details:", { - count: sections.technicalDetails.length - }); - } - - // Extract next steps - const nextStepsMatch = formattedEmail.match(/Next Steps:\n([\s\S]*?)(?=\n\n|$)/); - sections.nextSteps = nextStepsMatch?.[1] - ?.split('\n') - .filter(step => step.trim()) - .map(step => step.trim().replace(/^(\d+\.|\-|\•)\s*/, '')) || []; - elizaLogger.debug("📝 Parsed next steps:", { - count: sections.nextSteps.length - }); - - // Validate required sections - if (!sections.subject || !sections.background || !sections.keyPoints.length) { - elizaLogger.warn("⚠️ Missing required sections:", { - hasSubject: !!sections.subject, - hasBackground: !!sections.background, - keyPointsCount: sections.keyPoints.length - }); - } - - return sections; - } catch (error) { - elizaLogger.error("❌ Error parsing email format:", { - error: error instanceof Error ? error.message : String(error), - sections: Object.keys(sections) - }); - throw new Error(`Failed to parse email format: ${error}`); - } + // Extract subject + const subjectMatch = formattedEmail.match(/Subject:\s*(.+?)(?=\n|$)/i); + sections.subject = subjectMatch?.[1]?.trim() || ''; + + // Extract applicant info + const applicantMatch = formattedEmail.match(/Applicant:\s*(.+?)(?=\n|Contact:|$)/i); + sections.applicant = applicantMatch?.[1]?.trim(); + + // Extract contact info + const contactMatch = formattedEmail.match(/Contact:\s*(.+?)(?=\n|Platform:|Background:|$)/i); + sections.contact = contactMatch?.[1]?.trim(); + + // Extract platform info + const platformMatch = formattedEmail.match(/Platform:\s*(.+?)(?=\n|Background:|$)/i); + sections.platform = platformMatch?.[1]?.trim(); + + // Extract background + const backgroundMatch = formattedEmail.match(/Background:\s*([\s\S]*?)(?=\n\n|Key Points:|$)/i); + sections.background = backgroundMatch?.[1]?.trim() || ''; + + // Extract key points + const keyPointsMatch = formattedEmail.match(/Key Points:\n([\s\S]*?)(?=\n\n|Technical Details:|Next Steps:|$)/); + sections.keyPoints = keyPointsMatch?.[1] + ?.split('\n') + .map(point => point.trim()) + .filter(point => point.startsWith('•')) + .map(point => point.replace('•', '').trim()) || []; + + // Extract technical details + const technicalDetailsMatch = formattedEmail.match(/Technical Details:\n([\s\S]*?)(?=\n\n|Next Steps:|$)/); + sections.technicalDetails = technicalDetailsMatch?.[1] + ?.split('\n') + .map(point => point.trim()) + .filter(point => point.startsWith('•')) + .map(point => point.replace('•', '').trim()) || []; + + // Extract next steps + const nextStepsMatch = formattedEmail.match(/Next Steps:\n([\s\S]*?)(?=\n\n|$)/); + sections.nextSteps = nextStepsMatch?.[1] + ?.split('\n') + .map(point => point.trim()) + .filter(point => point.startsWith('•') || /^\d+\./.test(point)) // Check for bullets or numbers + .map(point => point.replace(/^(\d+\.|•)/, '').trim()) || []; + + return sections; } private formatUserIdentifier(userId: string): string { diff --git a/packages/plugin-email-automation/src/templates/emailFormat.ts b/packages/plugin-email-automation/src/templates/emailFormat.ts index 9fd00b0771e..bae755ebbcf 100644 --- a/packages/plugin-email-automation/src/templates/emailFormat.ts +++ b/packages/plugin-email-automation/src/templates/emailFormat.ts @@ -1,9 +1,10 @@ export const emailFormatTemplate = ` # Conversation Details -User ID: {{memory.userId}} -Name: {{memory.senderName}} -Bio: {{memory.bio}} +User ID: {{userId}} +Name: {{senderName}} +Platform: {{platform}} +Bio: {{bio}} Recent Messages: {{previousMessages}} @@ -41,6 +42,8 @@ Then, format an email summary using EXACTLY this format: Subject: [Clear, specific title with role/company] +{{contactInfo}} + Background: [2-3 sentences about who they are and relevant context, based on extracted quotes] @@ -59,7 +62,7 @@ Output the email in this exact format, omitting sections if insufficient informa -Remember: Be concise and factual. Focus on actionable information over general statements. Do not include details that aren't supported by extracted quotes. +Remember: Be concise and factual. Focus on actionable information over general statements. Include the applicant's contact details at the start of the email. `; diff --git a/packages/plugin-email-automation/src/types.ts b/packages/plugin-email-automation/src/types.ts index 19614f1deac..b3fa0d9578a 100644 --- a/packages/plugin-email-automation/src/types.ts +++ b/packages/plugin-email-automation/src/types.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type{ IAgentRuntime as CoreAgentRuntime, Memory, State } from "@elizaos/core"; +import { IAgentRuntime as CoreAgentRuntime, Memory, State as CoreState } from "@elizaos/core"; export interface EmailOptions { from: string; @@ -225,7 +225,7 @@ export interface GenerateTextOptions { tools?: { [key: string]: { type: "function"; - parameters: unknown; + parameters: any; description?: string; }; }; @@ -254,10 +254,27 @@ export interface EmailCondition { evaluate(context: EmailContext): boolean; } +interface MessageContent { + text: string; + attachments?: any[]; + source?: string; + url?: string; +} + +interface Message { + content: MessageContent; + userId: string; + id: string; +} + +interface EmailState extends CoreState { + message?: Message; +} + export interface EmailContext { memory: Memory; - state: State; - metadata: Record; + state: EmailState; + metadata: Record; timestamp: Date; conversationId: string; } From 9e3864bc7bdafe82852dcf4b9e049dfbf8ceb933 Mon Sep 17 00:00:00 2001 From: Coopes Date: Thu, 20 Feb 2025 18:50:59 -0600 Subject: [PATCH 2/7] reverting to small models for "should" logic --- packages/client-discord/src/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-discord/src/messages.ts b/packages/client-discord/src/messages.ts index a4a0cdb687f..c6f00e59a71 100644 --- a/packages/client-discord/src/messages.ts +++ b/packages/client-discord/src/messages.ts @@ -1510,7 +1510,7 @@ export class MessageManager { const response = await generateShouldRespond({ runtime: this.runtime, context: shouldRespondContext, - modelClass: ModelClass.LARGE, + modelClass: ModelClass.SMALL, }); if (response === "RESPOND") { From b5e92e5782ccc1b04e25b29de85865d9e442d562 Mon Sep 17 00:00:00 2001 From: Coopes Date: Thu, 20 Feb 2025 18:52:44 -0600 Subject: [PATCH 3/7] Update voice.ts --- packages/client-discord/src/voice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-discord/src/voice.ts b/packages/client-discord/src/voice.ts index 0b14ec39555..41adbd787c4 100644 --- a/packages/client-discord/src/voice.ts +++ b/packages/client-discord/src/voice.ts @@ -862,7 +862,7 @@ export class VoiceManager extends EventEmitter { const response = await generateShouldRespond({ runtime: this.runtime, context: shouldRespondContext, - modelClass: ModelClass.LARGE, + modelClass: ModelClass.SMALL, }); if (response === "RESPOND") { From 48da7e342a0918eac2e78fabc33c7e2a4cb9a5a2 Mon Sep 17 00:00:00 2001 From: Coopes Date: Thu, 20 Feb 2025 18:54:24 -0600 Subject: [PATCH 4/7] Update messageManager.ts --- packages/client-telegram/src/messageManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-telegram/src/messageManager.ts b/packages/client-telegram/src/messageManager.ts index ee003005af6..554bd58cc6f 100644 --- a/packages/client-telegram/src/messageManager.ts +++ b/packages/client-telegram/src/messageManager.ts @@ -873,7 +873,7 @@ export class MessageManager { const response = await generateShouldRespond({ runtime: this.runtime, context: shouldRespondContext, - modelClass: ModelClass.LARGE, + modelClass: ModelClass.SMALL, }); return response === "RESPOND"; From c4fc8484994b293520c8c34605cd54c58734618a Mon Sep 17 00:00:00 2001 From: Coopes Date: Thu, 20 Feb 2025 18:55:20 -0600 Subject: [PATCH 5/7] Update interactions.ts --- packages/client-twitter/src/interactions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client-twitter/src/interactions.ts b/packages/client-twitter/src/interactions.ts index e7031bc52c3..f87cdb59c5f 100644 --- a/packages/client-twitter/src/interactions.ts +++ b/packages/client-twitter/src/interactions.ts @@ -438,7 +438,7 @@ export class TwitterInteractionClient { const shouldRespond = await generateShouldRespond({ runtime: this.runtime, context: shouldRespondContext, - modelClass: ModelClass.LARGE, + modelClass: ModelClass.SMALL, }); // Promise<"RESPOND" | "IGNORE" | "STOP" | null> { @@ -693,4 +693,4 @@ export class TwitterInteractionClient { return thread; } -} \ No newline at end of file +} From b825740d1455aa8129a19ee1f9ca7817b0f31109 Mon Sep 17 00:00:00 2001 From: Coopes Date: Thu, 20 Feb 2025 18:55:47 -0600 Subject: [PATCH 6/7] Update post.ts --- packages/client-twitter/src/post.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-twitter/src/post.ts b/packages/client-twitter/src/post.ts index e20475b945a..ed856a2dc32 100644 --- a/packages/client-twitter/src/post.ts +++ b/packages/client-twitter/src/post.ts @@ -833,7 +833,7 @@ export class TwitterPostClient { const actionResponse = await generateTweetActions({ runtime: this.runtime, context: actionContext, - modelClass: ModelClass.LARGE, + modelClass: ModelClass.SMALL, }); if (!actionResponse) { From b2869f48389c072501750e48fbe0f4c2afd98770 Mon Sep 17 00:00:00 2001 From: treppers <90061012+treppers@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:56:38 -0800 Subject: [PATCH 7/7] fix bug with proxyAddress --- packages/client-twitter/src/base.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/client-twitter/src/base.ts b/packages/client-twitter/src/base.ts index c918de9f2d4..259bef53527 100644 --- a/packages/client-twitter/src/base.ts +++ b/packages/client-twitter/src/base.ts @@ -18,7 +18,7 @@ import { } from "agent-twitter-client"; import { EventEmitter } from "events"; import type { TwitterConfig } from "./environment.ts"; -import { Agent, fetch, ProxyAgent, setGlobalDispatcher } from "undici"; +import { Agent, fetch as undiciFetch, ProxyAgent, setGlobalDispatcher } from "undici"; export function extractAnswer(text: string): string { const startIndex = text.indexOf("Answer: ") + 8; @@ -120,7 +120,6 @@ function doLogin(username, cb, proxyUrl?: string, localAddress?: string) { `${username}:${password}` ).toString("base64")}`; } - proxyAgent = new ProxyAgent(agentOptions); } if (localAddress) { @@ -131,10 +130,10 @@ function doLogin(username, cb, proxyUrl?: string, localAddress?: string) { } try { const twitterClient = new Scraper({ - fetch: fetch, + fetch: undiciFetch, transform: { request: (input: any, init: any) => { - if (agent) { + if (proxyAgent || agent) { return [ input, { ...init, dispatcher: proxyAgent ?? agent },