Files
openswarm/backend/apps/outputs/models.py
T

200 lines
7.0 KiB
Python

from pydantic import BaseModel, Field, model_validator
from typing import Literal, Optional, Any
from uuid import uuid4
from datetime import datetime
class Output(BaseModel):
id: str = Field(default_factory=lambda: uuid4().hex)
name: str
description: str = ""
icon: str = "view_quilt"
input_schema: dict[str, Any] = Field(default_factory=lambda: {
"type": "object",
"properties": {},
"required": [],
})
files: dict[str, str] = Field(default_factory=dict)
thumbnail: Optional[str] = None
# Bumped only when a fresh thumbnail is saved; drives sidebar/grid order so merely opening an app doesn't reshuffle the list.
preview_updated_at: Optional[str] = None
# Linkage so reopening the App Builder reattaches to the in-progress session
# and reuses the same on-disk workspace folder instead of seeding a fresh one
# (which would orphan the running agent + lose chat history on every navigate).
session_id: Optional[str] = None
workspace_id: Optional[str] = None
created_at: str = Field(default_factory=lambda: datetime.now().isoformat())
updated_at: str = Field(default_factory=lambda: datetime.now().isoformat())
@model_validator(mode="before")
@classmethod
def _migrate_flat_fields(cls, data: Any) -> Any:
"""Migrate legacy frontend_code/backend_code fields into the files dict."""
if not isinstance(data, dict):
return data
if "files" not in data or not data["files"]:
files: dict[str, str] = {}
fc = data.pop("frontend_code", None)
bc = data.pop("backend_code", None)
if fc:
files["index.html"] = fc
if bc:
files["backend.py"] = bc
data["files"] = files
else:
data.pop("frontend_code", None)
data.pop("backend_code", None)
return data
@property
def frontend_code(self) -> str:
return self.files.get("index.html", "")
@property
def backend_code(self) -> str | None:
return self.files.get("backend.py")
class OutputCreate(BaseModel):
name: str
description: str = ""
icon: str = "view_quilt"
input_schema: dict[str, Any] = Field(default_factory=lambda: {
"type": "object",
"properties": {},
"required": [],
})
files: dict[str, str] = Field(default_factory=dict)
thumbnail: Optional[str] = None
session_id: Optional[str] = None
workspace_id: Optional[str] = None
@model_validator(mode="before")
@classmethod
def _migrate_flat_fields(cls, data: Any) -> Any:
if not isinstance(data, dict):
return data
if "files" not in data or not data["files"]:
files: dict[str, str] = {}
fc = data.pop("frontend_code", None)
bc = data.pop("backend_code", None)
if fc:
files["index.html"] = fc
if bc:
files["backend.py"] = bc
data["files"] = files
else:
data.pop("frontend_code", None)
data.pop("backend_code", None)
return data
class OutputUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
icon: Optional[str] = None
input_schema: Optional[dict[str, Any]] = None
files: Optional[dict[str, str]] = None
thumbnail: Optional[str] = None
session_id: Optional[str] = None
workspace_id: Optional[str] = None
@model_validator(mode="before")
@classmethod
def _migrate_flat_fields(cls, data: Any) -> Any:
if not isinstance(data, dict):
return data
if "files" not in data:
files: dict[str, str] = {}
fc = data.pop("frontend_code", None)
bc = data.pop("backend_code", None)
if fc:
files["index.html"] = fc
if bc:
files["backend.py"] = bc
if files:
data["files"] = files
else:
data.pop("frontend_code", None)
data.pop("backend_code", None)
return data
class OutputExecute(BaseModel):
output_id: str
input_data: dict[str, Any] = Field(default_factory=dict)
# When False (default), `/execute` returns AST warnings instead of
# running if the backend code touches anything outside the safe
# data-shaping allowlist. The UI shows those warnings to the user and
# re-submits with force=True after they click "Run Anyway." This is
# a UX gate, not a security one; anyone holding the auth token can
# set force=True; the value is providing the user explicit visibility
# of what's about to execute.
force: bool = False
class OutputExecuteResult(BaseModel):
output_id: str
output_name: str
frontend_code: str
input_data: dict[str, Any]
backend_result: Optional[dict[str, Any]] = None
stdout: Optional[str] = None
stderr: Optional[str] = None
error: Optional[str] = None
# Populated when the AST validator flagged risky constructs and the
# caller didn't set force=True. When present, `backend_result` is null
# because execution was deferred pending user consent.
warnings: Optional[list[str]] = None
code_preview: Optional[str] = None
class WorkspaceSeedRequest(BaseModel):
workspace_id: str
files: Optional[dict[str, str]] = None
meta: Optional[dict[str, Any]] = None
# "webapp_template" (default) → seed the vendored
# openswarm-ai/webapp-template snapshot (React + Vite + TS frontend
# with optional FastAPI backend), allocate a free FRONTEND_PORT,
# leave BACKEND_PORT=NONE. Runtime spawns `bash run.sh`; preview
# pane points at `http://localhost:{FRONTEND_PORT}/`.
# "flat" → legacy single-`index.html` workspace, kept for explicit
# opt-in (migration helper, regression tests). Workspaces predating
# this flip continue to work in old-mode automatically since the
# runtime detects mode via the presence of `run.sh`.
template_mode: Literal["flat", "webapp_template"] = "webapp_template"
@model_validator(mode="before")
@classmethod
def _migrate_flat_fields(cls, data: Any) -> Any:
"""Accept legacy frontend_code/backend_code/schema_json fields."""
if not isinstance(data, dict):
return data
if "files" not in data:
files: dict[str, str] = {}
fc = data.pop("frontend_code", None)
bc = data.pop("backend_code", None)
sj = data.pop("schema_json", None)
if fc:
files["index.html"] = fc
if bc:
files["backend.py"] = bc
if sj:
files["schema.json"] = sj
if files:
data["files"] = files
else:
data.pop("frontend_code", None)
data.pop("backend_code", None)
data.pop("schema_json", None)
return data
class VibeCodeRequest(BaseModel):
prompt: str
current_frontend_code: str = ""
current_backend_code: str = ""
current_schema: str = ""
name: str = ""
description: str = ""