Files
openswarm/backend/apps/swarm/entities/workflows.py
T

130 lines
4.5 KiB
Python

"""WorkflowExportable: shares a scheduled-task/workflow recipe (steps, schedule
shape, actions, model). The workflow store lives on the eric/workflow branch and
is NOT on eric/dev yet, so every store touch is lazy: on a build without it,
export finds nothing and import fails with a clear message, and the module still
imports cleanly. It lights up the moment the workflow forward-port lands.
Safety: an imported workflow must never silently start running on someone else's
machine, so the schedule is forced off on import (the importer re-arms it). The
sharer's phone numbers (text/call escalation) are stripped as PII, and run
history / session + dashboard linkage are dropped."""
from __future__ import annotations
from backend.apps.swarm.exportable import DepRef, ExportContext, RemapTable
from backend.apps.swarm.models import EntityType, Requirement, RequirementKind
P_BUILTIN_MODES = {"agent", "ask", "plan", "view-builder", "skill-builder"}
# Run-state, machine-linkage, and identifiers that must not ride along.
P_DROP_FIELDS = {
"id", "source_session_id", "dashboard_id", "edit_agent_session_id",
"last_run_at", "last_run_status", "last_run_id", "next_run_at",
"created_at", "updated_at", "cost_cap_usd_monthly",
}
def sanitize_workflow(data: dict) -> dict:
out = {k: v for k, v in data.items() if k not in P_DROP_FIELDS}
sched = dict(out.get("schedule") or {})
if sched:
sched["enabled"] = False
sched["runs_count"] = 0
sched["next_run_at"] = None
sched["ends_at"] = None
out["schedule"] = sched
perms = []
for tier in out.get("permissions") or []:
t = dict(tier)
t["phone"] = None # the sharer's number; the importer sets their own
perms.append(t)
if perms:
out["permissions"] = perms
return out
class WorkflowExportable:
type = EntityType.workflow
def __init__(self, local_id: str, name: str, data: dict):
self.local_id = local_id
self.name = name
self.p_data = data
@classmethod
def load(cls, local_id: str) -> "WorkflowExportable | None":
store = p_store()
if store is None:
return None
wf = store.get_workflow(local_id)
if wf is None:
return None
data = wf.model_dump(mode="json")
return cls(local_id, data.get("title") or "Untitled workflow", data)
def serialize(self, ctx: ExportContext) -> dict:
return sanitize_workflow(self.p_data)
def files(self) -> dict[str, bytes]:
return {}
def dependencies(self) -> list[DepRef]:
return []
def requirements(self) -> list[Requirement]:
reqs: list[Requirement] = []
for name in (self.p_data.get("actions") or {}).get("configured_sets") or []:
reqs.append(Requirement(
kind=RequirementKind.mcp_action, key=name, label=name,
detail="This workflow uses this action.",
))
mode = self.p_data.get("mode") or "agent"
if mode in P_BUILTIN_MODES and mode != "agent":
reqs.append(Requirement(
kind=RequirementKind.builtin_mode, key=mode, label=f"{mode} mode",
detail="A built-in mode this workflow runs in.",
))
provider = self.p_data.get("provider") or "anthropic"
reqs.append(Requirement(
kind=RequirementKind.api_key, key=provider, label=f"A {provider} model",
detail="Set up this provider to run the workflow.",
))
return reqs
@classmethod
def import_(cls, payload: dict, files: dict[str, bytes], remap: RemapTable) -> str:
store = p_store()
model = p_model()
if store is None or model is None:
from backend.apps.swarm.ziputil import BundleError
raise BundleError("this build doesn't support workflows yet; please update OpenSwarm")
clean = sanitize_workflow(payload)
clean.pop("id", None) # fresh id via the model's default_factory
wf = model(**clean)
store.save_workflow(wf)
return wf.id
@classmethod
def rollback(cls, local_id: str) -> None:
store = p_store()
if store is not None:
try:
store.delete_workflow(local_id)
except Exception:
pass
def p_store():
try:
from backend.apps.workflows import storage
return storage
except Exception:
return None
def p_model():
try:
from backend.apps.workflows.models import Workflow
return Workflow
except Exception:
return None