274 lines
8.9 KiB
Python
274 lines
8.9 KiB
Python
"""
|
|
Minimal E2B-backed sandbox example for manual validation.
|
|
|
|
This example is intentionally small: it creates a tiny workspace, lets the
|
|
agent inspect it through one shell tool, and prints a short answer.
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import io
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
from openai.types.responses import ResponseTextDeltaEvent
|
|
|
|
from agents import ModelSettings, Runner
|
|
from agents.run import RunConfig
|
|
from agents.sandbox import LocalSnapshotSpec, Manifest, SandboxAgent, SandboxRunConfig
|
|
|
|
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
|
|
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability
|
|
|
|
try:
|
|
from agents.extensions.sandbox import (
|
|
E2BSandboxClient,
|
|
E2BSandboxClientOptions,
|
|
E2BSandboxType,
|
|
)
|
|
except Exception as exc: # pragma: no cover - import path depends on optional extras
|
|
raise SystemExit(
|
|
"E2B sandbox examples require the optional repo extra.\n"
|
|
"Install it with: uv sync --extra e2b"
|
|
) from exc
|
|
|
|
|
|
DEFAULT_QUESTION = "Summarize this cloud sandbox workspace in 2 sentences."
|
|
DEFAULT_SANDBOX_TYPE = E2BSandboxType.E2B.value
|
|
SNAPSHOT_CHECK_PATH = Path("snapshot-check.txt")
|
|
SNAPSHOT_CHECK_CONTENT = "e2b snapshot round-trip ok\n"
|
|
|
|
|
|
def _build_manifest() -> Manifest:
|
|
return text_manifest(
|
|
{
|
|
"README.md": (
|
|
"# Renewal Notes\n\n"
|
|
"This workspace contains a tiny account review packet for manual sandbox testing.\n"
|
|
),
|
|
"customer.md": (
|
|
"# Customer\n\n"
|
|
"- Name: Northwind Health.\n"
|
|
"- Renewal date: 2026-04-15.\n"
|
|
"- Risk: unresolved SSO setup.\n"
|
|
),
|
|
"next_steps.md": (
|
|
"# Next steps\n\n"
|
|
"1. Finish the SSO fix.\n"
|
|
"2. Confirm legal language before procurement review.\n"
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
def _require_env(name: str) -> None:
|
|
if os.environ.get(name):
|
|
return
|
|
raise SystemExit(f"{name} must be set before running this example.")
|
|
|
|
|
|
def _rewrite_template_resolution_error(exc: Exception) -> None:
|
|
message = str(exc)
|
|
marker = "error resolving template '"
|
|
if marker not in message:
|
|
return
|
|
template = message.split(marker, 1)[1].split("'", 1)[0]
|
|
raise SystemExit(
|
|
f"E2B could not resolve template `{template}`.\n"
|
|
"Pass `--template <your-template>` with a template that exists for this E2B account/team. "
|
|
"If you were relying on the example default, the SDK default template for this backend is "
|
|
"not available in your current E2B environment."
|
|
) from exc
|
|
|
|
|
|
async def _verify_stop_resume(
|
|
*,
|
|
sandbox_type: Literal["e2b_code_interpreter", "e2b"],
|
|
template: str | None,
|
|
timeout: int | None,
|
|
pause_on_exit: bool,
|
|
workspace_persistence: Literal["tar", "snapshot"],
|
|
) -> None:
|
|
client = E2BSandboxClient()
|
|
with tempfile.TemporaryDirectory(prefix="e2b-snapshot-example-") as snapshot_dir:
|
|
sandbox = await client.create(
|
|
manifest=_build_manifest(),
|
|
snapshot=LocalSnapshotSpec(base_path=Path(snapshot_dir)),
|
|
options=E2BSandboxClientOptions(
|
|
sandbox_type=E2BSandboxType(sandbox_type),
|
|
template=template,
|
|
timeout=timeout,
|
|
pause_on_exit=pause_on_exit,
|
|
workspace_persistence=workspace_persistence,
|
|
),
|
|
)
|
|
|
|
try:
|
|
await sandbox.start()
|
|
await sandbox.write(
|
|
SNAPSHOT_CHECK_PATH,
|
|
io.BytesIO(SNAPSHOT_CHECK_CONTENT.encode("utf-8")),
|
|
)
|
|
await sandbox.stop()
|
|
finally:
|
|
await sandbox.shutdown()
|
|
|
|
resumed_sandbox = await client.resume(sandbox.state)
|
|
try:
|
|
await resumed_sandbox.start()
|
|
restored = await resumed_sandbox.read(SNAPSHOT_CHECK_PATH)
|
|
restored_text = restored.read()
|
|
if isinstance(restored_text, bytes):
|
|
restored_text = restored_text.decode("utf-8")
|
|
if restored_text != SNAPSHOT_CHECK_CONTENT:
|
|
raise RuntimeError(
|
|
"Snapshot resume verification failed for "
|
|
f"{sandbox_type!r}: expected {SNAPSHOT_CHECK_CONTENT!r}, got {restored_text!r}"
|
|
)
|
|
finally:
|
|
await resumed_sandbox.shutdown()
|
|
|
|
print(f"snapshot round-trip ok ({sandbox_type}, {workspace_persistence})")
|
|
|
|
|
|
async def main(
|
|
*,
|
|
model: str,
|
|
question: str,
|
|
sandbox_type: Literal["e2b_code_interpreter", "e2b"],
|
|
template: str | None,
|
|
timeout: int | None,
|
|
pause_on_exit: bool,
|
|
workspace_persistence: Literal["tar", "snapshot"],
|
|
stream: bool,
|
|
) -> None:
|
|
_require_env("OPENAI_API_KEY")
|
|
_require_env("E2B_API_KEY")
|
|
|
|
try:
|
|
await _verify_stop_resume(
|
|
sandbox_type=sandbox_type,
|
|
template=template,
|
|
timeout=timeout,
|
|
pause_on_exit=pause_on_exit,
|
|
workspace_persistence=workspace_persistence,
|
|
)
|
|
except Exception as exc:
|
|
_rewrite_template_resolution_error(exc)
|
|
raise
|
|
|
|
manifest = _build_manifest()
|
|
agent = SandboxAgent(
|
|
name="E2B 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."
|
|
),
|
|
default_manifest=manifest,
|
|
capabilities=[WorkspaceShellCapability()],
|
|
model_settings=ModelSettings(tool_choice="required"),
|
|
)
|
|
|
|
run_config = RunConfig(
|
|
sandbox=SandboxRunConfig(
|
|
client=E2BSandboxClient(),
|
|
options=E2BSandboxClientOptions(
|
|
sandbox_type=E2BSandboxType(sandbox_type),
|
|
template=template,
|
|
timeout=timeout,
|
|
pause_on_exit=pause_on_exit,
|
|
workspace_persistence=workspace_persistence,
|
|
),
|
|
),
|
|
workflow_name="E2B sandbox example",
|
|
)
|
|
|
|
if not stream:
|
|
try:
|
|
result = await Runner.run(agent, question, run_config=run_config)
|
|
except Exception as exc:
|
|
_rewrite_template_resolution_error(exc)
|
|
raise
|
|
print(result.final_output)
|
|
return
|
|
|
|
try:
|
|
stream_result = Runner.run_streamed(agent, question, run_config=run_config)
|
|
except Exception as exc:
|
|
_rewrite_template_resolution_error(exc)
|
|
raise
|
|
saw_text_delta = False
|
|
try:
|
|
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)
|
|
except Exception as exc:
|
|
_rewrite_template_resolution_error(exc)
|
|
raise
|
|
|
|
if saw_text_delta:
|
|
print()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--model", default="gpt-5.6-sol", help="Model name to use.")
|
|
parser.add_argument("--question", default=DEFAULT_QUESTION, help="Prompt to send to the agent.")
|
|
parser.add_argument(
|
|
"--sandbox-type",
|
|
default=DEFAULT_SANDBOX_TYPE,
|
|
choices=[member.value for member in E2BSandboxType],
|
|
help=(
|
|
"E2B sandbox interface to create. `e2b` provides a bash-style interface; "
|
|
"`e2b_code_interpreter` provides a Jupyter-style interface."
|
|
),
|
|
)
|
|
parser.add_argument("--template", default=None, help="Optional E2B template name.")
|
|
parser.add_argument(
|
|
"--timeout",
|
|
type=int,
|
|
default=300,
|
|
help="Optional E2B sandbox timeout in seconds.",
|
|
)
|
|
parser.add_argument(
|
|
"--pause-on-exit",
|
|
action="store_true",
|
|
default=False,
|
|
help="Pause the sandbox on shutdown instead of killing it.",
|
|
)
|
|
parser.add_argument(
|
|
"--workspace-persistence",
|
|
default="tar",
|
|
choices=["tar", "snapshot"],
|
|
help="Workspace persistence mode for the E2B sandbox.",
|
|
)
|
|
parser.add_argument("--stream", action="store_true", default=False, help="Stream the response.")
|
|
args = parser.parse_args()
|
|
|
|
asyncio.run(
|
|
main(
|
|
model=args.model,
|
|
question=args.question,
|
|
sandbox_type=args.sandbox_type,
|
|
template=args.template,
|
|
timeout=args.timeout,
|
|
pause_on_exit=args.pause_on_exit,
|
|
workspace_persistence=args.workspace_persistence,
|
|
stream=args.stream,
|
|
)
|
|
)
|