Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
# EVENT_DATABASE_NAME="madsci_events"
# EVENT_COLLECTION_NAME="events"
# EVENT_ALERT_LEVEL=40
# EVENT_EMAIL_ALERTS=null
# EVENT_EVENT_HANDLERS=["madsci.event_manager.event_handlers.default_error_handler.EventHandler","madsci.event_manager.event_handlers.default_notification_handler.EventHandler"]
# EVENT_RETENTION_ENABLED=false
# EVENT_SOFT_DELETE_AFTER_DAYS=90
# EVENT_HARD_DELETE_AFTER_DAYS=365
Expand Down
82 changes: 41 additions & 41 deletions docs/Configuration.md

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions src/madsci_client/madsci/client/workcell_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,60 @@ def cancel_workflow(
response.raise_for_status()
return Workflow.model_validate(response.json())

def send_admin_command(
self, command: str, timeout: Optional[float] = None
) -> dict[str, Any]:
"""
Send an admin command to a specific node.

Parameters
----------
command : str
The admin command to send.
timeout : Optional[float]
Timeout in seconds for this request. If not provided, uses the default timeout from config.

Returns
-------
dict[str, Any]
The response from the node.
"""
response = self._request(
"POST",
f"{self.workcell_server_url}admin/{command}",
timeout=timeout or self.config.timeout_default,
)
response.raise_for_status()
return response.json()

def send_admin_command_to_node(
self, node_name: str, command: str, timeout: Optional[float] = None
) -> dict[str, Any]:
"""
Send an admin command to a specific node.

Parameters
----------
node_name : str
The name of the node to send the command to.
command : str
The admin command to send.
timeout : Optional[float]
Timeout in seconds for this request. If not provided, uses the default timeout from config.

Returns
-------
dict[str, Any]
The response from the node.
"""
response = self._request(
"POST",
f"{self.workcell_server_url}admin/{command}/{node_name}",
timeout=timeout or self.config.timeout_default,
)
response.raise_for_status()
return response.json()

# ------------------------------------------------------------------
# Async methods
# ------------------------------------------------------------------
Expand Down
62 changes: 9 additions & 53 deletions src/madsci_common/madsci/common/types/event_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
ManagerType,
)
from madsci.common.utils import new_ulid_str
from pydantic import AliasChoices, AnyUrl, Field
from pydantic import AliasChoices, AnyUrl, Field, ImportString
from pydantic.functional_validators import field_validator
from pydantic_settings import SettingsConfigDict

Expand Down Expand Up @@ -113,11 +113,14 @@ class EventManagerSettings(
title="Alert Level",
description="The log level at which to send an alert.",
)
# TODO: Break out email alert config into separate settings
email_alerts: Optional["EmailAlertsConfig"] = Field(
default=None,
title="Email Alerts Configuration",
description="The configuration for sending email alerts.",

event_handlers: list[ImportString] = Field(
default=[
"madsci.event_manager.event_handlers.default_error_handler.EventHandler",
"madsci.event_manager.event_handlers.default_notification_handler.EventHandler",
],
title="Event Handlers",
description="Comma-separated list of event handler modules to use",
)

# Retention settings
Expand Down Expand Up @@ -643,53 +646,6 @@ def _missing_(cls, value: object) -> "EventType":
}


class EmailAlertsConfig(MadsciBaseModel):
"""Configuration for sending emails."""

smtp_server: str = Field(
default="smtp.example.com",
title="SMTP Server",
description="The SMTP server address used for sending emails.",
)
smtp_port: int = Field(
default=587,
title="SMTP Port",
description="The port number used by the SMTP server.",
)
smtp_username: Optional[str] = Field(
default=None,
title="SMTP Username",
description="The username for authenticating with the SMTP server.",
json_schema_extra={"secret": True},
)
smtp_password: Optional[str] = Field(
default=None,
title="SMTP Password",
description="The password for authenticating with the SMTP server.",
json_schema_extra={"secret": True},
)
use_tls: bool = Field(
default=True,
title="Use TLS",
description="Whether to use TLS for the SMTP connection.",
)
sender: str = Field(
default="no-reply@example.com",
title="Sender Email",
description="The default sender email address.",
)
default_importance: str = Field(
default="Normal",
title="Default Importance",
description="The default importance level of the email. Options are: High, Normal, Low.",
)
email_addresses: list[str] = Field(
default_factory=list,
title="Default Email Addresses",
description="The default email addresses to send alerts to.",
)


class EventManagerDefinition(ManagerDefinition):
"""Definition for a Squid Event Manager"""

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Default Error Handler for MADSci Event Manager."""

from event_handler import AbstractEventHandler, EventHandlerSettings
from madsci.client.workcell_client import WorkcellClient
from madsci.common.types.event_types import Event, EventLogLevel, EventManagerSettings


class ErrorHandler(AbstractEventHandler):
"""Default error handler that pauses workflows for nodes that throw an error"""

def __init__(
self,
custom_settings: EventHandlerSettings,
event_manager_settings: EventManagerSettings,
) -> None:
"""Initialize the event handler with the given settings."""
self.custom_settings = custom_settings
self.event_manager_settings = event_manager_settings
self.workcell_client = WorkcellClient()

def handle_event(self, event: Event) -> None:
"""Handle an event by pausing any active workflows for the node that threw the error."""
if event.log_level >= EventLogLevel.ERROR and event.source.node_id:
for id, workflow in self.workcell_client.get_active_workflows().items():
current_step = workflow.steps[workflow.status.current_step_index]
node = self.workcell_client.get_node(current_step.node)
if node.id == event.source.node_id:
self.workcell_client.pause_workflow(id)
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Default Notificiation Handler for MADSci Event Manager."""

