Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| from typing import Annotated, Literal | |
| from pydantic import BaseModel, Field, field_validator, model_validator | |
| _DESTINATION_NAME_CHARS = set("abcdefghijklmnopqrstuvwxyz0123456789._-") | |
| SUPPORTED_AUTO_EVENT_TYPES = {"approval_required", "error", "turn_complete"} | |
| class SlackDestinationConfig(BaseModel): | |
| provider: Literal["slack"] = "slack" | |
| token: str | |
| channel: str | |
| allow_agent_tool: bool = False | |
| allow_auto_events: bool = False | |
| username: str | None = None | |
| icon_emoji: str | None = None | |
| def _require_non_empty(cls, value: str) -> str: | |
| value = value.strip() | |
| if not value: | |
| raise ValueError("must not be empty") | |
| return value | |
| DestinationConfig = Annotated[SlackDestinationConfig, Field(discriminator="provider")] | |
| class MessagingConfig(BaseModel): | |
| enabled: bool = False | |
| auto_event_types: list[str] = Field( | |
| default_factory=lambda: ["approval_required", "error", "turn_complete"] | |
| ) | |
| destinations: dict[str, DestinationConfig] = Field(default_factory=dict) | |
| def _validate_destination_names( | |
| cls, destinations: dict[str, DestinationConfig] | |
| ) -> dict[str, DestinationConfig]: | |
| for name in destinations: | |
| if not name or any(char not in _DESTINATION_NAME_CHARS for char in name): | |
| raise ValueError( | |
| "destination names must use lowercase letters, digits, '.', '_' or '-'" | |
| ) | |
| return destinations | |
| def _validate_auto_event_types(cls, event_types: list[str]) -> list[str]: | |
| if not event_types: | |
| return [] | |
| normalized: list[str] = [] | |
| seen: set[str] = set() | |
| for event_type in event_types: | |
| if event_type not in SUPPORTED_AUTO_EVENT_TYPES: | |
| raise ValueError(f"unsupported auto event type '{event_type}'") | |
| if event_type not in seen: | |
| normalized.append(event_type) | |
| seen.add(event_type) | |
| return normalized | |
| def _require_destinations_when_enabled(self) -> "MessagingConfig": | |
| if self.enabled and not self.destinations: | |
| raise ValueError("messaging.enabled requires at least one destination") | |
| return self | |
| def get_destination(self, name: str) -> DestinationConfig | None: | |
| return self.destinations.get(name) | |
| def can_agent_tool_send(self, name: str) -> bool: | |
| destination = self.get_destination(name) | |
| return bool(destination and destination.allow_agent_tool) | |
| def can_auto_send(self, name: str) -> bool: | |
| destination = self.get_destination(name) | |
| return bool(destination and destination.allow_auto_events) | |
| def default_auto_destinations(self) -> list[str]: | |
| if not self.enabled: | |
| return [] | |
| return [name for name in self.destinations if self.can_auto_send(name)] | |
| class NotificationRequest(BaseModel): | |
| destination: str | |
| title: str | None = None | |
| message: str | |
| severity: Literal["info", "success", "warning", "error"] = "info" | |
| metadata: dict[str, str] = Field(default_factory=dict) | |
| event_type: str | None = None | |
| def _require_text(cls, value: str) -> str: | |
| value = value.strip() | |
| if not value: | |
| raise ValueError("must not be empty") | |
| return value | |
| def _normalize_title(cls, value: str | None) -> str | None: | |
| if value is None: | |
| return None | |
| value = value.strip() | |
| return value or None | |
| class NotificationResult(BaseModel): | |
| destination: str | |
| ok: bool | |
| provider: str | |
| error: str | None = None | |
| external_id: str | None = None | |