Merge branch 'PRODUCTION' into sync/production

This commit is contained in:
sharath 2025-06-05 02:15:28 +00:00
commit ec09c6d149
No known key found for this signature in database
9 changed files with 363 additions and 9 deletions

View File

@ -22,6 +22,8 @@ from services import billing as billing_api
from services import transcription as transcription_api
from services.mcp_custom import discover_custom_tools
import sys
from services import email_api
load_dotenv()
@ -136,6 +138,8 @@ app.include_router(mcp_api.router, prefix="/api")
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."""

View File

@ -10,7 +10,7 @@ services:
memory: 32G
worker:
command: python -m dramatiq --processes 20 --threads 16 run_agent_background
command: python -m dramatiq --processes 10 --threads 32 run_agent_background
deploy:
resources:
limits:

54
backend/poetry.lock generated
View File

@ -543,6 +543,27 @@ files = [
{file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
]
[[package]]
name = "dnspython"
version = "2.7.0"
description = "DNS toolkit"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"},
{file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"},
]
[package.extras]
dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"]
dnssec = ["cryptography (>=43)"]
doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"]
doq = ["aioquic (>=1.0.0)"]
idna = ["idna (>=3.7)"]
trio = ["trio (>=0.23)"]
wmi = ["wmi (>=1.5.1)"]
[[package]]
name = "dramatiq"
version = "1.17.1"
@ -605,6 +626,22 @@ attrs = ">=21.3.0"
e2b = ">=1.3.1,<2.0.0"
httpx = ">=0.20.0,<1.0.0"
[[package]]
name = "email-validator"
version = "2.2.0"
description = "A robust email address syntax and deliverability validation library."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"},
{file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"},
]
[package.dependencies]
dnspython = ">=2.0.0"
idna = ">=2.0.0"
[[package]]
name = "entrypoints"
version = "0.4"
@ -1297,6 +1334,21 @@ tokenizers = "*"
extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0) ; python_version >= \"3.9\" and python_version < \"3.14\"", "resend (>=0.8.0,<0.9.0)"]
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-proxy-extras (==0.1.7)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"]
[[package]]
name = "mailtrap"
version = "2.1.0"
description = "Official mailtrap.io API client"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "mailtrap-2.1.0-py3-none-any.whl", hash = "sha256:6cef8dc02734e3e3a16161e38d184ea6971e925673c731c8ac968b88556f069e"},
{file = "mailtrap-2.1.0.tar.gz", hash = "sha256:22fccf3cd912a7e47d4a1bb86865cf0f0587d59dc73bc78d9e77d596767f5b85"},
]
[package.dependencies]
requests = ">=2.26.0"
[[package]]
name = "markupsafe"
version = "3.0.2"
@ -3719,4 +3771,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.1"
python-versions = "^3.11"
content-hash = "ed0ccb92ccc81ecff536968c883a2ea96d2ee8a6c06a18d0023b0cd4185f8a28"
content-hash = "70ef4a9be6ddd82debb9e9e377d9f47f6f6917b43b75a688e404adeef9e61018"

View File

@ -56,6 +56,10 @@ langfuse = "^2.60.5"
Pillow = "^10.0.0"
mcp = "^1.0.0"
sentry-sdk = {extras = ["fastapi"], version = "^2.29.1"}
httpx = "^0.28.0"
aiohttp = "^3.9.0"
email-validator = "^2.0.0"
mailtrap = "^2.0.1"
[tool.poetry.scripts]
agentpress = "agentpress.cli:main"

View File

@ -38,4 +38,6 @@ Pillow>=10.0.0
sentry-sdk[fastapi]>=2.29.1
mcp>=1.0.0
mcp_use>=1.0.0
aiohttp>=3.9.0
aiohttp>=3.9.0
email-validator>=2.0.0
mailtrap>=2.0.1

View File

@ -132,6 +132,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.")
@ -141,8 +143,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(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
@ -239,8 +241,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}")

188
backend/services/email.py Normal file
View File

@ -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"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to Kortix Suna</title>
<style>
body {{
font-family: Arial, sans-serif;
background-color: #ffffff;
color: #000000;
margin: 0;
padding: 0;
line-height: 1.6;
}}
.container {{
max-width: 600px;
margin: 40px auto;
padding: 30px;
background-color: #ffffff;
}}
.logo-container {{
text-align: center;
margin-bottom: 30px;
padding: 10px 0;
}}
.logo {{
max-width: 100%;
height: auto;
max-height: 60px;
display: inline-block;
}}
h1 {{
font-size: 24px;
color: #000000;
margin-bottom: 20px;
}}
p {{
margin-bottom: 16px;
}}
a {{
color: #3366cc;
text-decoration: none;
}}
a:hover {{
text-decoration: underline;
}}
.button {{
display: inline-block;
margin-top: 30px;
background-color: #3B82F6;
color: white !important;
padding: 14px 24px;
text-align: center;
text-decoration: none;
font-weight: bold;
border-radius: 6px;
border: none;
}}
.button:hover {{
background-color: #2563EB;
text-decoration: none;
}}
.emoji {{
font-size: 20px;
}}
</style>
</head>
<body>
<div class="container">
<div class="logo-container">
<img src="https://i.postimg.cc/WdNtRx5Z/kortix-suna-logo.png" alt="Kortix Suna Logo" class="logo">
</div>
<h1>Welcome to Kortix Suna!</h1>
<p>Hi {user_name},</p>
<p><em><strong>Welcome to Kortix Suna we're excited to have you on board!</strong></em></p>
<p>To get started, we'd like to get to know you better: fill out this short <a href="https://docs.google.com/forms/d/e/1FAIpQLSef1EHuqmIh_iQz-kwhjnzSC3Ml-V_5wIySDpMoMU9W_j24JQ/viewform">form</a>!</p>
<p>To celebrate your arrival, here's a <strong>15% discount</strong> to try out the best version of Suna (1 month):</p>
<p>🎁 Use code <strong>WELCOME15</strong> at checkout.</p>
<p>Let us know if you need help getting started or have questions we're always here, and join our <a href="https://discord.com/invite/FjD644cfcs">Discord community</a>.</p>
<p>Thanks again, and welcome to the Suna community <span class="emoji">🌞</span></p>
<p> The Suna Team</p>
<a href="https://www.suna.so/" class="button">Go to the platform</a>
</div>
</body>
</html>"""
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()

View File

@ -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"
)

View File

@ -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: