Skip to content

SMTP Email Alerter integration + make generic Alerter steps #3379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 63 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
69b98d9
Add SMTP Email Alerter integration
strickvl Feb 25, 2025
7f9dac7
Merge branch 'develop' into feature/email-alerter
strickvl Mar 5, 2025
56feb23
Improve SMTP Email Alerter ask() method error handling
strickvl Mar 5, 2025
1c7a814
add quotes to registration command
strickvl Mar 5, 2025
64f7f1a
Add email-specific alerter hooks with improved formatting
strickvl Mar 5, 2025
2dcb785
Replace Rich tracebacks with standard Python tracebacks in email alerter
strickvl Mar 5, 2025
a9a3dda
Completely replace Rich tracebacks with standard Python tracebacks
strickvl Mar 5, 2025
d2e88d4
Remove Markdown formatting from email subjects
strickvl Mar 5, 2025
0637e3e
formatting
strickvl Mar 5, 2025
2364bde
Fix mypy type issues in SMTP email alerter
strickvl Mar 5, 2025
4403a4f
Improve OpenAI failure hook formatting for SMTP email alerter
strickvl Mar 5, 2025
df7d8d3
Remove unused 'language' variable in OpenAI failure hook
strickvl Mar 5, 2025
f780e63
Fix darglint error by removing unnecessary Returns section in docstring
strickvl Mar 5, 2025
ffaa583
Merge branch 'develop' into feature/email-alerter
strickvl Mar 6, 2025
b0d0f67
Merge remote-tracking branch 'origin/develop' into feature/email-alerter
strickvl Mar 19, 2025
44fa4a9
Refactor SMTPEmailIntegration: Remove circular import handling and un…
strickvl Mar 19, 2025
9408792
Update type hint for `ask` method in `SMTPEmailAlerter` to `Never`
strickvl Mar 19, 2025
8746ca0
Refactor recipient email validation in `SMTPEmailAlerter`
strickvl Mar 19, 2025
928b193
Refactor SMTPEmailAlerter: Add generic attribute retrieval method
strickvl Mar 19, 2025
90b5537
Refactor: Create a shared email template system for alerters
strickvl Mar 19, 2025
7ceaa8b
Merge remote-tracking branch 'origin/develop' into feature/email-alerter
strickvl Mar 20, 2025
8a69a34
fix(typehints): Fix mypy errors in SMTP email alerter
strickvl Mar 20, 2025
cb0c66d
docs: Update alerter documentation for unified approach
strickvl Mar 20, 2025
9241d92
refactor: Update Discord alerter for unified alerter approach
strickvl Mar 20, 2025
177c977
refactor: Update Slack alerter for unified alerter approach
strickvl Mar 20, 2025
d4cc00c
refactor: Update SMTP Email alerter for unified alerter approach
strickvl Mar 20, 2025
dbc2426
feat: Implement unified alerter approach with AlerterMessage
strickvl Mar 20, 2025
131fb9a
refactor: Move AlerterMessage model to models/v2/misc structure
strickvl Mar 20, 2025
a3fa2fc
fix: Resolve mypy type errors in alerter components
strickvl Mar 20, 2025
3d9663c
format script
strickvl Mar 20, 2025
451a469
formatting script
strickvl Mar 20, 2025
e55c6c1
Merge branch 'develop' into feature/email-alerter
strickvl Mar 20, 2025
21d0b27
fix: Add license headers and docstrings to alerter step files
strickvl Mar 20, 2025
4da5ac2
Merge remote-tracking branch 'origin/develop' into feature/email-alerter
strickvl Apr 20, 2025
633eb20
Merge branch 'develop' into feature/email-alerter
strickvl May 22, 2025
9301994
Add unit tests for HTML email template generation functions
strickvl May 22, 2025
d8a0b92
Add email address validation for SMTP alerter
strickvl May 22, 2025
d5d8d04
Fix HTML escaping to prevent XSS vulnerabilities
strickvl May 22, 2025
7699d51
Improve error handling for SMTP connection failures
strickvl May 22, 2025
810bb07
Add unit tests for parameter inheritance in SMTP alerter
strickvl May 22, 2025
924aca3
Add unit tests for _format_markdown_for_html function
strickvl May 22, 2025
77e5f46
Improve alerter_ask_step error handling
strickvl May 22, 2025
7cc46fa
Fix Slack/Discord alerters and add SMTP exports
strickvl May 22, 2025
844d0a6
Fix Discord import and verify logo URL
strickvl May 22, 2025
81790fb
Formatting
strickvl May 22, 2025
42a88ab
Fix mypy str-bytes-safe error in SMTP alerter
strickvl May 22, 2025
9b6187e
Fix mypy errors in Discord integration
strickvl May 22, 2025
c9907d2
Formatting
strickvl May 22, 2025
2e0c318
Fix Discord client cleanup to prevent unclosed connector warnings
strickvl May 22, 2025
4f56da3
Formatting
strickvl May 22, 2025
301ae93
linting fix
strickvl May 22, 2025
9bfc4c5
Fix broken relative link in custom alerter documentation
strickvl May 22, 2025
1a040e8
Fix links
strickvl May 22, 2025
e08f06b
Fix mypy discord issue
strickvl May 22, 2025
a963c29
Merge develop branch into feature/email-alerter
strickvl May 22, 2025
1c6906a
Fix imports and formatting
strickvl May 22, 2025
8e364ab
Refactor _format_markdown_for_html into smaller helper functions
strickvl May 22, 2025
dade9e3
Formatting
strickvl May 22, 2025
857e88a
Update alerter hooks to use structured AlerterMessage format
strickvl May 23, 2025
0ead6ee
Fix mypy errors
strickvl May 23, 2025
e417927
Merge remote-tracking branch 'origin/develop' into feature/email-alerter
strickvl May 23, 2025
e633564
Fix mypy error by importing Never from typing_extensions
strickvl May 23, 2025
55b6b5c
Merge branch 'develop' into feature/email-alerter
strickvl Jun 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 78 additions & 33 deletions docs/book/component-guide/alerters/README.md
Original file line number Diff line number Diff line change
@@ -1,72 +1,97 @@
---
description: Sending automated alerts to chat services.
description: Sending automated alerts to chat services and email.
icon: message-exclamation
---

