Skip to content

Commit 5c99a84

Browse files
emrgnt-cmplxtylogerzeroxellipsis-dev[bot]
authored
Sendgrid Email Provider Implementation (SciPhi-AI#1614) (SciPhi-AI#1618)
* +sendgrid email provider * Update py/tests/core/providers/email/test_email_providers.py The template_id parameter shown here is an example and is not intended to represent actual data. I included it as a placeholder. * Update py/tests/core/providers/email/test_email_providers.py The template_id parameter shown here is an example and is not intended to represent actual data. I included it as a placeholder. --------- Co-authored-by: logerzerox <[email protected]> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1 parent b963121 commit 5c99a84

File tree

12 files changed

+376
-47
lines changed

12 files changed

+376
-47
lines changed

py/core/base/providers/email.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# email_provider.py
22
import logging
33
from abc import ABC, abstractmethod
4-
from typing import Optional
4+
from typing import Optional, Dict
5+
import os
56

67
from .base import Provider, ProviderConfig
78

@@ -13,27 +14,22 @@ class EmailConfig(ProviderConfig):
1314
smtp_password: Optional[str] = None
1415
from_email: Optional[str] = None
1516
use_tls: Optional[bool] = True
16-
17+
sendgrid_api_key: Optional[str] = None
18+
verify_email_template_id: Optional[str] = None
19+
reset_password_template_id: Optional[str] = None
20+
frontend_url: Optional[str] = None
1721
@property
1822
def supported_providers(self) -> list[str]:
1923
return [
2024
"smtp",
2125
"console",
26+
"sendgrid",
2227
] # Could add more providers like AWS SES, SendGrid etc.
2328

2429
def validate_config(self) -> None:
25-
pass
26-
# if self.provider == "smtp":
27-
# if not all(
28-
# [
29-
# self.smtp_server,
30-
# self.smtp_port,
31-
# self.smtp_username,
32-
# self.smtp_password,
33-
# self.from_email,
34-
# ]
35-
# ):
36-
# raise ValueError("SMTP configuration is incomplete")
30+
if self.provider == "sendgrid":
31+
if not (self.sendgrid_api_key or os.getenv("SENDGRID_API_KEY")):
32+
raise ValueError("SendGrid API key is required when using SendGrid provider")
3733

3834

3935
logger = logging.getLogger(__name__)
@@ -55,17 +51,27 @@ async def send_email(
5551
subject: str,
5652
body: str,
5753
html_body: Optional[str] = None,
54+
*args,
55+
**kwargs
5856
) -> None:
5957
pass
6058

6159
@abstractmethod
6260
async def send_verification_email(
63-
self, to_email: str, verification_code: str
61+
self,
62+
to_email: str,
63+
verification_code: str,
64+
*args,
65+
**kwargs
6466
) -> None:
6567
pass
6668

6769
@abstractmethod
6870
async def send_password_reset_email(
69-
self, to_email: str, reset_token: str
71+
self,
72+
to_email: str,
73+
reset_token: str,
74+
*args,
75+
**kwargs
7076
) -> None:
7177
pass

py/core/main/abstractions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
SqlitePersistentLoggingProvider,
2222
SupabaseAuthProvider,
2323
UnstructuredIngestionProvider,
24+
SendGridEmailProvider,
2425
)
2526

2627

@@ -38,7 +39,7 @@ class R2RProviders(BaseModel):
3839
HatchetOrchestrationProvider, SimpleOrchestrationProvider
3940
]
4041
logging: SqlitePersistentLoggingProvider
41-
email: Union[AsyncSMTPEmailProvider, ConsoleMockEmailProvider]
42+
email: Union[AsyncSMTPEmailProvider, ConsoleMockEmailProvider, SendGridEmailProvider]
4243

4344
class Config:
4445
arbitrary_types_allowed = True

py/core/main/assembly/factory.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from core.pipes import GeneratorPipe, MultiSearchPipe, SearchPipe
2121
from core.providers.logger.r2r_logger import SqlitePersistentLoggingProvider
2222

23+
from core.providers.email.sendgrid import SendGridEmailProvider
24+
2325
from ..abstractions import R2RAgents, R2RPipelines, R2RPipes, R2RProviders
2426
from ..config import R2RConfig
2527

