From b872061528cf7f15e7e6cf544945817d36e3f536 Mon Sep 17 00:00:00 2001 From: Soumyadas15 Date: Tue, 3 Jun 2025 23:04:27 +0530 Subject: [PATCH 1/5] feat: send welcome email to users --- backend/api.py | 4 + backend/requirements.txt | 2 + backend/services/email.py | 188 +++++++++++++++++++++++++++++++ backend/services/email_api.py | 70 ++++++++++++ frontend/src/app/auth/actions.ts | 33 +++++- 5 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 backend/services/email.py create mode 100644 backend/services/email_api.py diff --git a/backend/api.py b/backend/api.py index 0e8cc709..75f7c295 100644 --- a/backend/api.py +++ b/backend/api.py @@ -19,6 +19,8 @@ from agent import api as agent_api from sandbox import api as sandbox_api from services import billing as billing_api from services import transcription as transcription_api +from services import email_api + # Load environment variables (these will be available through config) load_dotenv() @@ -136,6 +138,8 @@ app.include_router(billing_api.router, prefix="/api") # Include the transcription router with a prefix app.include_router(transcription_api.router, prefix="/api") +app.include_router(email_api.router, prefix="/api") + @app.get("/api/health") async def health_check(): """Health check endpoint to verify API is working.""" diff --git a/backend/requirements.txt b/backend/requirements.txt index 3e7d0eba..3e6e6464 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -35,3 +35,5 @@ prometheus-client>=0.21.1 langfuse>=2.60.5 Pillow>=10.0.0 sentry-sdk[fastapi]>=2.29.1 +email-validator>=2.0.0 +mailtrap>=3.0.0 \ No newline at end of file diff --git a/backend/services/email.py b/backend/services/email.py new file mode 100644 index 00000000..9e9230df --- /dev/null +++ b/backend/services/email.py @@ -0,0 +1,188 @@ +import os +import logging +from typing import Optional +import mailtrap as mt +from utils.config import config + +logger = logging.getLogger(__name__) + +class EmailService: + def __init__(self): + self.api_token = os.getenv('MAILTRAP_API_TOKEN') + self.sender_email = os.getenv('MAILTRAP_SENDER_EMAIL', 'dom@kortix.ai') + self.sender_name = os.getenv('MAILTRAP_SENDER_NAME', 'Suna Team') + + if not self.api_token: + logger.warning("MAILTRAP_API_TOKEN not found in environment variables") + self.client = None + else: + self.client = mt.MailtrapClient(token=self.api_token) + + def send_welcome_email(self, user_email: str, user_name: Optional[str] = None) -> bool: + if not self.client: + logger.error("Cannot send email: MAILTRAP_API_TOKEN not configured") + return False + + if not user_name: + user_name = user_email.split('@')[0].title() + + subject = "🎉 Welcome to Suna — Let's Get Started " + html_content = self._get_welcome_email_template(user_name) + text_content = self._get_welcome_email_text(user_name) + + return self._send_email( + to_email=user_email, + to_name=user_name, + subject=subject, + html_content=html_content, + text_content=text_content + ) + + def _send_email( + self, + to_email: str, + to_name: str, + subject: str, + html_content: str, + text_content: str + ) -> bool: + try: + mail = mt.Mail( + sender=mt.Address(email=self.sender_email, name=self.sender_name), + to=[mt.Address(email=to_email, name=to_name)], + subject=subject, + text=text_content, + html=html_content, + category="welcome" + ) + + response = self.client.send(mail) + + logger.info(f"Welcome email sent to {to_email}. Response: {response}") + return True + + except Exception as e: + logger.error(f"Error sending email to {to_email}: {str(e)}") + return False + + def _get_welcome_email_template(self, user_name: str) -> str: + return f""" + + + + + Welcome to Kortix Suna + + + +
+
+ +
+

Welcome to Kortix Suna!

+ +

Hi {user_name},

+ +

Welcome to Kortix Suna — we're excited to have you on board!

+ +

To get started, we'd like to get to know you better: fill out this short form!

+ +

To celebrate your arrival, here's a 15% discount to try out the best version of Suna (1 month):

+ +

🎁 Use code WELCOME15 at checkout.

+ +

Let us know if you need help getting started or have questions — we're always here, and join our Discord community.

+ +

Thanks again, and welcome to the Suna community 🌞

+ +

— The Suna Team