# Alerters

**Alerters** allow you to send messages to chat services (like Slack, Discord, Mattermost, etc.) from within your
pipelines. This is useful to immediately get notified when failures happen, for general monitoring/reporting, and also
for building human-in-the-loop ML.
**Alerters** allow you to send messages to chat services (like Slack, Discord, etc.) or via email from within your
pipelines. This is useful to immediately get notified about failures, for general monitoring/reporting, or
for building human-in-the-loop ML workflows.

## Alerter Flavors

Currently, the [SlackAlerter](slack.md) and [DiscordAlerter](discord.md) are the available alerter integrations. However, it is straightforward to
extend ZenML and [build an alerter for other chat services](custom.md).
Currently, the [SlackAlerter](slack.md), [DiscordAlerter](discord.md), and [SMTP Email Alerter](smtp_email.md) are the available alerter integrations. However, it is straightforward to extend ZenML and [build an alerter for other services](custom.md).

| Alerter | Flavor | Integration | Notes |
|------------------------------------|-----------|-------------|--------------------------------------------------------------------|
| [Slack](slack.md) | `slack` | `slack` | Interacts with a Slack channel |
| [Discord](discord.md) | `discord` | `discord` | Interacts with a Discord channel |
| [Custom Implementation](custom.md) | _custom_ | | Extend the alerter abstraction and provide your own implementation |
| Alerter | Flavor | Integration | Notes |
|------------------------------------|---------------|---------------|--------------------------------------------------------------------|
| [Slack](slack.md) | `slack` | `slack` | Interacts with a Slack channel |
| [Discord](discord.md) | `discord` | `discord` | Interacts with a Discord channel |
| [SMTP Email](smtp_email.md) | `smtp_email` | `smtp_email` | Sends email notifications via SMTP |
| [Custom Implementation](custom.md) | _custom_ | | Extend the alerter abstraction and provide your own implementation |

