DesignGym / realapps /__init__.py
AdithyaSK's picture
AdithyaSK HF Staff
Serve Crello corpus aafdb0e86816 from the mounted bucket
1d12f00 verified
Raw History Blame Contribute Delete
6.13 kB
"""Real design apps behind the design-canvas GUI interface (skin="penpot", ...).
Each backend sets an episode up inside the app (a blank board at the task's size with its assets loaded), hands
the GUI driver a URL and cookies to open, and on submit reads the app's own document back as a DesignDoc so the
same reward scores it.
"""
from __future__ import annotations
import os
import threading
from pathlib import Path
_BACKENDS: dict = {}
_LOCK = threading.Lock()
# CSS selector that appears once each app's editor has loaded (the GUI driver waits for it)
READY = {"penpot": "[class*=viewport]"}
def get_backend(skin: str, fonts_dir: Path | None = None):
"""The shared backend for a real-app skin.
With DESIGN_CANVAS_APPS=daytona the server runs the apps in Daytona sandboxes it manages (realapps/daytona.py;
DESIGN_CANVAS_DAYTONA_DESKTOPS desktops, default 2). Otherwise they are reached at fixed addresses:
DESIGN_CANVAS_PENPOT_URL (default http://localhost:9011) with DESIGN_CANVAS_PENPOT_CREDS (where the test
profile's credentials are kept), and DESIGN_CANVAS_DESKTOP_URLS (desktop agents, one episode each at a time).
The desktop apps (impress, inkscape-desktop, gimp) share one pool of desktops."""
with _LOCK:
if skin not in _BACKENDS:
daytona = os.environ.get("DESIGN_CANVAS_APPS", "").lower() == "daytona"
if skin == "penpot":
if daytona:
from .daytona import DaytonaPenpotBackend
_BACKENDS[skin] = DaytonaPenpotBackend(_apps())
else:
from .penpot.backend import PenpotBackend
creds = os.environ.get("DESIGN_CANVAS_PENPOT_CREDS",
str(Path.home() / ".cache" / "design_canvas" / "penpot-test-user.json"))
_BACKENDS[skin] = PenpotBackend(
os.environ.get("DESIGN_CANVAS_PENPOT_URL", "http://localhost:9011"), creds)
elif skin in ("impress", "inkscape-desktop", "gimp"):
desk = next((b for k, b in _BACKENDS.items() if k != "penpot"), None)
if desk is None and daytona:
from .daytona import DaytonaDesktopBackend
desk = DaytonaDesktopBackend(_apps(), int(os.environ.get("DESIGN_CANVAS_DAYTONA_DESKTOPS", "2")),
fonts_dir)
elif desk is None:
from .desktop.client import DesktopBackend
desk = DesktopBackend()
_BACKENDS[skin] = desk
else:
raise ValueError(f"no real-app backend for skin {skin!r}")
return _BACKENDS[skin]
_APPS = None
def _apps():
global _APPS
if _APPS is None:
from .daytona import DaytonaApps
_APPS = DaytonaApps()
return _APPS
def status(check: bool = False) -> dict:
"""Which real apps this server offers and where they run, for the playground and GET /api/apps.
With check, each app's endpoint (or the Daytona pool) is also asked whether it is up; without it nothing is
contacted, so the call is free."""
try:
from ..server.episodes import REAL_SKINS
except ImportError:
from server.episodes import REAL_SKINS
daytona = os.environ.get("DESIGN_CANVAS_APPS", "").lower() == "daytona"
out = {"runs_on": "daytona" if daytona else "local",
"apps": [{"skin": k, "name": n, "about": d, "surface": "gui"} for k, (n, d) in REAL_SKINS.items()]}
if daytona:
out["desktops"] = int(os.environ.get("DESIGN_CANVAS_DAYTONA_DESKTOPS", "2"))
if check:
apps = _apps()
out["sandboxes"] = [{"role": sb.labels.get("role"), "id": sb.id[:8],
"state": str(sb.state).split(".")[-1].lower(), "cpu": sb.cpu,
"memory_gb": sb.memory, "episodes": apps.busy.get(sb.id, 0)}
for sb in apps.sandboxes() if sb.labels.get("role")]
else:
penpot = os.environ.get("DESIGN_CANVAS_PENPOT_URL", "http://localhost:9011")
desktops = [u.strip() for u in os.environ.get("DESIGN_CANVAS_DESKTOP_URLS", "http://localhost:5005").split(",")
if u.strip()]
out["endpoints"] = [{"role": "penpot", "url": penpot}] + [{"role": "desktop", "url": u} for u in desktops]
if check:
for e in out["endpoints"]:
e["up"] = _up(e["url"] + ("/api/rpc/command/get-profile" if e["role"] == "penpot" else "/health"))
return out
def _up(url: str) -> bool:
import urllib.error
import urllib.request
try:
with urllib.request.urlopen(url, timeout=3):
return True
except urllib.error.HTTPError as e: # Penpot answers get-profile without a session too
return e.code < 500
except Exception: # noqa: BLE001
return False
_AVAILABLE: dict = {}
def available() -> tuple[bool, str]:
"""Whether this server can run real apps, and if not, why (cached for 30 s; cheap enough for every page load).
On Daytona that needs the SDK and a key; locally, the Penpot and desktop endpoints must answer."""
import time
if _AVAILABLE and time.time() - _AVAILABLE["at"] < 30:
return _AVAILABLE["ok"], _AVAILABLE["why"]
if os.environ.get("DESIGN_CANVAS_APPS", "").lower() == "daytona":
try:
import daytona # noqa: F401
except ImportError:
ok, why = False, "the daytona package is not installed (install the `daytona` extra)"
else:
ok, why = (bool(os.environ.get("DAYTONA_API_KEY")), "DAYTONA_API_KEY is not set")
else:
st = status(check=True)
down = [e["url"] for e in st["endpoints"] if not e.get("up")]
ok, why = not down, ("real apps run locally here and nothing answers at " + ", ".join(down) +
"; run them (realapps/) or set DESIGN_CANVAS_APPS=daytona")
_AVAILABLE.update(at=time.time(), ok=ok, why="" if ok else why)
return ok, _AVAILABLE["why"]