209 lines
7.1 KiB
Python
209 lines
7.1 KiB
Python
"""
|
|
Minimal Daytona-backed sandbox example for manual validation.
|
|
|
|
This mirrors the E2B and Modal extension examples: it creates a tiny workspace,
|
|
asks a sandboxed agent to inspect it through one shell tool, and prints a short
|
|
answer.
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from openai.types.responses import ResponseTextDeltaEvent
|
|
|
|
from agents import ModelSettings, Runner
|
|
from agents.run import RunConfig
|
|
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
|
|
from agents.sandbox.entries import S3Mount
|
|
|
|
if __package__ is None or __package__ == "":
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[4]))
|
|
|
|
from examples.sandbox.misc.example_support import text_manifest
|
|
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability
|
|
|
|
try:
|
|
from agents.extensions.sandbox import (
|
|
DEFAULT_DAYTONA_WORKSPACE_ROOT,
|
|
DaytonaCloudBucketMountStrategy,
|
|
DaytonaSandboxClient,
|
|
DaytonaSandboxClientOptions,
|
|
)
|
|
except Exception as exc: # pragma: no cover - import path depends on optional extras
|
|
raise SystemExit(
|
|
"Daytona sandbox examples require the optional repo extra.\n"
|
|
"Install it with: uv sync --extra daytona"
|
|
) from exc
|
|
|
|
|
|
DEFAULT_QUESTION = "Summarize this cloud sandbox workspace in 2 sentences."
|
|
|
|
|
|
def _build_manifest(
|
|
*,
|
|
cloud_bucket_name: str | None = None,
|
|
cloud_bucket_mount_path: str | None = None,
|
|
cloud_bucket_endpoint_url: str | None = None,
|
|
cloud_bucket_key_prefix: str | None = None,
|
|
) -> Manifest:
|
|
"""Build a small demo manifest, optionally including a cloud bucket mount."""
|
|
manifest = text_manifest(
|
|
{
|
|
"README.md": (
|
|
"# Daytona Demo Workspace\n\n"
|
|
"This workspace exists to validate the Daytona sandbox backend manually.\n"
|
|
),
|
|
"launch.md": (
|
|
"# Launch\n\n"
|
|
"- Customer: Contoso Logistics.\n"
|
|
"- Goal: validate the remote sandbox agent path.\n"
|
|
"- Current status: Daytona backend smoke and app-server connectivity are passing.\n"
|
|
),
|
|
"tasks.md": (
|
|
"# Tasks\n\n"
|
|
"1. Inspect the workspace files.\n"
|
|
"2. Summarize the setup and any notable status in two sentences.\n"
|
|
),
|
|
}
|
|
)
|
|
if cloud_bucket_name is None:
|
|
return Manifest(root=DEFAULT_DAYTONA_WORKSPACE_ROOT, entries=manifest.entries)
|
|
|
|
manifest.entries["cloud-bucket"] = S3Mount(
|
|
bucket=cloud_bucket_name,
|
|
access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"),
|
|
secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"),
|
|
session_token=os.environ.get("AWS_SESSION_TOKEN"),
|
|
endpoint_url=cloud_bucket_endpoint_url,
|
|
prefix=cloud_bucket_key_prefix,
|
|
mount_path=Path(cloud_bucket_mount_path) if cloud_bucket_mount_path is not None else None,
|
|
read_only=False,
|
|
mount_strategy=DaytonaCloudBucketMountStrategy(),
|
|
)
|
|
return Manifest(root=DEFAULT_DAYTONA_WORKSPACE_ROOT, entries=manifest.entries)
|
|
|
|
|
|
def _require_env(name: str) -> None:
|
|
if os.environ.get(name):
|
|
return
|
|
raise SystemExit(f"{name} must be set before running this example.")
|
|
|
|
|
|
async def main(
|
|
*,
|
|
model: str,
|
|
question: str,
|
|
pause_on_exit: bool,
|
|
stream: bool,
|
|
cloud_bucket_name: str | None = None,
|
|
cloud_bucket_mount_path: str | None = None,
|
|
cloud_bucket_endpoint_url: str | None = None,
|
|
cloud_bucket_key_prefix: str | None = None,
|
|
) -> None:
|
|
_require_env("OPENAI_API_KEY")
|
|
_require_env("DAYTONA_API_KEY")
|
|
|
|
manifest = _build_manifest(
|
|
cloud_bucket_name=cloud_bucket_name,
|
|
cloud_bucket_mount_path=cloud_bucket_mount_path,
|
|
cloud_bucket_endpoint_url=cloud_bucket_endpoint_url,
|
|
cloud_bucket_key_prefix=cloud_bucket_key_prefix,
|
|
)
|
|
agent = SandboxAgent(
|
|
name="Daytona 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"),
|
|
)
|
|
|
|
client = DaytonaSandboxClient()
|
|
run_config = RunConfig(
|
|
sandbox=SandboxRunConfig(
|
|
client=client,
|
|
options=DaytonaSandboxClientOptions(pause_on_exit=pause_on_exit),
|
|
),
|
|
workflow_name="Daytona sandbox example",
|
|
)
|
|
|
|
try:
|
|
if not stream:
|
|
result = await Runner.run(agent, question, run_config=run_config)
|
|
print(result.final_output)
|
|
return
|
|
|
|
stream_result = Runner.run_streamed(agent, 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()
|
|
finally:
|
|
await client.close()
|
|
|
|
|
|
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(
|
|
"--pause-on-exit",
|
|
action="store_true",
|
|
default=False,
|
|
help="Pause the Daytona sandbox on shutdown instead of deleting it.",
|
|
)
|
|
parser.add_argument("--stream", action="store_true", default=False, help="Stream the response.")
|
|
parser.add_argument(
|
|
"--cloud-bucket-name",
|
|
default=None,
|
|
help="S3 bucket name to mount into the sandbox.",
|
|
)
|
|
parser.add_argument(
|
|
"--cloud-bucket-mount-path",
|
|
default=None,
|
|
help=(
|
|
"Mount path for --cloud-bucket-name. Relative paths are resolved under the "
|
|
"workspace root. Defaults to the mount class default."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--cloud-bucket-endpoint-url",
|
|
default=None,
|
|
help="Optional endpoint URL for --cloud-bucket-name (S3 only, e.g. MinIO).",
|
|
)
|
|
parser.add_argument(
|
|
"--cloud-bucket-key-prefix",
|
|
default=None,
|
|
help="Optional key prefix for --cloud-bucket-name.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
asyncio.run(
|
|
main(
|
|
model=args.model,
|
|
question=args.question,
|
|
pause_on_exit=args.pause_on_exit,
|
|
stream=args.stream,
|
|
cloud_bucket_name=args.cloud_bucket_name,
|
|
cloud_bucket_mount_path=args.cloud_bucket_mount_path,
|
|
cloud_bucket_endpoint_url=args.cloud_bucket_endpoint_url,
|
|
cloud_bucket_key_prefix=args.cloud_bucket_key_prefix,
|
|
)
|
|
)
|