{% hint style="info" %}
If you would like to see the available flavors of alerters in your terminal, you can use the following command:

```shell
zenml alerter flavor list
```

{% endhint %}

## How to use Alerters with ZenML
## How to Use Alerters with ZenML

Each alerter integration comes with specific standard steps that you can use out of the box.
### 1. Register an Alerter

However, you first need to register an alerter component in your terminal:
First, you register the alerter flavor of your choice. For example, for Slack:

```shell
zenml alerter register <ALERTER_NAME> ...
zenml alerter register my_slack_alerter \
--flavor=slack \
--slack_token=<YOUR_SLACK_BOT_TOKEN> \
--slack_channel_id=<YOUR_SLACK_CHANNEL_ID>
```

Then you can add it to your stack using
Then attach it to your stack:

```shell
zenml stack register ... -al <ALERTER_NAME>
zenml stack register my_stack -al my_slack_alerter ...
```

Afterward, you can import the alerter standard steps provided by the respective integration and directly use them in
your pipelines.
### 2. Use `alerter_post_step` (Recommended)

ZenML now provides a **unified** step for posting alerts, regardless of which alerter flavor you've chosen. Simply import
and use `alerter_post_step`, passing an `AlerterMessage` object that you can fill with details:

```python
from zenml.alerter.steps.alerter_post_step import alerter_post_step
from zenml.models.v2.misc.alerter_models import AlerterMessage
from zenml import pipeline

@pipeline
def my_pipeline():
# Construct a message with an optional title, body, and extra metadata
msg = AlerterMessage(
title="Hello from my pipeline!",
body="All checks passed successfully.",
metadata={"accuracy": 0.95, "f1_score": 0.92}
)
result = alerter_post_step(msg)
# result is True if successfully posted, else False
```

## Using the Ask Step for Human-in-the-Loop Workflows
When you run this pipeline, ZenML will automatically detect which alerter is configured in your active stack, then post
to Slack, Discord, or send an email, etc. Each flavor interprets the message in its own format (Slack blocks, email HTML, etc.).

### 3. Use `alerter_ask_step` for Human-in-the-Loop Workflows

All alerters provide an `ask()` method and corresponding ask steps that enable human-in-the-loop workflows. These are essential for:

- Getting approval before deploying models to production
- Confirming critical pipeline decisions
- Manual intervention points in automated workflows

### How Ask Steps Work
#### How Ask Steps Work

Ask steps (like `discord_alerter_ask_step` and `slack_alerter_ask_step`):
The generic `alerter_ask_step` (and flavor-specific ask steps):

1. **Post a message** to your chat service with your question
2. **Wait for user response** containing specific approval or disapproval keywords
3. **Return a boolean** - `True` if approved, `False` if disapproved or timeout

```python
from zenml.alerter.steps.alerter_ask_step import alerter_ask_step
from zenml.models.v2.misc.alerter_models import AlerterMessage
from zenml import step, pipeline
from zenml.integrations.slack.steps.slack_alerter_ask_step import slack_alerter_ask_step

@step
def deploy_model(model, approved: bool) -> None:
Expand All @@ -81,23 +106,31 @@ def deploy_model(model, approved: bool) -> None:
def deployment_pipeline():
trained_model = train_model()
# Ask for human approval before deployment
approved = slack_alerter_ask_step("Deploy model to production?")
approval_msg = AlerterMessage(
title="Deployment Approval",
body="Deploy model to production?"
)
approved = alerter_ask_step(approval_msg)
deploy_model(trained_model, approved)
```

### Default Response Keywords
If the underlying alerter doesn't support interactive approvals (e.g., SMTP Email), a `NotImplementedError`
may be raised or it will always return `False`.

#### Default Response Keywords

By default, alerters recognize these response options:

**Approval:** `approve`, `LGTM`, `ok`, `yes`
**Disapproval:** `decline`, `disapprove`, `no`, `reject`

### Customizing Response Keywords
#### Customizing Response Keywords

You can customize the approval and disapproval keywords using alerter parameters:

