Files
2026-07-13 12:39:17 +08:00

467 lines
15 KiB
Python

"""
Blaxel-backed sandbox example for manual validation.
This example mirrors the other cloud extension runners. It supports:
- Standard agent run (non-streaming and streaming).
- PTY interactive session demo (agent-driven).
- Blaxel Drive mount demo (persistent storage).
Prerequisites:
uv sync --extra blaxel
export OPENAI_API_KEY=...
export BL_API_KEY=...
export BL_WORKSPACE=...
Run:
# Basic agent run
uv run python examples/sandbox/extensions/blaxel_runner.py --stream
# With a specific image and region
uv run python examples/sandbox/extensions/blaxel_runner.py \\
--image blaxel/py-app --region us-pdx-1 --stream
# PTY terminal demo (agent-driven interactive Python session)
uv run python examples/sandbox/extensions/blaxel_runner.py --demo pty
# Drive mount demo (requires an existing drive, defaults region to us-was-1)
uv run python examples/sandbox/extensions/blaxel_runner.py \\
--demo drive --drive-name my-drive
"""
from __future__ import annotations
import argparse
import asyncio
import os
import sys
import uuid
from pathlib import Path
from openai.types.responses import ResponseTextDeltaEvent
from agents import ModelSettings, Runner, set_tracing_disabled
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Shell
from agents.sandbox.entries import File
from agents.sandbox.manifest import Environment
if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
from examples.sandbox.misc.example_support import text_manifest, tool_call_name
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability
try:
from agents.extensions.sandbox import (
DEFAULT_BLAXEL_WORKSPACE_ROOT,
BlaxelDriveMountStrategy,
BlaxelSandboxClient,
BlaxelSandboxClientOptions,
)
from agents.extensions.sandbox.blaxel import BlaxelDriveMount
except Exception as exc:
raise SystemExit(
"Blaxel sandbox examples require the optional repo extra.\n"
"Install it with: uv sync --extra blaxel"
) from exc
DEFAULT_MODEL = "gpt-5.6-sol"
DEFAULT_QUESTION = "Summarize this cloud sandbox workspace in 2 sentences."
DEFAULT_PTY_QUESTION = (
"Start an interactive Python session with `tty=true`. In that same session, compute "
"`5 + 5`, then add 5 more to the previous result. Briefly report the outputs and "
"confirm that you stayed in one Python process."
)
def _build_manifest() -> Manifest:
"""Build a small demo manifest for the default agent run."""
manifest = text_manifest(
{
"README.md": (
"# Blaxel Demo Workspace\n\nThis workspace validates the Blaxel sandbox backend.\n"
),
"project/status.md": (
"# Project Status\n\n"
"- Backend: Blaxel cloud sandbox\n"
"- Region: auto-selected\n"
"- Features: exec, file I/O, PTY, drives, preview URLs\n"
),
"project/tasks.md": (
"# Tasks\n\n"
"1. Inspect the workspace files.\n"
"2. List all features mentioned in status.md.\n"
"3. Summarize in 2-3 sentences.\n"
),
}
)
return Manifest(
root=DEFAULT_BLAXEL_WORKSPACE_ROOT,
entries=manifest.entries,
environment=Environment(
value={"DEMO_ENV": "blaxel-agent-demo"},
),
)
def _require_env(name: str) -> str:
value = os.environ.get(name)
if value:
return value
raise SystemExit(f"{name} must be set before running this example.")
def _stream_event_banner(event_name: str, raw_item: object) -> str | None:
_ = raw_item
if event_name == "tool_called":
return "[tool call]"
if event_name == "tool_output":
return "[tool output]"
return None
def _raw_item_call_id(raw_item: object) -> str | None:
if isinstance(raw_item, dict):
call_id = raw_item.get("call_id") or raw_item.get("id")
else:
call_id = getattr(raw_item, "call_id", None) or getattr(raw_item, "id", None)
return call_id if isinstance(call_id, str) and call_id else None
# ---------------------------------------------------------------------------
# PTY demo (agent-driven)
# ---------------------------------------------------------------------------
async def _run_pty_demo(
*,
model: str,
question: str,
image: str | None,
region: str | None,
) -> None:
"""Demonstrate PTY interaction: start an interactive Python process and continue it."""
agent = SandboxAgent(
name="Blaxel PTY Demo",
model=model,
instructions=(
"Complete the task by interacting with the sandbox through the shell capability. "
"Keep the final answer concise. "
"Preserve process state when the task depends on it. If you start an interactive "
"program, continue using that same process instead of launching a second one."
),
default_manifest=Manifest(
root=DEFAULT_BLAXEL_WORKSPACE_ROOT,
entries=text_manifest(
{
"README.md": (
"# Blaxel PTY Agent Example\n\n"
"This workspace is used by the Blaxel PTY demo.\n"
),
}
).entries,
),
capabilities=[Shell()],
model_settings=ModelSettings(tool_choice="required"),
)
client = BlaxelSandboxClient()
run_config = RunConfig(
sandbox=SandboxRunConfig(
client=client,
options=BlaxelSandboxClientOptions(
name=f"blaxel-demo-pty-{uuid.uuid4().hex[:8]}",
image=image,
region=region,
),
),
workflow_name="Blaxel PTY sandbox example",
)
try:
result = Runner.run_streamed(agent, question, run_config=run_config)
saw_text_delta = False
saw_any_text = False
tool_names_by_call_id: dict[str, str] = {}
async for event in result.stream_events():
if event.type == "raw_response_event" and isinstance(
event.data, ResponseTextDeltaEvent
):
if not saw_text_delta:
print("assistant> ", end="", flush=True)
saw_text_delta = True
print(event.data.delta, end="", flush=True)
saw_any_text = True
continue
if event.type != "run_item_stream_event":
continue
raw_item = event.item.raw_item
banner = _stream_event_banner(event.name, raw_item)
if banner is None:
continue
if saw_text_delta:
print()
saw_text_delta = False
if event.name == "tool_called":
t_name = tool_call_name(raw_item)
call_id = _raw_item_call_id(raw_item)
if call_id is not None and t_name:
tool_names_by_call_id[call_id] = t_name
if t_name:
banner = f"{banner} {t_name}"
elif event.name == "tool_output":
call_id = _raw_item_call_id(raw_item)
output_tool_name = tool_names_by_call_id.get(call_id or "")
if output_tool_name:
banner = f"{banner} {output_tool_name}"
print(banner)
if saw_text_delta:
print()
if not saw_any_text:
print(result.final_output)
finally:
await client.close()
# ---------------------------------------------------------------------------
# Drive demo
# ---------------------------------------------------------------------------
async def _run_drive_demo(
*,
model: str,
question: str | None,
image: str | None,
region: str | None,
drive_name: str | None,
stream: bool,
) -> None:
"""Mount a Blaxel Drive and write a file to it."""
if not drive_name:
print("Usage: --demo drive --drive-name <name>")
print()
print("You need an existing Blaxel Drive. Create one at:")
print(" https://app.blaxel.ai or via the Blaxel CLI.")
return
# Blaxel drives must be in the same region as the sandbox.
effective_region = region or os.environ.get("BL_REGION") or "us-was-1"
mount_path = "/mnt/demo-drive"
manifest = Manifest(
root=DEFAULT_BLAXEL_WORKSPACE_ROOT,
entries={
"README.md": File(
content=(b"# Blaxel Drive Demo\n\nThe drive is mounted at /mnt/demo-drive.\n")
),
"drive": BlaxelDriveMount(
drive_name=drive_name,
drive_mount_path=mount_path,
mount_strategy=BlaxelDriveMountStrategy(),
),
},
)
marker = f"demo-{uuid.uuid4().hex[:8]}"
agent = SandboxAgent(
name="Blaxel Drive Demo",
model=model,
instructions=(
"Execute the exact shell commands the user gives you. "
"Do not explore, do not run any other commands. "
"Report the stdout and stderr of each command you ran. "
"You must run the exact commands from the user message using the shell tool. "
"Do not substitute, rewrite, or add any commands. Just execute and report output."
),
default_manifest=manifest,
capabilities=[Shell()],
model_settings=ModelSettings(tool_choice="required"),
)
client = BlaxelSandboxClient()
run_config = RunConfig(
sandbox=SandboxRunConfig(
client=client,
options=BlaxelSandboxClientOptions(
name=f"blaxel-demo-drive-{uuid.uuid4().hex[:8]}",
image=image,
region=effective_region,
),
),
workflow_name="Blaxel drive demo",
)
effective_question = question or (
f"Run: echo 'drive persistence ok ({marker})' > {mount_path}/{marker}.txt && "
f"cat {mount_path}/{marker}.txt && ls {mount_path}"
)
if not stream:
result = await Runner.run(agent, effective_question, run_config=run_config)
print(result.final_output)
else:
stream_result = Runner.run_streamed(agent, effective_question, run_config=run_config)
saw_text_delta = False
async for event in stream_result.stream_events():
if event.type == "raw_response_event" and isinstance(
event.data, ResponseTextDeltaEvent
):
if not saw_text_delta:
print("assistant> ", end="", flush=True)
saw_text_delta = True
print(event.data.delta, end="", flush=True)
if saw_text_delta:
print()
await client.close()
# ---------------------------------------------------------------------------
# Standard agent run (streaming / non-streaming)
# ---------------------------------------------------------------------------
async def main(
*,
model: str,
question: str | None,
image: str | None,
region: str | None,
memory: int | None,
ttl: str | None,
pause_on_exit: bool,
stream: bool,
demo: str | None,
drive_name: str | None,
) -> None:
_require_env("OPENAI_API_KEY")
# Handle dedicated demos.
if demo == "pty":
await _run_pty_demo(
model=model,
question=question or DEFAULT_PTY_QUESTION,
image=image,
region=region,
)
return
if demo == "drive":
await _run_drive_demo(
model=model,
question=question,
image=image,
region=region,
drive_name=drive_name,
stream=stream,
)
return
manifest = _build_manifest()
agent = SandboxAgent(
name="Blaxel Sandbox Assistant",
model=model,
instructions=(
"Answer questions about the sandbox workspace. Inspect the files before answering "
"and keep the response concise. "
"Do not invent files or statuses that are not present in the workspace. Cite the "
"file names you inspected. Also run `echo $DEMO_ENV` to confirm environment "
"variables are set."
),
default_manifest=manifest,
capabilities=[WorkspaceShellCapability()],
model_settings=ModelSettings(tool_choice="required"),
)
run_config = RunConfig(
sandbox=SandboxRunConfig(
client=BlaxelSandboxClient(),
options=BlaxelSandboxClientOptions(
name=f"blaxel-demo-agent-{uuid.uuid4().hex[:8]}",
image=image,
region=region,
memory=memory,
ttl=ttl,
labels={"purpose": "agent-demo", "source": "blaxel-runner"},
pause_on_exit=pause_on_exit,
),
),
workflow_name="Blaxel sandbox example",
)
effective_question = question or DEFAULT_QUESTION
if not stream:
result = await Runner.run(agent, effective_question, run_config=run_config)
print(result.final_output)
return
stream_result = Runner.run_streamed(agent, effective_question, run_config=run_config)
saw_text_delta = False
async for event in stream_result.stream_events():
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
if not saw_text_delta:
print("assistant> ", end="", flush=True)
saw_text_delta = True
print(event.data.delta, end="", flush=True)
if saw_text_delta:
print()
if __name__ == "__main__":
set_tracing_disabled(True)
parser = argparse.ArgumentParser(
description="Blaxel sandbox demo -- showcases sandbox features.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"demos:\n"
" agent Run a sandboxed agent (default)\n"
" pty Agent-driven PTY interactive terminal\n"
" drive Mount a Blaxel Drive (requires --drive-name)\n"
),
)
parser.add_argument(
"--demo",
choices=["agent", "pty", "drive"],
default="agent",
help="Which demo to run (default: agent).",
)
parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name.")
parser.add_argument("--question", default=None, help="Override the default prompt.")
parser.add_argument("--stream", action="store_true", help="Stream response.")
parser.add_argument("--image", default=None, help="Sandbox image.")
parser.add_argument("--region", default=None, help="Sandbox region.")
parser.add_argument("--memory", type=int, default=None, help="Memory in MB.")
parser.add_argument("--ttl", default=None, help="Sandbox TTL (e.g. '1h').")
parser.add_argument("--pause-on-exit", action="store_true", help="Pause on exit.")
parser.add_argument("--drive-name", default=None, help="Drive name for drive demo.")
args = parser.parse_args()
asyncio.run(
main(
model=args.model,
question=args.question,
image=args.image,
region=args.region,
memory=args.memory,
ttl=args.ttl,
pause_on_exit=args.pause_on_exit,
stream=args.stream,
demo=args.demo,
drive_name=args.drive_name,
)
)