AdithyaSK HF Staff commited on
Commit
1d12f00
·
verified ·
1 Parent(s): 90bff50

Serve Crello corpus aafdb0e86816 from the mounted bucket

Browse files
Dockerfile CHANGED
@@ -10,10 +10,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends libegl1 libgl1
10
 
11
  # Dependencies first, for layer caching.
12
  COPY pyproject.toml uv.lock ./
13
- RUN uv sync --frozen --no-dev --no-install-project
14
 
15
  COPY . .
16
- RUN uv sync --frozen --no-dev \
17
  && .venv/bin/python -m playwright install --with-deps chromium \
18
  && rm -rf /var/lib/apt/lists/*
19
 
 
10
 
11
  # Dependencies first, for layer caching.
12
  COPY pyproject.toml uv.lock ./
13
+ RUN uv sync --frozen --no-dev --extra daytona --no-install-project
14
 
15
  COPY . .
16
+ RUN uv sync --frozen --no-dev --extra daytona \
17
  && .venv/bin/python -m playwright install --with-deps chromium \
18
  && rm -rf /var/lib/apt/lists/*
19
 
realapps/__init__.py CHANGED
@@ -112,3 +112,30 @@ def _up(url: str) -> bool:
112
  return e.code < 500
113
  except Exception: # noqa: BLE001
114
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  return e.code < 500
113
  except Exception: # noqa: BLE001
114
  return False
115
+
116
+
117
+ _AVAILABLE: dict = {}
118
+
119
+
120
+ def available() -> tuple[bool, str]:
121
+ """Whether this server can run real apps, and if not, why (cached for 30 s; cheap enough for every page load).
122
+
123
+ On Daytona that needs the SDK and a key; locally, the Penpot and desktop endpoints must answer."""
124
+ import time
125
+
126
+ if _AVAILABLE and time.time() - _AVAILABLE["at"] < 30:
127
+ return _AVAILABLE["ok"], _AVAILABLE["why"]
128
+ if os.environ.get("DESIGN_CANVAS_APPS", "").lower() == "daytona":
129
+ try:
130
+ import daytona # noqa: F401
131
+ except ImportError:
132
+ ok, why = False, "the daytona package is not installed (install the `daytona` extra)"
133
+ else:
134
+ ok, why = (bool(os.environ.get("DAYTONA_API_KEY")), "DAYTONA_API_KEY is not set")
135
+ else:
136
+ st = status(check=True)
137
+ down = [e["url"] for e in st["endpoints"] if not e.get("up")]
138
+ ok, why = not down, ("real apps run locally here and nothing answers at " + ", ".join(down) +
139
+ "; run them (realapps/) or set DESIGN_CANVAS_APPS=daytona")
140
+ _AVAILABLE.update(at=time.time(), ok=ok, why="" if ok else why)
141
+ return ok, _AVAILABLE["why"]
realapps/daytona.py CHANGED
@@ -9,7 +9,8 @@ URL, and the env's browser may reach that app and nothing else (server/gui.py):
9
  penpot one Docker-in-Docker sandbox running realapps/penpot/docker-compose.yaml, shared by all Penpot
10
  episodes; Penpot's public URI is the sandbox's preview URL, which is stable for the sandbox's lifetime
11
 
12
- Sandboxes carry the labels {"project": "design-canvas", "role": ...} and outlive the server: the next server reuses
 
13
  them (starting stopped ones). While an episode runs (up to DESIGN_CANVAS_DAYTONA_MAX_EPISODE_MIN, default 120) the
14
  server keeps its sandbox awake; idle ones stop by themselves after DESIGN_CANVAS_DAYTONA_AUTO_STOP minutes (default
15
  15) and are started again when needed.
@@ -43,6 +44,17 @@ from .penpot.backend import PenpotBackend
43
 
44
  HERE = Path(__file__).parent
45
  PROJECT = {"project": "design-canvas"}
 
 
 
 
 
 
 
 
 
 
 
46
  PENPOT_PORT, AGENT_PORT = 9011, 5005
47
  HOME = "/home/user" # the desktop image's user (realapps/desktop/Dockerfile)
48
 
@@ -86,9 +98,10 @@ class DaytonaApps:
86
  def sandboxes(self, role: str | None = None) -> list:
87
  from daytona import ListSandboxesQuery
88
 
89
- labels = {**PROJECT, **({"role": role} if role else {})}
90
  return [s for s in self.d.list(ListSandboxesQuery(labels=labels))
91
- if str(s.state).split(".")[-1].lower() not in ("destroyed", "destroying", "error")]
 
92
 
93
  def _state(self, sb) -> str:
94
  sb.refresh_data()
@@ -167,7 +180,7 @@ class DaytonaApps:
167
 
168
  def create(_):
169
  return self.d.create(CreateSandboxFromSnapshotParams(
170
- snapshot=snap, labels={**PROJECT, "role": "desktop", "snapshot": snap},
171
  auto_stop_interval=_auto_stop(), public=False), timeout=600)
172
 
173
  with ThreadPoolExecutor(max_workers=8) as pool:
@@ -210,7 +223,7 @@ class DaytonaApps:
210
 
211
  sbs = self.sandboxes("penpot")
212
  sb = sbs[0] if sbs else self.d.create(CreateSandboxFromImageParams(
213
- image="docker:28.3.3-dind", labels={**PROJECT, "role": "penpot"},
214
  resources=Resources(cpu=2, memory=4, disk=10), auto_stop_interval=_auto_stop(), public=False),
215
  timeout=600)
216
  self.prepare_penpot(sb)
 
9
  penpot one Docker-in-Docker sandbox running realapps/penpot/docker-compose.yaml, shared by all Penpot
10
  episodes; Penpot's public URI is the sandbox's preview URL, which is stable for the sandbox's lifetime
11
 
12
+ Sandboxes carry the labels {"project": "design-canvas", "role": ...} (plus "pool" when DESIGN_CANVAS_DAYTONA_POOL
13
+ names a separate pool) and outlive the server: the next server of the same pool reuses
14
  them (starting stopped ones). While an episode runs (up to DESIGN_CANVAS_DAYTONA_MAX_EPISODE_MIN, default 120) the
15
  server keeps its sandbox awake; idle ones stop by themselves after DESIGN_CANVAS_DAYTONA_AUTO_STOP minutes (default
16
  15) and are started again when needed.
 
44
 
45
  HERE = Path(__file__).parent
46
  PROJECT = {"project": "design-canvas"}
47
+
48
+
49
+ def _pool() -> str | None:
50
+ """DESIGN_CANVAS_DAYTONA_POOL names a separate pool (e.g. "space" for the public Space): its sandboxes carry
51
+ that name as a label, so two servers on different machines never take each other's desktops. Unset = the
52
+ default pool, the sandboxes with no pool label."""
53
+ return os.environ.get("DESIGN_CANVAS_DAYTONA_POOL") or None
54
+
55
+
56
+ def _labels(**extra) -> dict:
57
+ return {**PROJECT, **({"pool": _pool()} if _pool() else {}), **extra}
58
  PENPOT_PORT, AGENT_PORT = 9011, 5005
59
  HOME = "/home/user" # the desktop image's user (realapps/desktop/Dockerfile)
60
 
 
98
  def sandboxes(self, role: str | None = None) -> list:
99
  from daytona import ListSandboxesQuery
100
 
101
+ labels = _labels(**({"role": role} if role else {}))
102
  return [s for s in self.d.list(ListSandboxesQuery(labels=labels))
103
+ if s.labels.get("pool") == _pool() # a label query also matches other pools' sandboxes
104
+ and str(s.state).split(".")[-1].lower() not in ("destroyed", "destroying", "error")]
105
 
106
  def _state(self, sb) -> str:
107
  sb.refresh_data()
 
180
 
181
  def create(_):
182
  return self.d.create(CreateSandboxFromSnapshotParams(
183
+ snapshot=snap, labels=_labels(role="desktop", snapshot=snap),
184
  auto_stop_interval=_auto_stop(), public=False), timeout=600)
185
 
186
  with ThreadPoolExecutor(max_workers=8) as pool:
 
223
 
224
  sbs = self.sandboxes("penpot")
225
  sb = sbs[0] if sbs else self.d.create(CreateSandboxFromImageParams(
226
+ image="docker:28.3.3-dind", labels=_labels(role="penpot"),
227
  resources=Resources(cpu=2, memory=4, disk=10), auto_stop_interval=_auto_stop(), public=False),
228
  timeout=600)
229
  self.prepare_penpot(sb)
scripts/deploy_space.py CHANGED
@@ -36,6 +36,9 @@ def main(argv=None) -> int:
36
  ap.add_argument("--max-envs", type=int, default=32, help="concurrent environment sessions per worker")
37
  ap.add_argument("--workers", type=int, default=4, help="server processes behind the Space's one URL")
38
  ap.add_argument("--reward", default="3", help="DESIGN_CANVAS_REWARD (3 = reward v3, as the evals use)")
 
 
 
39
  ap.add_argument("--transport", choices=["http", "mount"], default="http",
40
  help="read designs over HTTP (default) or from the bucket mounted at /corpus; the mount returned "
41
  "I/O errors under the workers' random reads (2026-10-05)")
@@ -82,8 +85,20 @@ def main(argv=None) -> int:
82
  "DESIGN_CANVAS_PUBLIC_URL": f"https://{args.space.replace('/', '-').lower()}.hf.space"}.items():
83
  if key not in current or current[key].value != value:
84
  api.add_space_variable(args.space, key, value)
 
 
 
 
 
 
 
 
 
 
 
 
85
  print(json.dumps({"space": args.space, "commit": commit.oid, "snapshot_id": manifest["snapshot_id"],
86
- "bucket": bucket, "transport": args.transport,
87
  "url": f"https://huggingface.co/spaces/{args.space}"}, indent=2))
88
  return 0
89
 
 
36
  ap.add_argument("--max-envs", type=int, default=32, help="concurrent environment sessions per worker")
37
  ap.add_argument("--workers", type=int, default=4, help="server processes behind the Space's one URL")
38
  ap.add_argument("--reward", default="3", help="DESIGN_CANVAS_REWARD (3 = reward v3, as the evals use)")
39
+ ap.add_argument("--daytona-desktops", type=int, default=0,
40
+ help="run the real apps on Daytona with this many desktops (0 = no real apps); the key and URL "
41
+ "are copied from DAYTONA_API_KEY / DAYTONA_API_URL into the Space's secrets")
42
  ap.add_argument("--transport", choices=["http", "mount"], default="http",
43
  help="read designs over HTTP (default) or from the bucket mounted at /corpus; the mount returned "
44
  "I/O errors under the workers' random reads (2026-10-05)")
 
85
  "DESIGN_CANVAS_PUBLIC_URL": f"https://{args.space.replace('/', '-').lower()}.hf.space"}.items():
86
  if key not in current or current[key].value != value:
87
  api.add_space_variable(args.space, key, value)
88
+ if args.daytona_desktops > 0: # the Space's own pool, so it never takes a local server's desktops
89
+ import os
90
+
91
+ for key, value in {"DESIGN_CANVAS_APPS": "daytona", "DESIGN_CANVAS_DAYTONA_POOL": "space",
92
+ "DESIGN_CANVAS_DAYTONA_DESKTOPS": str(args.daytona_desktops)}.items():
93
+ if key not in current or current[key].value != value:
94
+ api.add_space_variable(args.space, key, value)
95
+ for key in ("DAYTONA_API_KEY", "DAYTONA_API_URL"):
96
+ if os.environ.get(key):
97
+ api.add_space_secret(args.space, key, os.environ[key])
98
+ elif "DESIGN_CANVAS_APPS" in current:
99
+ api.delete_space_variable(args.space, "DESIGN_CANVAS_APPS")
100
  print(json.dumps({"space": args.space, "commit": commit.oid, "snapshot_id": manifest["snapshot_id"],
101
+ "bucket": bucket, "transport": args.transport, "daytona_desktops": args.daytona_desktops,
102
  "url": f"https://huggingface.co/spaces/{args.space}"}, indent=2))
103
  return 0
104
 
server/environment.py CHANGED
@@ -201,7 +201,11 @@ class DesignCanvasEnvironment(MCPEnvironment):
201
  # A seeded reset also fixes where the file window opens and how it lists files.
202
  f"&fs={rng.randrange(1 << 30)}" if seed is not None else "")
203
  if skin in REAL_SKINS: # a real app behind the same interface: its own editor, set up per episode
204
- from ..realapps import READY, get_backend
 
 
 
 
205
 
206
  backend = get_backend(skin, fonts_dir=self.pack.root / "fonts")
207
  gt = self.pack.gt_doc(task)
 
201
  # A seeded reset also fixes where the file window opens and how it lists files.
202
  f"&fs={rng.randrange(1 << 30)}" if seed is not None else "")
203
  if skin in REAL_SKINS: # a real app behind the same interface: its own editor, set up per episode
204
+ from ..realapps import READY, available, get_backend
205
+
206
+ ok, why = available()
207
+ if not ok:
208
+ raise RuntimeError(f"{REAL_SKINS[skin][0]} is not available on this server: {why}")
209
 
210
  backend = get_backend(skin, fonts_dir=self.pack.root / "fonts")
211
  gt = self.pack.gt_doc(task)
server/gradio_ui.py CHANGED
@@ -27,6 +27,7 @@ try:
27
  from ..core import expert
28
  from ..core.pack import TaskPack
29
  from ..core.score import CONFIGS
 
30
  from ..realapps import status as apps_status
31
  from .environment import DesignCanvasEnvironment
32
  from .episodes import REAL_SKINS, SKIN_INFO, STORE
@@ -34,6 +35,7 @@ except ImportError:
34
  from core import expert
35
  from core.pack import TaskPack
36
  from core.score import CONFIGS
 
37
  from realapps import status as apps_status
38
  from server.environment import DesignCanvasEnvironment
39
  from server.episodes import REAL_SKINS, SKIN_INFO, STORE
@@ -43,8 +45,13 @@ SKIN_CHOICES = SIM_CHOICES + [(f"{name} (real app)", sid) for sid, (name, _) in
43
 
44
 
45
  def skin_choices(surface: str) -> list:
46
- """The editors a surface can use: real apps only work on computer use (the env rejects them elsewhere)."""
47
- return SKIN_CHOICES if surface == "gui" else SIM_CHOICES
 
 
 
 
 
48
  MODE_CHOICES = [("Recreate from reference", "reference"), ("Build from brief", "description")]
49
  SURFACE_CHOICES = [("Tool calls", "tools"), ("HTML", "html"), ("Computer use", "gui")]
50
  CLICK_CHOICES = [("Click", "left_click"), ("Double-click", "double_click"), ("Right-click", "right_click")]
@@ -186,6 +193,8 @@ def build_ui(pack: TaskPack) -> gr.Blocks:
186
  real = skin in REAL_SKINS
187
  if real and surface != "gui":
188
  raise gr.Error(f"{REAL_SKINS[skin][0]} is a real app: it runs on Computer use only.")
 
 
189
  if real:
190
  progress(0, desc=f"Opening {REAL_SKINS[skin][0]} (a real app: the first one can take a minute)")
191
  env, obs = pg.start(split, index, mode, surface, skin, previous)
 
27
  from ..core import expert
28
  from ..core.pack import TaskPack
29
  from ..core.score import CONFIGS
30
+ from ..realapps import available as apps_available
31
  from ..realapps import status as apps_status
32
  from .environment import DesignCanvasEnvironment
33
  from .episodes import REAL_SKINS, SKIN_INFO, STORE
 
35
  from core import expert
36
  from core.pack import TaskPack
37
  from core.score import CONFIGS
38
+ from realapps import available as apps_available
39
  from realapps import status as apps_status
40
  from server.environment import DesignCanvasEnvironment
41
  from server.episodes import REAL_SKINS, SKIN_INFO, STORE
 
45
 
46
 
47
  def skin_choices(surface: str) -> list:
48
+ """The editors a surface can use: real apps only on computer use (the env rejects them elsewhere), and marked
49
+ when this server cannot reach them."""
50
+ if surface != "gui":
51
+ return SIM_CHOICES
52
+ ok, _ = apps_available()
53
+ return SIM_CHOICES + [(f"{name} (real app{'' if ok else ', not set up here'})", sid)
54
+ for sid, (name, _) in REAL_SKINS.items()]
55
  MODE_CHOICES = [("Recreate from reference", "reference"), ("Build from brief", "description")]
56
  SURFACE_CHOICES = [("Tool calls", "tools"), ("HTML", "html"), ("Computer use", "gui")]
57
  CLICK_CHOICES = [("Click", "left_click"), ("Double-click", "double_click"), ("Right-click", "right_click")]
 
193
  real = skin in REAL_SKINS
194
  if real and surface != "gui":
195
  raise gr.Error(f"{REAL_SKINS[skin][0]} is a real app: it runs on Computer use only.")
196
+ if real and not apps_available()[0]:
197
+ raise gr.Error(f"{REAL_SKINS[skin][0]} is not available on this server: {apps_available()[1]}")
198
  if real:
199
  progress(0, desc=f"Opening {REAL_SKINS[skin][0]} (a real app: the first one can take a minute)")
200
  env, obs = pg.start(split, index, mode, surface, skin, previous)