```python
from zenml.integrations.slack.steps.slack_alerter_ask_step import slack_alerter_ask_step
from zenml.alerter.steps.alerter_ask_step import alerter_ask_step
from zenml.models.v2.misc.alerter_models import AlerterMessage
from zenml.integrations.slack.alerters.slack_alerter import SlackAlerterParameters

# Use custom approval/disapproval keywords
Expand All @@ -106,18 +139,30 @@ params = SlackAlerterParameters(
disapprove_msg_options=["stop", "cancel", "❌"]
)

approved = slack_alerter_ask_step(
"Deploy model to production?",
params=params
approval_msg = AlerterMessage(
title="Deployment Approval",
body="Deploy model to production?"
)

approved = alerter_ask_step(approval_msg, params=params)
```

### Important Notes
#### Important Notes

- **Return Type**: Ask steps return a boolean value - ensure your pipeline logic handles this correctly
- **Keywords**: Response keywords are case-sensitive (except Slack, which converts to lowercase)
- **Timeout**: If no valid response is received within the timeout period, the step returns `False`
- **Permissions**: Ensure your bot has permissions to read messages in the target channel

## Specialized Steps: Deprecated

We previously provided specialized steps for Slack, Discord, and Email:
- `slack_alerter_post_step`, `slack_alerter_ask_step`
- `discord_alerter_post_step`, `discord_alerter_ask_step`
- `smtp_email_alerter_post_step`

These steps are **deprecated** and will be removed in a future release. Please migrate to the generic
`alerter_post_step` and `alerter_ask_step` as shown above.

