chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# Vision UI reproduction
|
||||
|
||||
## Goal
|
||||
|
||||
Use the sandbox `view_image` tool to inspect a reference app screenshot, then reproduce the visible screen as a static HTML/CSS artifact. This is a narrow UI repro target for vision and screenshot-debugging; it is not a web-app scaffold.
|
||||
|
||||
This demo is intentionally file-only: no FastAPI, no exposed port, and no local browser server. The agent calls `view_image`, lazy-loads the `playwright` skill, writes the site under `output/site/`, captures browser screenshots for visual revision, and the host copies the generated site plus the visual-review artifacts back to this example's `output/` directory.
|
||||
|
||||
## Setup
|
||||
|
||||
Run the Unix-local example from the repository root:
|
||||
|
||||
```bash
|
||||
uv run python examples/sandbox/tutorials/vision_website_clone/main.py
|
||||
```
|
||||
|
||||
To run the same manifest in Docker, build the shared tutorial image once and pass
|
||||
`--docker`:
|
||||
|
||||
```bash
|
||||
docker build -t sandbox-tutorials:latest -f examples/sandbox/tutorials/Dockerfile .
|
||||
uv run python examples/sandbox/tutorials/vision_website_clone/main.py --docker
|
||||
```
|
||||
|
||||
## Expected artifact
|
||||
|
||||
- `output/index.html`
|
||||
- `output/styles.css`
|
||||
- `output/screenshots/draft-1.png`
|
||||
- `output/screenshots/draft-2.png`
|
||||
- `output/visual-notes.md`
|
||||
|
||||
Open `output/index.html` locally after the run to inspect the generated clone. Open the copied draft screenshots to inspect the agent's visual-debug loop.
|
||||
|
||||
## Demo shape
|
||||
|
||||
- Inputs: one checked-in PNG reference screenshot mounted under `reference/`.
|
||||
- Runtime primitives: sandbox-local shell/edit tools, `view_image`, and the lazy-loaded `playwright` skill.
|
||||
- Required vision call: `view_image("reference/reference-site.png")`.
|
||||
- Required debug loop: capture `output/screenshots/draft-1.png`, view it with `view_image`, revise, then repeat with `output/screenshots/draft-2.png`.
|
||||
- Artifact path: the sandbox agent writes `output/site/`, `output/screenshots/`, and `output/visual-notes.md`; `main.py` copies the site files and review artifacts to this example's `output/`.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
"""
|
||||
Clone a reference app screenshot as static HTML/CSS with the sandbox filesystem tools.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
from agents import ModelSettings, Runner
|
||||
from agents.run import RunConfig
|
||||
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig, WorkspaceReadNotFoundError
|
||||
from agents.sandbox.capabilities import (
|
||||
Filesystem,
|
||||
LocalDirLazySkillSource,
|
||||
Shell,
|
||||
Skills,
|
||||
)
|
||||
from agents.sandbox.entries import Dir, File, LocalDir, LocalFile
|
||||
from agents.sandbox.session import BaseSandboxSession
|
||||
|
||||
if __package__ is None or __package__ == "":
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[4]))
|
||||
|
||||
from examples.sandbox.tutorials.misc import (
|
||||
DEFAULT_SANDBOX_IMAGE,
|
||||
console,
|
||||
create_sandbox_client_and_session,
|
||||
load_env_defaults,
|
||||
print_event,
|
||||
)
|
||||
|
||||
DEMO_DIR = Path(__file__).resolve().parent
|
||||
REFERENCE_IMAGE = DEMO_DIR / "reference-site.png"
|
||||
SKILLS_SOURCE_DIR = DEMO_DIR / "skills"
|
||||
SANDBOX_SITE_DIR = Path("output") / "site"
|
||||
REMOTE_REVIEW_ARTIFACTS = (
|
||||
Path("output") / "screenshots" / "draft-1.png",
|
||||
Path("output") / "screenshots" / "draft-2.png",
|
||||
Path("output") / "visual-notes.md",
|
||||
)
|
||||
DEFAULT_MODEL = "gpt-5.4-mini"
|
||||
DEFAULT_QUESTION = (
|
||||
"Inspect the reference screenshot and build a static HTML/CSS reproduction of the "
|
||||
"screen. Write output/site/index.html and output/site/styles.css, then capture "
|
||||
"browser screenshots, inspect them, and revise the site."
|
||||
)
|
||||
AGENTS_MD = dedent(
|
||||
"""\
|
||||
# Vision UI Reproduction Instructions
|
||||
|
||||
Create a static HTML/CSS reproduction of the provided reference screenshot.
|
||||
|
||||
Build only the single screen shown in the reference.
|
||||
|
||||
## Required workflow (must do)
|
||||
|
||||
- First call `view_image` on `reference/reference-site.png`.
|
||||
- Before writing code, write `output/visual-notes.md` with brief layout + typography notes.
|
||||
- Write the site to `output/site/index.html` and `output/site/styles.css`.
|
||||
- Before taking screenshots, call `load_skill("playwright")` and read `skills/playwright/SKILL.md`.
|
||||
- Capture `output/screenshots/draft-1.png`, inspect it, revise, then capture `output/screenshots/draft-2.png`.
|
||||
- Do not finish without the screenshots.
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def default_output_dir() -> Path:
|
||||
"""Return the local directory for copied example artifacts."""
|
||||
artifacts_dir = os.environ.get("EXAMPLES_ARTIFACTS_DIR")
|
||||
if artifacts_dir:
|
||||
return Path(artifacts_dir)
|
||||
return DEMO_DIR / "output"
|
||||
|
||||
|
||||
def build_manifest() -> Manifest:
|
||||
return Manifest(
|
||||
entries={
|
||||
"AGENTS.md": File(content=AGENTS_MD.encode("utf-8")),
|
||||
"reference": Dir(
|
||||
children={
|
||||
"reference-site.png": LocalFile(src=REFERENCE_IMAGE),
|
||||
},
|
||||
description="Reference app screenshot to clone.",
|
||||
),
|
||||
"output": Dir(description="Write generated website files here."),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def build_agent(model: str) -> SandboxAgent:
|
||||
return SandboxAgent(
|
||||
name="Vision Website Clone Builder",
|
||||
model=model,
|
||||
instructions=AGENTS_MD,
|
||||
capabilities=[
|
||||
Shell(),
|
||||
Filesystem(),
|
||||
Skills(
|
||||
lazy_from=LocalDirLazySkillSource(
|
||||
# This is a host path read by the SDK process.
|
||||
# Requested skills are copied into `skills_path` in the sandbox.
|
||||
source=LocalDir(src=SKILLS_SOURCE_DIR),
|
||||
),
|
||||
skills_path="skills",
|
||||
),
|
||||
],
|
||||
model_settings=ModelSettings(tool_choice="required"),
|
||||
)
|
||||
|
||||
|
||||
async def copy_site_output_dir(
|
||||
*,
|
||||
session: BaseSandboxSession,
|
||||
output_dir: Path,
|
||||
) -> list[Path]:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
remote_site_dir = session.normalize_path(SANDBOX_SITE_DIR)
|
||||
pending_dirs = [remote_site_dir]
|
||||
copied_files: list[Path] = []
|
||||
|
||||
while pending_dirs:
|
||||
current_dir = pending_dirs.pop()
|
||||
for entry in await session.ls(current_dir):
|
||||
entry_path = Path(entry.path)
|
||||
if entry.is_dir():
|
||||
pending_dirs.append(entry_path)
|
||||
continue
|
||||
|
||||
relative_path = entry_path.relative_to(remote_site_dir)
|
||||
local_path = output_dir / relative_path
|
||||
local_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
handle = await session.read(entry_path)
|
||||
try:
|
||||
payload = handle.read()
|
||||
finally:
|
||||
handle.close()
|
||||
|
||||
if isinstance(payload, str):
|
||||
local_path.write_text(payload, encoding="utf-8")
|
||||
else:
|
||||
local_path.write_bytes(bytes(payload))
|
||||
copied_files.append(local_path)
|
||||
|
||||
return copied_files
|
||||
|
||||
|
||||
async def copy_review_artifacts(
|
||||
*,
|
||||
session: BaseSandboxSession,
|
||||
output_dir: Path,
|
||||
remote_artifacts: tuple[Path, ...] = REMOTE_REVIEW_ARTIFACTS,
|
||||
) -> list[Path]:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
copied_files: list[Path] = []
|
||||
|
||||
for remote_artifact in remote_artifacts:
|
||||
remote_path = session.normalize_path(remote_artifact)
|
||||
relative_artifact = remote_artifact.relative_to(Path("output"))
|
||||
local_path = output_dir / relative_artifact
|
||||
local_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
handle = await session.read(remote_path)
|
||||
except WorkspaceReadNotFoundError:
|
||||
continue
|
||||
try:
|
||||
payload = handle.read()
|
||||
finally:
|
||||
handle.close()
|
||||
|
||||
if isinstance(payload, str):
|
||||
local_path.write_text(payload, encoding="utf-8")
|
||||
else:
|
||||
local_path.write_bytes(bytes(payload))
|
||||
copied_files.append(local_path)
|
||||
|
||||
return copied_files
|
||||
|
||||
|
||||
async def main(model: str, question: str, use_docker: bool, image: str, output_dir: Path) -> None:
|
||||
client, sandbox = await create_sandbox_client_and_session(
|
||||
manifest=build_manifest(),
|
||||
use_docker=use_docker,
|
||||
image=image,
|
||||
)
|
||||
try:
|
||||
async with sandbox:
|
||||
result = Runner.run_streamed(
|
||||
build_agent(model),
|
||||
[{"role": "user", "content": question}],
|
||||
max_turns=30,
|
||||
run_config=RunConfig(
|
||||
sandbox=SandboxRunConfig(session=sandbox),
|
||||
tracing_disabled=True,
|
||||
workflow_name="Vision Website Clone example",
|
||||
),
|
||||
)
|
||||
async for event in result.stream_events():
|
||||
print_event(event)
|
||||
if result.final_output is None:
|
||||
raise RuntimeError("Vision Website Clone Builder returned no final message.")
|
||||
print_event(str(result.final_output).strip())
|
||||
copied_files = await copy_site_output_dir(session=sandbox, output_dir=output_dir)
|
||||
copied_review_files = await copy_review_artifacts(
|
||||
session=sandbox,
|
||||
output_dir=output_dir,
|
||||
)
|
||||
finally:
|
||||
await client.delete(sandbox)
|
||||
|
||||
expected_files = {output_dir / "index.html", output_dir / "styles.css"}
|
||||
if not expected_files <= set(copied_files):
|
||||
raise RuntimeError(
|
||||
"Vision Website Clone Builder must write output/site/index.html and "
|
||||
"output/site/styles.css."
|
||||
)
|
||||
|
||||
console.print(f"[green]Copied static site to {output_dir / 'index.html'}[/green]")
|
||||
for path in copied_review_files:
|
||||
console.print(f"[green]Copied review artifact to {path}[/green]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_env_defaults(DEMO_DIR / ".env")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name to use.")
|
||||
parser.add_argument("--question", default=DEFAULT_QUESTION, help="Prompt to send to the agent.")
|
||||
parser.add_argument(
|
||||
"--docker",
|
||||
action="store_true",
|
||||
help="Run this example in Docker instead of Unix-local.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--image",
|
||||
default=DEFAULT_SANDBOX_IMAGE,
|
||||
help="Docker image to use when --docker is set.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=default_output_dir(),
|
||||
help="Directory for copied website files.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
asyncio.run(main(args.model, args.question, args.docker, args.image, args.output_dir))
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: "playwright"
|
||||
description: "Use when the task requires capturing or automating a real browser from the terminal."
|
||||
---
|
||||
|
||||
# Playwright
|
||||
|
||||
Use Playwright to capture the static site directly. Do not start a server for this example.
|
||||
|
||||
```sh
|
||||
mkdir -p output/screenshots output/playwright/.tmp
|
||||
export TMPDIR="$PWD/output/playwright/.tmp"
|
||||
export TEMP="$TMPDIR"
|
||||
export TMP="$TMPDIR"
|
||||
npx --yes --package playwright@1.50.0 playwright install chromium
|
||||
npx --yes --package playwright@1.50.0 playwright screenshot \
|
||||
--browser=chromium \
|
||||
--viewport-size=2048,1152 \
|
||||
"file://$PWD/output/site/index.html" \
|
||||
output/screenshots/draft-1.png
|
||||
```
|
||||
|
||||
Change the final path to `output/screenshots/draft-2.png` for the second pass.
|
||||
Reference in New Issue
Block a user