+ + Go to the platform +
+ +""" + + def _get_welcome_email_text(self, user_name: str) -> str: + return f"""Hi {user_name}, + +Welcome to Suna — we're excited to have you on board! + +To get started, we'd like to get to know you better: fill out this short form! +https://docs.google.com/forms/d/e/1FAIpQLSef1EHuqmIh_iQz-kwhjnzSC3Ml-V_5wIySDpMoMU9W_j24JQ/viewform + +To celebrate your arrival, here's a 15% discount to try out the best version of Suna (1 month): +🎁 Use code WELCOME15 at checkout. + +Let us know if you need help getting started or have questions — we're always here, and join our Discord community: https://discord.com/invite/FjD644cfcs + +Thanks again, and welcome to the Suna community 🌞 + +— The Suna Team + +Go to the platform: https://www.suna.so/ + +--- +© 2024 Suna. All rights reserved. +You received this email because you signed up for a Suna account.""" + +email_service = EmailService() diff --git a/backend/services/email_api.py b/backend/services/email_api.py new file mode 100644 index 00000000..9834c7ba --- /dev/null +++ b/backend/services/email_api.py @@ -0,0 +1,70 @@ +from fastapi import APIRouter, HTTPException, Depends +from pydantic import BaseModel, EmailStr +from typing import Optional +import asyncio +from services.email import email_service +from utils.logger import logger + +router = APIRouter() + +class SendWelcomeEmailRequest(BaseModel): + email: EmailStr + name: Optional[str] = None + +class EmailResponse(BaseModel): + success: bool + message: str + +@router.post("/send-welcome-email", response_model=EmailResponse) +async def send_welcome_email(request: SendWelcomeEmailRequest): + try: + logger.info(f"Sending welcome email to {request.email}") + success = email_service.send_welcome_email( + user_email=request.email, + user_name=request.name + ) + + if success: + return EmailResponse( + success=True, + message="Welcome email sent successfully" + ) + else: + return EmailResponse( + success=False, + message="Failed to send welcome email" + ) + + except Exception as e: + logger.error(f"Error sending welcome email to {request.email}: {str(e)}") + raise HTTPException( + status_code=500, + detail="Internal server error while sending email" + ) + +@router.post("/send-welcome-email-background", response_model=EmailResponse) +async def send_welcome_email_background(request: SendWelcomeEmailRequest): + try: + logger.info(f"Queuing welcome email for {request.email}") + + def send_email(): + return email_service.send_welcome_email( + user_email=request.email, + user_name=request.name + ) + + import concurrent.futures + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit(send_email) + + return EmailResponse( + success=True, + message="Welcome email queued for sending" + ) + + except Exception as e: + logger.error(f"Error queuing welcome email for {request.email}: {str(e)}") + raise HTTPException( + status_code=500, + detail="Internal server error while queuing email" + ) diff --git a/frontend/src/app/auth/actions.ts b/frontend/src/app/auth/actions.ts index 0d092fad..f9ed1074 100644 --- a/frontend/src/app/auth/actions.ts +++ b/frontend/src/app/auth/actions.ts @@ -3,6 +3,30 @@ import { createClient } from '@/lib/supabase/server'; import { redirect } from 'next/navigation'; +async function sendWelcomeEmail(email: string, name?: string) { + try { + const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL; + const response = await fetch(`${backendUrl}/send-welcome-email-background`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + name, + }), + }); + + if (response.ok) { + console.log(`Welcome email queued for ${email}`); + } else { + console.error(`Failed to queue welcome email for ${email}`); + } + } catch (error) { + console.error('Error sending welcome email:', error); + } +} + export async function signIn(prevState: any, formData: FormData) { const email = formData.get('email') as string; const password = formData.get('password') as string; @@ -64,12 +88,17 @@ export async function signUp(prevState: any, formData: FormData) { return { message: error.message || 'Could not create account' }; } - // Try to sign in immediately - const { error: signInError } = await supabase.auth.signInWithPassword({ + const userName = email.split('@')[0].replace(/[._-]/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); + + const { error: signInError, data: signInData } = await supabase.auth.signInWithPassword({ email, password, }); + if (signInData) { + sendWelcomeEmail(email, userName); + } + if (signInError) { return { message: From e371ef7df810af02576de36af02850f2c633f284 Mon Sep 17 00:00:00 2001 From: Soumyadas15 Date: Tue, 3 Jun 2025 23:44:08 +0530 Subject: [PATCH 2/5] hotfix: mailtrap version fix --- backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 3e6e6464..2f2430dd 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -36,4 +36,4 @@ langfuse>=2.60.5 Pillow>=10.0.0 sentry-sdk[fastapi]>=2.29.1 email-validator>=2.0.0 -mailtrap>=3.0.0 \ No newline at end of file +mailtrap>=2.0.1 \ No newline at end of file From 72759e06c7495db394efaa539cf5693646c140d6 Mon Sep 17 00:00:00 2001 From: sharath <29162020+tnfssc@users.noreply.github.com> Date: Thu, 5 Jun 2025 01:23:57 +0000 Subject: [PATCH 3/5] fix(run_agent_background): optimize Redis operation handling and increase worker processes --- backend/docker-compose.prod.yml | 2 +- backend/run_agent_background.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/backend/docker-compose.prod.yml b/backend/docker-compose.prod.yml index 7c3d63a7..be1ad790 100644 --- a/backend/docker-compose.prod.yml +++ b/backend/docker-compose.prod.yml @@ -10,7 +10,7 @@ services: memory: 32G worker: - command: python -m dramatiq --processes 20 --threads 16 run_agent_background + command: python -m dramatiq --processes 40 --threads 8 run_agent_background deploy: resources: limits: diff --git a/backend/run_agent_background.py b/backend/run_agent_background.py index e957cf9c..2c3dcd48 100644 --- a/backend/run_agent_background.py +++ b/backend/run_agent_background.py @@ -124,6 +124,8 @@ async def run_agent_background( final_status = "running" error_message = None + pending_redis_operations = [] + async for response in agent_gen: if stop_signal_received: logger.info(f"Agent run {agent_run_id} stopped by signal.") @@ -133,8 +135,8 @@ async def run_agent_background( # Store response in Redis list and publish notification response_json = json.dumps(response) - asyncio.create_task(redis.rpush(response_list_key, response_json)) - asyncio.create_task(redis.publish(response_channel, "new")) + pending_redis_operations.append(redis.rpush(response_list_key, response_json)) + pending_redis_operations.append(redis.publish(response_channel, "new")) total_responses += 1 # Check for agent-signaled completion or error @@ -231,8 +233,11 @@ async def run_agent_background( # Remove the instance-specific active run key await _cleanup_redis_instance_key(agent_run_id) - # Wait for 5 seconds for any pending redis operations to complete - await asyncio.sleep(5) + # Wait for all pending redis operations to complete, with timeout + try: + await asyncio.wait_for(asyncio.gather(*pending_redis_operations), timeout=5.0) + except asyncio.TimeoutError: + logger.warning(f"Timeout waiting for pending Redis operations for {agent_run_id}") logger.info(f"Agent run background task fully completed for: {agent_run_id} (Instance: {instance_id}) with final status: {final_status}") From 27be48ed4696d5db8f6ebd7d20a6bbdb8a0672d5 Mon Sep 17 00:00:00 2001 From: Sharath <29162020+tnfssc@users.noreply.github.com> Date: Thu, 5 Jun 2025 07:23:49 +0530 Subject: [PATCH 4/5] fix(prod): change process count --- backend/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/docker-compose.prod.yml b/backend/docker-compose.prod.yml index be1ad790..add788ee 100644 --- a/backend/docker-compose.prod.yml +++ b/backend/docker-compose.prod.yml @@ -10,7 +10,7 @@ services: memory: 32G worker: - command: python -m dramatiq --processes 40 --threads 8 run_agent_background + command: python -m dramatiq --processes 10 --threads 32 run_agent_background deploy: resources: limits: From f3a2ce8de71202d6ac8caa238291e063b2ede987 Mon Sep 17 00:00:00 2001 From: sharath <29162020+tnfssc@users.noreply.github.com> Date: Thu, 5 Jun 2025 02:00:39 +0000 Subject: [PATCH 5/5] fix(run_agent_background): update Redis operation handling to use asyncio tasks --- backend/run_agent_background.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/run_agent_background.py b/backend/run_agent_background.py index 2c3dcd48..cf9715b9 100644 --- a/backend/run_agent_background.py +++ b/backend/run_agent_background.py @@ -135,8 +135,8 @@ async def run_agent_background( # Store response in Redis list and publish notification response_json = json.dumps(response) - pending_redis_operations.append(redis.rpush(response_list_key, response_json)) - pending_redis_operations.append(redis.publish(response_channel, "new")) + pending_redis_operations.append(asyncio.create_task(redis.rpush(response_list_key, response_json))) + pending_redis_operations.append(asyncio.create_task(redis.publish(response_channel, "new"))) total_responses += 1 # Check for agent-signaled completion or error