mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-22 04:32:22 +02:00
157 lines
6.4 KiB
Python
157 lines
6.4 KiB
Python
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
from typing import Optional, Literal, Any
|
|
from datetime import datetime
|
|
from uuid import uuid4
|
|
|
|
|
|
# Each "tier" in the permission chain: notify in app, fall through to text
|
|
# after N minutes if no response, then to call after a further N minutes/hours.
|
|
# Matches images 17 to 19 (Schedule edit). Order in the list = escalation order.
|
|
class PermissionTier(BaseModel):
|
|
kind: Literal["notify", "text", "call"] = "notify"
|
|
after_minutes: int = 0
|
|
phone: Optional[str] = None
|
|
|
|
|
|
class ScheduleConfig(BaseModel):
|
|
enabled: bool = False
|
|
# Bounds keep the scheduler from blowing up on malformed input. The
|
|
# FE clamps these too, but defense-in-depth: a misbehaving agent
|
|
# tool, an old JSON file, or a curl-wielding power user shouldn't
|
|
# be able to crash _next_fire_after by passing hour=99.
|
|
repeat_every: int = Field(default=1, ge=1, le=365)
|
|
repeat_unit: Literal["day", "week", "month"] = "week"
|
|
on_days: list[int] = Field(default_factory=list)
|
|
hour: int = Field(default=9, ge=0, le=23)
|
|
minute: int = Field(default=0, ge=0, le=59)
|
|
# IANA zone name (e.g. "America/Los_Angeles") or "local" for legacy
|
|
# records that predate explicit tz. storage._load_all_from_disk coerces
|
|
# "local" to the host zone in memory; we leave it on disk until the
|
|
# user's next save so backup/sync tools don't see spurious churn.
|
|
timezone: str = "local"
|
|
on_missed: Literal["skip", "run_once", "run_all"] = "skip"
|
|
# Optional end conditions. None = forever / unbounded. Schedule auto-
|
|
# disables once either is satisfied; scheduler._tick zeroes out
|
|
# next_run_at and flips enabled=False so the UI reflects reality.
|
|
ends_at: Optional[datetime] = None
|
|
max_runs: Optional[int] = Field(default=None, ge=1)
|
|
runs_count: int = Field(default=0, ge=0)
|
|
|
|
@field_validator("on_days")
|
|
@classmethod
|
|
def _clean_on_days(cls, v: list[int]) -> list[int]:
|
|
# Backend uses JS-style weekday (Sun=0..Sat=6). Drop entries
|
|
# outside that range so a malformed PATCH can't trip the
|
|
# scheduler later, and dedupe while preserving order.
|
|
seen: set[int] = set()
|
|
out: list[int] = []
|
|
for d in v or []:
|
|
if isinstance(d, int) and 0 <= d <= 6 and d not in seen:
|
|
seen.add(d)
|
|
out.append(d)
|
|
return out
|
|
|
|
|
|
class ActionsConfig(BaseModel):
|
|
prevent_unused: bool = False
|
|
freeze: bool = False
|
|
configured_sets: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class WorkflowStep(BaseModel):
|
|
id: str = Field(default_factory=lambda: uuid4().hex)
|
|
text: str = ""
|
|
# 3 to 6 word LLM-generated headline shown in the collapsed step row.
|
|
# The full prompt lives in `text`; this is the "at-a-glance" label.
|
|
label: Optional[str] = None
|
|
|
|
|
|
def _empty_str_default() -> str:
|
|
return ""
|
|
|
|
|
|
class Workflow(BaseModel):
|
|
# validate_assignment is load-bearing for the PATCH /workflows/{id} path
|
|
# (workflows.py:update_workflow setattr's raw dicts from body.model_dump
|
|
# straight onto the cached Workflow). Without coercion the nested
|
|
# schedule/steps/actions/permissions fields become plain dicts in
|
|
# memory, and every downstream call; scheduler tick, executor.execute,
|
|
# subsequent PATCHes; crashes on `.enabled` / `.text`.
|
|
model_config = ConfigDict(validate_assignment=True)
|
|
|
|
id: str = Field(default_factory=lambda: uuid4().hex)
|
|
title: str = "Untitled workflow"
|
|
description: str = ""
|
|
icon: str = ""
|
|
system_prompt: Optional[str] = None
|
|
use_synced_prompt: bool = True
|
|
steps: list[WorkflowStep] = Field(default_factory=list)
|
|
actions: ActionsConfig = Field(default_factory=ActionsConfig)
|
|
schedule: ScheduleConfig = Field(default_factory=ScheduleConfig)
|
|
permissions: list[PermissionTier] = Field(
|
|
default_factory=lambda: [PermissionTier(kind="notify")]
|
|
)
|
|
source_session_id: Optional[str] = None
|
|
dashboard_id: Optional[str] = None
|
|
model: str = "sonnet"
|
|
mode: str = "agent"
|
|
provider: str = "anthropic"
|
|
created_at: datetime = Field(default_factory=datetime.now)
|
|
updated_at: datetime = Field(default_factory=datetime.now)
|
|
last_run_at: Optional[datetime] = None
|
|
last_run_status: Optional[Literal["success", "failure", "ran_late", "running", "skipped"]] = None
|
|
last_run_id: Optional[str] = None
|
|
next_run_at: Optional[datetime] = None
|
|
cost_cap_usd_monthly: Optional[float] = None
|
|
|
|
|
|
class WorkflowRun(BaseModel):
|
|
id: str = Field(default_factory=lambda: uuid4().hex)
|
|
workflow_id: str
|
|
status: Literal["running", "success", "failure", "ran_late", "skipped"] = "running"
|
|
scheduled_for: Optional[datetime] = None
|
|
started_at: datetime = Field(default_factory=datetime.now)
|
|
finished_at: Optional[datetime] = None
|
|
session_id: Optional[str] = None
|
|
error: Optional[str] = None
|
|
cost_usd: float = 0.0
|
|
triggered_by: Literal["schedule", "manual", "retry"] = "schedule"
|
|
# Last tool-call label observed on the underlying agent session while
|
|
# the workflow is running. Surfaced under the active step in RunningView
|
|
# (Image #40) so the user can tell the run is still making progress.
|
|
last_tool_label: Optional[str] = None
|
|
|
|
|
|
class WorkflowCreate(BaseModel):
|
|
title: str = "Untitled workflow"
|
|
description: str = ""
|
|
icon: str = ""
|
|
system_prompt: Optional[str] = None
|
|
use_synced_prompt: bool = True
|
|
steps: list[WorkflowStep] = Field(default_factory=list)
|
|
actions: ActionsConfig = Field(default_factory=ActionsConfig)
|
|
schedule: ScheduleConfig = Field(default_factory=ScheduleConfig)
|
|
permissions: Optional[list[PermissionTier]] = None
|
|
source_session_id: Optional[str] = None
|
|
dashboard_id: Optional[str] = None
|
|
model: Optional[str] = None
|
|
mode: Optional[str] = None
|
|
provider: Optional[str] = None
|
|
cost_cap_usd_monthly: Optional[float] = None
|
|
|
|
|
|
class WorkflowUpdate(BaseModel):
|
|
title: Optional[str] = None
|
|
description: Optional[str] = None
|
|
icon: Optional[str] = None
|
|
system_prompt: Optional[str] = None
|
|
use_synced_prompt: Optional[bool] = None
|
|
steps: Optional[list[WorkflowStep]] = None
|
|
actions: Optional[ActionsConfig] = None
|
|
schedule: Optional[ScheduleConfig] = None
|
|
permissions: Optional[list[PermissionTier]] = None
|
|
model: Optional[str] = None
|
|
mode: Optional[str] = None
|
|
provider: Optional[str] = None
|
|
cost_cap_usd_monthly: Optional[float] = None
|