Domain model bridges frontend canvas, database, and xml-pipeline: - nodes.py: AgentNode, ToolNode, GatewayNode with serialization - Built-in tool mappings (calculate, fetch, shell, etc.) - Agent config (prompt, model, temperature) - Gateway config (federation + REST API) - edges.py: Edge connections with conditions - Auto-compute peers from edges - Cycle detection, entry/exit node finding - triggers.py: Webhook, Schedule, Manual, Event triggers - Config dataclasses for each type - Factory functions for common patterns - flow.py: Main Flow class aggregating all components - to_organism_yaml(): Generate xml-pipeline config - from_canvas_json() / to_canvas_json(): React Flow compat - validate(): Check for errors before execution - to_db_dict(): Database serialization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
317 lines
10 KiB
Python
317 lines
10 KiB
Python
"""
|
|
Trigger types for starting flow execution.
|
|
|
|
Triggers define how a flow is initiated:
|
|
- Webhook: External HTTP POST to a unique URL
|
|
- Schedule: Cron-based scheduled execution
|
|
- Manual: User-initiated from dashboard
|
|
- Event: Internal event subscription
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Any
|
|
from uuid import UUID, uuid4
|
|
|
|
|
|
class TriggerType(str, Enum):
|
|
"""Types of triggers that can start a flow."""
|
|
|
|
WEBHOOK = "webhook"
|
|
SCHEDULE = "schedule"
|
|
MANUAL = "manual"
|
|
EVENT = "event"
|
|
|
|
|
|
# =============================================================================
|
|
# Trigger Configs
|
|
# =============================================================================
|
|
|
|
|
|
@dataclass
|
|
class WebhookConfig:
|
|
"""Configuration for webhook triggers."""
|
|
|
|
# Auto-generated token for authentication
|
|
token: str | None = None
|
|
|
|
# Optional: require specific headers
|
|
required_headers: dict[str, str] = field(default_factory=dict)
|
|
|
|
# Optional: IP allowlist
|
|
allowed_ips: list[str] = field(default_factory=list)
|
|
|
|
# Payload transformation
|
|
payload_template: str | None = None # Jinja2 template for input transformation
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"token": self.token,
|
|
"requiredHeaders": self.required_headers,
|
|
"allowedIps": self.allowed_ips,
|
|
"payloadTemplate": self.payload_template,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> WebhookConfig:
|
|
return cls(
|
|
token=data.get("token"),
|
|
required_headers=data.get("requiredHeaders") or data.get("required_headers", {}),
|
|
allowed_ips=data.get("allowedIps") or data.get("allowed_ips", []),
|
|
payload_template=data.get("payloadTemplate") or data.get("payload_template"),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ScheduleConfig:
|
|
"""Configuration for scheduled triggers."""
|
|
|
|
# Cron expression (e.g., "0 9 * * *" for 9 AM daily)
|
|
cron: str
|
|
|
|
# Timezone (e.g., "America/New_York")
|
|
timezone: str = "UTC"
|
|
|
|
# Optional: specific dates to skip
|
|
skip_dates: list[str] = field(default_factory=list)
|
|
|
|
# Optional: payload to inject on trigger
|
|
default_payload: dict[str, Any] = field(default_factory=dict)
|
|
|
|
# Execution window (skip if missed by more than N minutes)
|
|
grace_period_minutes: int = 5
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"cron": self.cron,
|
|
"timezone": self.timezone,
|
|
"skipDates": self.skip_dates,
|
|
"defaultPayload": self.default_payload,
|
|
"gracePeriodMinutes": self.grace_period_minutes,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> ScheduleConfig:
|
|
return cls(
|
|
cron=data["cron"],
|
|
timezone=data.get("timezone", "UTC"),
|
|
skip_dates=data.get("skipDates") or data.get("skip_dates", []),
|
|
default_payload=data.get("defaultPayload") or data.get("default_payload", {}),
|
|
grace_period_minutes=data.get("gracePeriodMinutes") or data.get("grace_period_minutes", 5),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ManualConfig:
|
|
"""Configuration for manual triggers."""
|
|
|
|
# Form fields to show in dashboard
|
|
input_fields: list[dict[str, Any]] = field(default_factory=list)
|
|
|
|
# Default values
|
|
default_payload: dict[str, Any] = field(default_factory=dict)
|
|
|
|
# Confirmation required before execution
|
|
require_confirmation: bool = False
|
|
confirmation_message: str | None = None
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"inputFields": self.input_fields,
|
|
"defaultPayload": self.default_payload,
|
|
"requireConfirmation": self.require_confirmation,
|
|
"confirmationMessage": self.confirmation_message,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> ManualConfig:
|
|
return cls(
|
|
input_fields=data.get("inputFields") or data.get("input_fields", []),
|
|
default_payload=data.get("defaultPayload") or data.get("default_payload", {}),
|
|
require_confirmation=data.get("requireConfirmation") or data.get("require_confirmation", False),
|
|
confirmation_message=data.get("confirmationMessage") or data.get("confirmation_message"),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class EventConfig:
|
|
"""Configuration for internal event triggers."""
|
|
|
|
# Event name to subscribe to
|
|
event_name: str
|
|
|
|
# Optional: filter expression
|
|
filter_expression: str | None = None
|
|
|
|
# Source flow ID (if triggered by another flow)
|
|
source_flow_id: UUID | None = None
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"eventName": self.event_name,
|
|
"filterExpression": self.filter_expression,
|
|
"sourceFlowId": str(self.source_flow_id) if self.source_flow_id else None,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> EventConfig:
|
|
source_flow = data.get("sourceFlowId") or data.get("source_flow_id")
|
|
return cls(
|
|
event_name=data["eventName"] if "eventName" in data else data["event_name"],
|
|
filter_expression=data.get("filterExpression") or data.get("filter_expression"),
|
|
source_flow_id=UUID(source_flow) if source_flow else None,
|
|
)
|
|
|
|
|
|
# Union type for trigger configs
|
|
TriggerConfig = WebhookConfig | ScheduleConfig | ManualConfig | EventConfig
|
|
|
|
|
|
# =============================================================================
|
|
# Trigger
|
|
# =============================================================================
|
|
|
|
|
|
@dataclass
|
|
class Trigger:
|
|
"""
|
|
A trigger that initiates flow execution.
|
|
|
|
Triggers are connected to entry nodes in the flow.
|
|
When fired, they inject a message into the entry node(s).
|
|
"""
|
|
|
|
id: UUID
|
|
name: str
|
|
trigger_type: TriggerType
|
|
config: TriggerConfig
|
|
|
|
# Which node(s) to send the initial message to
|
|
target_node_ids: list[UUID] = field(default_factory=list)
|
|
|
|
# Enabled state
|
|
enabled: bool = True
|
|
|
|
# Metadata
|
|
created_at: datetime | None = None
|
|
last_triggered_at: datetime | None = None
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"id": str(self.id),
|
|
"name": self.name,
|
|
"triggerType": self.trigger_type.value,
|
|
"config": self.config.to_dict(),
|
|
"targetNodeIds": [str(nid) for nid in self.target_node_ids],
|
|
"enabled": self.enabled,
|
|
"createdAt": self.created_at.isoformat() if self.created_at else None,
|
|
"lastTriggeredAt": self.last_triggered_at.isoformat() if self.last_triggered_at else None,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> Trigger:
|
|
trigger_type = TriggerType(data.get("triggerType") or data.get("trigger_type"))
|
|
config_data = data.get("config", {})
|
|
|
|
# Parse config based on trigger type
|
|
config: TriggerConfig
|
|
if trigger_type == TriggerType.WEBHOOK:
|
|
config = WebhookConfig.from_dict(config_data)
|
|
elif trigger_type == TriggerType.SCHEDULE:
|
|
config = ScheduleConfig.from_dict(config_data)
|
|
elif trigger_type == TriggerType.MANUAL:
|
|
config = ManualConfig.from_dict(config_data)
|
|
elif trigger_type == TriggerType.EVENT:
|
|
config = EventConfig.from_dict(config_data)
|
|
else:
|
|
raise ValueError(f"Unknown trigger type: {trigger_type}")
|
|
|
|
# Parse target node IDs
|
|
target_ids_raw = data.get("targetNodeIds") or data.get("target_node_ids", [])
|
|
target_node_ids = [
|
|
UUID(nid) if isinstance(nid, str) else nid
|
|
for nid in target_ids_raw
|
|
]
|
|
|
|
# Parse timestamps
|
|
created_at = None
|
|
if created := data.get("createdAt") or data.get("created_at"):
|
|
created_at = datetime.fromisoformat(created) if isinstance(created, str) else created
|
|
|
|
last_triggered = None
|
|
if triggered := data.get("lastTriggeredAt") or data.get("last_triggered_at"):
|
|
last_triggered = datetime.fromisoformat(triggered) if isinstance(triggered, str) else triggered
|
|
|
|
return cls(
|
|
id=UUID(data["id"]) if isinstance(data.get("id"), str) else data.get("id", uuid4()),
|
|
name=data["name"],
|
|
trigger_type=trigger_type,
|
|
config=config,
|
|
target_node_ids=target_node_ids,
|
|
enabled=data.get("enabled", True),
|
|
created_at=created_at,
|
|
last_triggered_at=last_triggered,
|
|
)
|
|
|
|
def get_webhook_url(self, base_url: str, flow_id: UUID) -> str | None:
|
|
"""Get the webhook URL if this is a webhook trigger."""
|
|
if self.trigger_type != TriggerType.WEBHOOK:
|
|
return None
|
|
|
|
if not isinstance(self.config, WebhookConfig):
|
|
return None
|
|
|
|
token = self.config.token or ""
|
|
return f"{base_url}/webhooks/{flow_id}/{self.id}?token={token}"
|
|
|
|
|
|
def create_webhook_trigger(
|
|
name: str,
|
|
target_node_ids: list[UUID],
|
|
token: str | None = None,
|
|
) -> Trigger:
|
|
"""Factory function to create a webhook trigger."""
|
|
return Trigger(
|
|
id=uuid4(),
|
|
name=name,
|
|
trigger_type=TriggerType.WEBHOOK,
|
|
config=WebhookConfig(token=token),
|
|
target_node_ids=target_node_ids,
|
|
enabled=True,
|
|
)
|
|
|
|
|
|
def create_schedule_trigger(
|
|
name: str,
|
|
cron: str,
|
|
target_node_ids: list[UUID],
|
|
timezone: str = "UTC",
|
|
) -> Trigger:
|
|
"""Factory function to create a scheduled trigger."""
|
|
return Trigger(
|
|
id=uuid4(),
|
|
name=name,
|
|
trigger_type=TriggerType.SCHEDULE,
|
|
config=ScheduleConfig(cron=cron, timezone=timezone),
|
|
target_node_ids=target_node_ids,
|
|
enabled=True,
|
|
)
|
|
|
|
|
|
def create_manual_trigger(
|
|
name: str,
|
|
target_node_ids: list[UUID],
|
|
input_fields: list[dict[str, Any]] | None = None,
|
|
) -> Trigger:
|
|
"""Factory function to create a manual trigger."""
|
|
return Trigger(
|
|
id=uuid4(),
|
|
name=name,
|
|
trigger_type=TriggerType.MANUAL,
|
|
config=ManualConfig(input_fields=input_fields or []),
|
|
target_node_ids=target_node_ids,
|
|
enabled=True,
|
|
)
|