from typing import Optional

from event_handler import AbstractEventHandler, EventHandlerSettings
from madsci.common.types.base_types import MadsciBaseModel
from madsci.common.types.event_types import Event, EventManagerSettings
from madsci.event_manager.notifications import EmailAlerts
from pydantic import Field


class EmailAlertsConfig(MadsciBaseModel):
"""Configuration for sending emails."""

smtp_server: str = Field(
default="smtp.example.com",
title="SMTP Server",
description="The SMTP server address used for sending emails.",
)
smtp_port: int = Field(
default=587,
title="SMTP Port",
description="The port number used by the SMTP server.",
)
smtp_username: Optional[str] = Field(
default=None,
title="SMTP Username",
description="The username for authenticating with the SMTP server.",
json_schema_extra={"secret": True},
)
smtp_password: Optional[str] = Field(
default=None,
title="SMTP Password",
description="The password for authenticating with the SMTP server.",
json_schema_extra={"secret": True},
)
use_tls: bool = Field(
default=True,
title="Use TLS",
description="Whether to use TLS for the SMTP connection.",
)
sender: str = Field(
default="no-reply@example.com",
title="Sender Email",
description="The default sender email address.",
)
default_importance: str = Field(
default="Normal",
title="Default Importance",
description="The default importance level of the email. Options are: High, Normal, Low.",
)
email_addresses: list[str] = Field(
default_factory=list,
title="Default Email Addresses",
description="The default email addresses to send alerts to.",
)


class NotificationHandlerSettings(EventHandlerSettings):
"""Settings for the default notification handler."""

email_alerts: Optional["EmailAlertsConfig"] = Field(
default=None,
title="Email Alerts Configuration",
description="The configuration for sending email alerts.",
)


class NotificationHandler(AbstractEventHandler):
"""Default notification handler that sends alerts for events"""

def __init__(
self,
custom_settings: NotificationHandlerSettings,
event_manager_settings: EventManagerSettings,
) -> None:
"""Initialize the event handler with the given settings."""
self.custom_settings = custom_settings
self.event_manager_settings = event_manager_settings

def handle_event(self, event: Event) -> None:
"""Handle an event by sending notifications for the event."""
if (
event.alert or event.log_level >= self.custom_settings.alert_level
) and self.custom_settings.email_alerts:
email_alerter = EmailAlerts(
config=self.custom_settings.email_alerts, logger=self.logger
)
email_alerter.send_email_alerts(event)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Abstract Error Handler for MADSci Event Manager."""

from madsci.common.types.base_types import MadsciBaseSettings
from madsci.common.types.event_types import Event, EventManagerSettings


class EventHandlerSettings(MadsciBaseSettings):
"""Settings for the Event Handler."""


class AbstractEventHandler:
"""Abstract base class for event handlers in the MADSci Event Manager."""

def __init__(
self,
custom_settings: EventHandlerSettings,
event_manager_settings: EventManagerSettings,
) -> None:
"""Initialize the event handler with the given settings."""
self.custom_settings = custom_settings
self.event_manager_settings = event_manager_settings

def handle_event(self, event: Event) -> None:
"""
Handle an event that occurs during processing.

Args:
event (Event): The event to handle.
"""
raise NotImplementedError("Subclasses must implement this method.")
17 changes: 7 additions & 10 deletions src/madsci_event_manager/madsci/event_manager/event_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
EventType,
)
from madsci.event_manager.events_csv_exporter import CSVExporter
from madsci.event_manager.notifications import EmailAlerts
from madsci.event_manager.time_series_analyzer import TimeSeriesAnalyzer
from madsci.event_manager.utilization_analyzer import UtilizationAnalyzer
from pydantic import BaseModel, model_validator
Expand Down Expand Up @@ -124,11 +123,14 @@ def __init__(
DeprecationWarning,
stacklevel=2,
)

# Store additional dependencies before calling super().__init__
self._document_handler = document_handler
self._db_connection = db_connection
super().__init__(settings=settings, **kwargs)

self.event_handlers = []
for handler in settings.event_handlers:
self.event_handlers.append(handler(settings))
# Initialize database connection and collections
self._setup_database()

Expand Down Expand Up @@ -460,6 +462,7 @@ async def log_event(self, event: Event) -> Event:
event_id=event.event_id,
)
# Just continue - don't fail the request

except Exception as e:
self.logger.error(
"Failed to log event",
Expand All @@ -468,15 +471,9 @@ async def log_event(self, event: Event) -> Event:
exc_info=True,
)
raise e
for handler in self.event_handlers:
handler.handle_event(event)

if (
event.alert or event.log_level >= self.settings.alert_level
) and self.settings.email_alerts:
email_alerter = EmailAlerts(
config=self.settings.email_alerts,
logger=self.logger,
)
email_alerter.send_email_alerts(event)
return event

@get("/event/{event_id}")
Expand Down