<!-- For scarf -->
<figure><img alt="ZenML Scarf" referrerpolicy="no-referrer-when-downgrade" src="https://static.scarf.sh/a.png?x-pxid=f0b4f458-0a54-4fcd-aa95-d5ee424815bc" /></figure>
<figure><img alt="ZenML Scarf" referrerpolicy="no-referrer-when-downgrade" src="https://static.scarf.sh/a.png?x-pxid=f0b4f458-0a54-4fcd-aa95-d5ee424815bc" /></figure>
89 changes: 73 additions & 16 deletions docs/book/component-guide/alerters/custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ description: Learning how to develop a custom alerter.
Before diving into the specifics of this component type, it is beneficial to familiarize yourself with our [general guide to writing custom component flavors in ZenML](https://docs.zenml.io/how-to/infrastructure-deployment/stack-deployment/implement-a-custom-stack-component). This guide provides an essential understanding of ZenML's component flavor concepts.
{% endhint %}

> **Note**: Once you have implemented your custom alerter, you can post messages via the generic
[`alerter_post_step`](./README.md#how-to-use-alerters-with-zenml) or [`alerter_ask_step`](./README.md#how-to-use-alerters-with-zenml)
if your alerter supports interactive approvals.

### Base Abstraction

The base abstraction for alerters is very basic, as it only defines two abstract methods that subclasses should implement:
The base abstraction for alerters defines two main methods that subclasses should implement:

* `post()` takes a string, posts it to the desired chat service, and returns `True` if the operation succeeded, else `False`.
* `ask()` does the same as `post()`, but after sending the message, it waits until someone approves or rejects the operation from within the chat service (e.g., by sending "approve" / "reject" to the bot as a response). `ask()` then only returns `True` if the operation succeeded and was approved, else `False`.
* `post()` takes either a string or an `AlerterMessage` object, posts it to the desired service, and returns `True` if the operation succeeded, else `False`.
* `ask()` does the same as `post()`, but after sending the message, it waits until someone approves or rejects the operation from within the service (e.g., by sending "approve" / "reject" as a response). `ask()` then only returns `True` if the operation succeeded and was approved, else `False`.

The `ask()` method is particularly useful for implementing human-in-the-loop workflows. When implementing this method, you should:
- Wait for user responses containing approval keywords (like `"approve"`, `"yes"`, `"ok"`, `"LGTM"`)
Expand All @@ -22,22 +26,43 @@ The `ask()` method is particularly useful for implementing human-in-the-loop wor
- Return `False` for disapproval, timeout, or any errors
- Consider implementing configurable approval/disapproval keywords via parameters

Then base abstraction looks something like this:
The base abstraction looks something like this:

```python
from typing import Optional, Union
from zenml.models.v2.misc.alerter_models import AlerterMessage

class BaseAlerter(StackComponent, ABC):
"""Base class for all ZenML alerters."""

def post(
self, message: str, params: Optional[BaseAlerterStepParameters]
self, message: Union[str, AlerterMessage],
params: Optional[BaseAlerterStepParameters] = None
) -> bool:
"""Post a message to a chat service."""
"""Post a message to a service.

Args:
message: Either a simple string message or a structured AlerterMessage
params: Optional parameters to configure the alert behavior

Returns:
True if the message was successfully delivered, False otherwise
"""
return True

def ask(
self, question: str, params: Optional[BaseAlerterStepParameters]
self, question: Union[str, AlerterMessage],
params: Optional[BaseAlerterStepParameters] = None
) -> bool:
"""Post a message to a chat service and wait for approval."""
"""Post a message to a service and wait for approval.

Args:
question: Either a simple string question or a structured AlerterMessage
params: Optional parameters to configure the alert behavior

Returns:
True if the question was approved by a user, False otherwise
"""
return True
```

Expand All @@ -52,29 +77,61 @@ Creating your own custom alerter can be done in four steps:
1. Create a class that inherits from the `BaseAlerter` and implement the `post()` and `ask()` methods.

```python
from typing import Optional
from typing import Optional, Union

from zenml.alerter import BaseAlerter, BaseAlerterStepParameters
from zenml.models.v2.misc.alerter_models import AlerterMessage


class MyAlerter(BaseAlerter):
"""My alerter class."""

def post(
self, message: str, params: Optional[BaseAlerterStepParameters]
self, message: Union[str, AlerterMessage],
params: Optional[BaseAlerterStepParameters] = None
) -> bool:
"""Post a message to a chat service."""
"""Post a message to a service.

This method handles both string messages and AlerterMessage objects.

Args:
message: Either a string or AlerterMessage object
params: Optional parameters for the alert

Returns:
True if successful, False otherwise
"""
try:
# Implement your chat service posting logic here
# e.g., send HTTP request to chat API
logging.info(f"Posting message: {message}")
# Handle AlerterMessage objects
if isinstance(message, AlerterMessage):
# Extract structured data from the AlerterMessage
title = message.title
body = message.body
metadata = message.metadata

# Format the message appropriately for your service
formatted_message = f"{title}\n\n{body}"

# Use metadata as needed
if metadata:
# Process any custom metadata as needed
pass

# Send the formatted message to your service
# ...
print(f"Sending AlerterMessage: {formatted_message}")
else:
# Handle simple string messages for backward compatibility
print(f"Sending string message: {message}")

return True
except Exception as e:
logging.error(f"Failed to post message: {e}")
return False

def ask(
self, question: str, params: Optional[BaseAlerterStepParameters]
self, question: Union[str, AlerterMessage],
params: Optional[BaseAlerterStepParameters] = None
) -> bool:
"""Post a message to a chat service and wait for approval."""
try:
Expand Down Expand Up @@ -205,4 +262,4 @@ It is important to draw attention to when and how these abstractions are coming
The design behind this interaction lets us separate the configuration of the flavor from its implementation. This way we can register flavors and components even when the major dependencies behind their implementation are not installed in our local setting (assuming the `MyAlerterFlavor` and the `MyAlerterConfig` are implemented in a different module/path than the actual `MyAlerter`).
{% endhint %}

<figure><img src="https://static.scarf.sh/a.png?x-pxid=f0b4f458-0a54-4fcd-aa95-d5ee424815bc" alt="ZenML Scarf"><figcaption></figcaption></figure>
<figure><img src="https://static.scarf.sh/a.png?x-pxid=f0b4f458-0a54-4fcd-aa95-d5ee424815bc" alt="ZenML Scarf"><figcaption></figcaption></figure>
Loading
Loading