@@ -56,7 +58,7 @@ async def create_auth_provider(
5658
crypto_provider: BCryptProvider,
5759
database_provider: PostgresDBProvider,
5860
email_provider: Union[
59-
AsyncSMTPEmailProvider, ConsoleMockEmailProvider
61+
AsyncSMTPEmailProvider, ConsoleMockEmailProvider, SendGridEmailProvider
6062
],
6163
*args,
6264
**kwargs,
@@ -235,7 +237,7 @@ def create_llm_provider(
235237
@staticmethod
236238
async def create_email_provider(
237239
email_config: Optional[EmailConfig] = None, *args, **kwargs
238-
) -> Union[AsyncSMTPEmailProvider, ConsoleMockEmailProvider]:
240+
) -> Union[AsyncSMTPEmailProvider, ConsoleMockEmailProvider, SendGridEmailProvider]:
239241
"""Creates an email provider based on configuration."""
240242
if not email_config:
241243
raise ValueError(
@@ -246,6 +248,8 @@ async def create_email_provider(
246248
return AsyncSMTPEmailProvider(email_config)
247249
elif email_config.provider == "console_mock":
248250
return ConsoleMockEmailProvider(email_config)
251+
elif email_config.provider == "sendgrid":
252+
return SendGridEmailProvider(email_config)
249253
else:
250254
raise ValueError(
251255
f"Email provider {email_config.provider} not supported."
@@ -259,7 +263,7 @@ async def create_providers(
259263
crypto_provider_override: Optional[BCryptProvider] = None,
260264
database_provider_override: Optional[PostgresDBProvider] = None,
261265
email_provider_override: Optional[
262-
Union[AsyncSMTPEmailProvider, ConsoleMockEmailProvider]
266+
Union[AsyncSMTPEmailProvider, ConsoleMockEmailProvider, SendGridEmailProvider]
263267
] = None,
264268
embedding_provider_override: Optional[
265269
Union[

py/core/providers/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
from .auth import R2RAuthProvider, SupabaseAuthProvider
22
from .crypto import BCryptConfig, BCryptProvider
33
from .database import PostgresDBProvider
4-
from .email import AsyncSMTPEmailProvider, ConsoleMockEmailProvider
4+
from .email import (
5+
AsyncSMTPEmailProvider,
6+
ConsoleMockEmailProvider,
7+
SendGridEmailProvider,
8+
)
59
from .embeddings import (
610
LiteLLMEmbeddingProvider,
711
OllamaEmbeddingProvider,
@@ -41,6 +45,7 @@
4145
# Email
4246
"AsyncSMTPEmailProvider",
4347
"ConsoleMockEmailProvider",
48+
"SendGridEmailProvider",
4449
# Orchestration
4550
"HatchetOrchestrationProvider",
4651
"SimpleOrchestrationProvider",

py/core/providers/auth/r2r_auth.py

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ async def register(self, email: str, password: str) -> UserResponse:
147147
)
148148

149149
if self.config.require_email_verification:
150-
# Generate verification code and send email
151150
verification_code = (
152151
self.crypto_provider.generate_verification_code()
153152
)
@@ -157,10 +156,12 @@ async def register(self, email: str, password: str) -> UserResponse:
157156
new_user.id, verification_code, expiry
158157
)
159158
new_user.verification_code_expiry = expiry
160-
# TODO - Integrate email provider(s)
161159

160+
# Safely get first name, defaulting to email if name is None
161+
first_name = new_user.name.split(" ")[0] if new_user.name else email.split("@")[0]
162+
162163
await self.email_provider.send_verification_email(
163-
new_user.email, verification_code
164+
new_user.email, verification_code, {"first_name": first_name}
164165
)
165166
else:
166167
expiry = datetime.now(timezone.utc) + timedelta(hours=366 * 10)
@@ -307,8 +308,9 @@ async def request_password_reset(self, email: str) -> dict[str, str]:
307308
user.id, reset_token, expiry
308309
)
309310

310-
# TODO: Integrate with email provider to send reset link
311-
await self.email_provider.send_password_reset_email(email, reset_token)
311+
# Safely get first name, defaulting to email if name is None
312+
first_name = user.name.split(" ")[0] if user.name else email.split("@")[0]
313+
await self.email_provider.send_password_reset_email(email, reset_token, {"first_name": first_name})
312314

313315
return {"message": "If the email exists, a reset link has been sent"}
314316

@@ -341,19 +343,6 @@ async def clean_expired_blacklisted_tokens(self):
341343
await self.database_provider.clean_expired_blacklisted_tokens()
342344

343345
async def send_reset_email(self, email: str) -> dict:
344-
"""
345-
Generate a new verification code and send a reset email to the user.
346-
Returns the verification code for testing/sandbox environments.
347-
348-
Args:
349-
email (str): The email address of the user
350-
351-
Returns:
352-
dict: Contains verification_code and message
353-
354-
Raises:
355-
R2RException: If user is not found
356-
"""
357346
user = await self.database_provider.get_user_by_email(email)
358347
if not user:
359348
raise R2RException(status_code=404, message="User not found")
@@ -369,9 +358,11 @@ async def send_reset_email(self, email: str) -> dict:
369358
expiry,
370359
)
371360

361+
# Safely get first name, defaulting to email if name is None
362+
first_name = user.name.split(" ")[0] if user.name else email.split("@")[0]
372363
# Send verification email
373364
await self.email_provider.send_verification_email(
374-
email, verification_code
365+
email, verification_code, {"first_name": first_name}
375366
)
376367

377368
return {

py/core/providers/email/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
from .console_mock import ConsoleMockEmailProvider
22
from .smtp import AsyncSMTPEmailProvider
3+
from .sendgrid import SendGridEmailProvider
34

4-
__all__ = ["ConsoleMockEmailProvider", "AsyncSMTPEmailProvider"]
5+
__all__ = [
6+
"ConsoleMockEmailProvider",
7+
"AsyncSMTPEmailProvider",
8+
"SendGridEmailProvider",
9+
]

py/core/providers/email/console_mock.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ async def send_email(
1515
subject: str,
1616
body: str,
1717
html_body: Optional[str] = None,
18+
*args,
19+
**kwargs
1820
) -> None:
1921
logger.info(
2022
f"""
@@ -28,7 +30,11 @@ async def send_email(
2830
)
2931

3032
async def send_verification_email(
31-
self, to_email: str, verification_code: str
33+
self,
34+
to_email: str,
35+
verification_code: str,
36+
*args,
37+
**kwargs
3238
) -> None:
3339
logger.info(
3440
f"""
@@ -42,7 +48,11 @@ async def send_verification_email(
4248
)
4349

4450
async def send_password_reset_email(
45-
self, to_email: str, reset_token: str
51+
self,
52+
to_email: str,
53+
reset_token: str,
54+
*args,
55+
**kwargs
4656
) -> None:
4757
logger.info(
4858
f"""

0 commit comments

Comments
 (0)