initial commit
This commit is contained in:
550
venv/Lib/site-packages/langsmith/sandbox/README.md
Normal file
550
venv/Lib/site-packages/langsmith/sandbox/README.md
Normal file
@@ -0,0 +1,550 @@
|
||||
# LangSmith Sandbox
|
||||
|
||||
Sandboxed code execution for LangSmith. Run untrusted code safely in isolated containers.
|
||||
|
||||
> ⚠️ **Warning**: This module is experimental. Features and APIs may change, and breaking changes are expected as we iterate.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from langsmith.sandbox import SandboxClient
|
||||
|
||||
# Client uses LANGSMITH_ENDPOINT and LANGSMITH_API_KEY from environment
|
||||
client = SandboxClient()
|
||||
|
||||
# First, create a template (defines the container image)
|
||||
client.create_template(
|
||||
name="python-sandbox",
|
||||
image="python:3.12-slim",
|
||||
)
|
||||
|
||||
# Now create a sandbox from the template and run code
|
||||
with client.sandbox(template_name="python-sandbox") as sb:
|
||||
result = sb.run("python -c 'print(2 + 2)'")
|
||||
print(result.stdout) # "4\n"
|
||||
print(result.success) # True
|
||||
|
||||
# Or create a sandbox to keep
|
||||
sb = client.create_sandbox(template_name="python-sandbox")
|
||||
result = sb.run("python -c 'print(2 + 2)'")
|
||||
client.delete_sandbox(sb.name) # Don't forget to clean up when done
|
||||
|
||||
# Or use an existing sandbox by ID
|
||||
sb = client.get_sandbox(name="your-sandbox")
|
||||
result = sb.run("python -c 'print(2 + 2)'")
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
The sandbox module works out of the box for basic command execution (HTTP). For
|
||||
**real-time output** (streaming, callbacks, and `timeout=0`), install the
|
||||
optional dependency:
|
||||
|
||||
```bash
|
||||
pip install 'langsmith[sandbox]'
|
||||
```
|
||||
|
||||
This pulls in the `websockets` package. Without it, `sb.run()` falls back to
|
||||
HTTP automatically.
|
||||
|
||||
## Configuration
|
||||
|
||||
The client automatically uses LangSmith environment variables:
|
||||
|
||||
```python
|
||||
from langsmith.sandbox import SandboxClient
|
||||
|
||||
# Uses LANGSMITH_ENDPOINT and LANGSMITH_API_KEY
|
||||
client = SandboxClient()
|
||||
|
||||
# Or configure explicitly
|
||||
client = SandboxClient(
|
||||
api_endpoint="https://api.smith.langchain.com/v2/sandboxes",
|
||||
api_key="your-api-key",
|
||||
timeout=30.0,
|
||||
)
|
||||
```
|
||||
|
||||
## Running Commands
|
||||
|
||||
```python
|
||||
# Assuming you've created a template called "my-sandbox"
|
||||
with client.sandbox(template_name="my-sandbox") as sb:
|
||||
# Run a command
|
||||
result = sb.run("echo 'Hello, World!'")
|
||||
|
||||
print(result.stdout) # "Hello, World!\n"
|
||||
print(result.stderr) # ""
|
||||
print(result.exit_code) # 0
|
||||
print(result.success) # True
|
||||
|
||||
# Commands that fail return non-zero exit codes
|
||||
result = sb.run("exit 1")
|
||||
print(result.success) # False
|
||||
print(result.exit_code) # 1
|
||||
```
|
||||
|
||||
## Streaming Output
|
||||
|
||||
For long-running commands, you can stream output in real time. This requires
|
||||
the `websockets` package (`pip install 'langsmith[sandbox]'`).
|
||||
|
||||
### Callbacks
|
||||
|
||||
The simplest way to get real-time output. Blocks until the command completes.
|
||||
|
||||
```python
|
||||
import sys
|
||||
|
||||
with client.sandbox(template_name="my-sandbox") as sb:
|
||||
result = sb.run(
|
||||
"make build",
|
||||
timeout=600,
|
||||
on_stdout=lambda s: print(s, end=""),
|
||||
on_stderr=lambda s: print(s, end="", file=sys.stderr),
|
||||
)
|
||||
print(f"\nBuild {'succeeded' if result.success else 'failed'}")
|
||||
```
|
||||
|
||||
### Streaming with CommandHandle
|
||||
|
||||
For full control — access to the process handle, stream identity, kill, and
|
||||
reconnection.
|
||||
|
||||
```python
|
||||
with client.sandbox(template_name="my-sandbox") as sb:
|
||||
handle = sb.run("make build", timeout=600, wait=False)
|
||||
|
||||
print(f"Command ID: {handle.command_id}")
|
||||
|
||||
for chunk in handle:
|
||||
prefix = "OUT" if chunk.stream == "stdout" else "ERR"
|
||||
print(f"[{prefix}] {chunk.data}", end="")
|
||||
|
||||
result = handle.result
|
||||
print(f"\nExit code: {result.exit_code}")
|
||||
```
|
||||
|
||||
### Killing a Running Command
|
||||
|
||||
```python
|
||||
import threading
|
||||
import time
|
||||
|
||||
with client.sandbox(template_name="my-sandbox") as sb:
|
||||
handle = sb.run("sleep 3600", timeout=7200, wait=False)
|
||||
|
||||
# Kill after 10 seconds from another thread
|
||||
def kill_after(h, seconds):
|
||||
time.sleep(seconds)
|
||||
h.kill()
|
||||
|
||||
threading.Thread(target=kill_after, args=(handle, 10)).start()
|
||||
|
||||
for chunk in handle:
|
||||
print(chunk.data, end="")
|
||||
|
||||
result = handle.result
|
||||
print(f"Exit code: {result.exit_code}") # non-zero (killed)
|
||||
```
|
||||
|
||||
### Sending Stdin Input
|
||||
|
||||
```python
|
||||
with client.sandbox(template_name="my-sandbox") as sb:
|
||||
handle = sb.run(
|
||||
"python -c 'name = input(\"Name: \"); print(f\"Hello {name}\")'",
|
||||
timeout=30,
|
||||
wait=False,
|
||||
)
|
||||
|
||||
for chunk in handle:
|
||||
if "Name:" in chunk.data:
|
||||
handle.send_input("World\n")
|
||||
print(chunk.data, end="")
|
||||
|
||||
result = handle.result
|
||||
```
|
||||
|
||||
### Auto-Reconnect
|
||||
|
||||
`CommandHandle` (returned by `sb.run(wait=False)`) automatically
|
||||
reconnects on transient disconnects — hot-reloads, network blips, etc. No user
|
||||
code needed:
|
||||
|
||||
```python
|
||||
with client.sandbox(template_name="my-sandbox") as sb:
|
||||
handle = sb.run("make build", timeout=600, wait=False)
|
||||
|
||||
# Auto-reconnects on transient errors (hot-reload, network blips)
|
||||
for chunk in handle:
|
||||
print(chunk.data, end="")
|
||||
|
||||
result = handle.result
|
||||
```
|
||||
|
||||
For manual reconnection across process restarts:
|
||||
|
||||
```python
|
||||
with client.sandbox(template_name="my-sandbox") as sb:
|
||||
handle = sb.run("make build", timeout=600, wait=False)
|
||||
command_id = handle.command_id
|
||||
|
||||
# ... later, possibly in a different process ...
|
||||
|
||||
handle = sb.reconnect(command_id)
|
||||
for chunk in handle:
|
||||
print(chunk.data, end="")
|
||||
result = handle.result
|
||||
```
|
||||
|
||||
### No Timeout (`timeout=0`)
|
||||
|
||||
With WebSocket enabled, you can set `timeout=0` to let a command run
|
||||
indefinitely with no server-side deadline. This works with both `wait=False`
|
||||
and callbacks. Useful for long-lived processes like dev servers, file watchers,
|
||||
or background tasks that you control via `kill()`.
|
||||
|
||||
```python
|
||||
with client.sandbox(template_name="my-sandbox") as sb:
|
||||
handle = sb.run("python server.py", timeout=0, wait=False)
|
||||
|
||||
for chunk in handle:
|
||||
print(chunk.data, end="")
|
||||
if "Ready" in chunk.data:
|
||||
break # server is up, do other work
|
||||
|
||||
handle.kill() # stop when done
|
||||
```
|
||||
|
||||
> **Note:** `timeout=0` requires WebSocket support
|
||||
> (`pip install 'langsmith[sandbox]'`). Without WebSocket, `run()` falls
|
||||
> back to HTTP which has its own request-level timeout.
|
||||
|
||||
## File Operations
|
||||
|
||||
Read and write files in the sandbox:
|
||||
|
||||
```python
|
||||
# Assuming you've created a Python template
|
||||
with client.sandbox(template_name="my-python") as sb:
|
||||
# Write a file
|
||||
sb.write("/app/script.py", "print('Hello from file!')")
|
||||
|
||||
# Run the script
|
||||
result = sb.run("python /app/script.py")
|
||||
print(result.stdout) # "Hello from file!\n"
|
||||
|
||||
# Read a file (returns bytes)
|
||||
content = sb.read("/app/script.py")
|
||||
print(content.decode()) # "print('Hello from file!')"
|
||||
|
||||
# Write binary files
|
||||
sb.write("/app/data.bin", b"\x00\x01\x02\x03")
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
Templates define the container image and resources for sandboxes. **You must create a template before you can create sandboxes.**
|
||||
|
||||
```python
|
||||
# Create a template (required before creating sandboxes)
|
||||
template = client.create_template(
|
||||
name="my-python-env",
|
||||
image="python:3.12-slim", # Any Docker image
|
||||
cpu="1", # CPU limit (default: "500m")
|
||||
memory="1Gi", # Memory limit (default: "512Mi")
|
||||
)
|
||||
|
||||
# Now you can create sandboxes from this template
|
||||
with client.sandbox(template_name="my-python-env") as sb:
|
||||
result = sb.run("python --version")
|
||||
|
||||
# List all templates
|
||||
templates = client.list_templates()
|
||||
|
||||
# Get a specific template
|
||||
template = client.get_template("my-python-env")
|
||||
|
||||
# Update a template's name
|
||||
client.update_template("my-python-env", new_name="python-env-v2")
|
||||
|
||||
# Delete a template (fails if sandboxes or pools are using it)
|
||||
client.delete_template("my-python-env")
|
||||
```
|
||||
|
||||
### Common Template Images
|
||||
|
||||
```python
|
||||
# Python
|
||||
client.create_template(name="python", image="python:3.12-slim")
|
||||
|
||||
# Node.js
|
||||
client.create_template(name="node", image="node:20-slim")
|
||||
|
||||
# Ubuntu (general purpose)
|
||||
client.create_template(name="ubuntu", image="ubuntu:24.04")
|
||||
```
|
||||
|
||||
## Persistent Volumes
|
||||
|
||||
Use volumes to persist data across sandbox sessions:
|
||||
|
||||
```python
|
||||
from langsmith.sandbox import VolumeMountSpec
|
||||
|
||||
# Create a volume
|
||||
volume = client.create_volume(name="my-data", size="1Gi")
|
||||
|
||||
# Create a template with the volume mounted
|
||||
template = client.create_template(
|
||||
name="stateful-sandbox",
|
||||
image="python:3.12-slim",
|
||||
volume_mounts=[
|
||||
VolumeMountSpec(volume_name="my-data", mount_path="/data")
|
||||
],
|
||||
)
|
||||
|
||||
# Data written to /data persists across sandbox sessions
|
||||
with client.sandbox(template_name="stateful-sandbox") as sb:
|
||||
sb.write("/data/state.txt", "persistent data")
|
||||
|
||||
# Later, in a new sandbox...
|
||||
with client.sandbox(template_name="stateful-sandbox") as sb:
|
||||
content = sb.read("/data/state.txt")
|
||||
print(content.decode()) # "persistent data"
|
||||
```
|
||||
|
||||
## Pools (Pre-warmed Sandboxes)
|
||||
|
||||
Pools pre-provision sandboxes for faster startup:
|
||||
|
||||
```python
|
||||
# First create a template (without volumes - pools don't support volumes)
|
||||
client.create_template(name="fast-python", image="python:3.12-slim")
|
||||
|
||||
# Create a pool with 5 warm sandboxes
|
||||
pool = client.create_pool(
|
||||
name="python-pool",
|
||||
template_name="fast-python",
|
||||
replicas=2,
|
||||
)
|
||||
|
||||
# Sandboxes from pooled templates start faster
|
||||
with client.sandbox(template_name="fast-python") as sb:
|
||||
result = sb.run("python --version")
|
||||
|
||||
# Scale the pool
|
||||
client.update_pool("python-pool", replicas=3)
|
||||
|
||||
# Delete the pool
|
||||
client.delete_pool("python-pool")
|
||||
```
|
||||
|
||||
> **Note:** Templates with volume mounts cannot be used in pools.
|
||||
|
||||
## Reusing Existing Sandboxes
|
||||
|
||||
Get a sandbox that's already running:
|
||||
|
||||
```python
|
||||
# Create a sandbox (requires explicit cleanup)
|
||||
sb = client.create_sandbox(template_name="my-template")
|
||||
print(sb.name) # e.g., "sandbox-abc123"
|
||||
|
||||
# Later, get the same sandbox
|
||||
sb = client.get_sandbox("sandbox-abc123")
|
||||
result = sb.run("echo 'Still running!'")
|
||||
|
||||
# Clean up when done
|
||||
client.delete_sandbox("sandbox-abc123")
|
||||
```
|
||||
|
||||
## Async Sandbox Creation
|
||||
|
||||
By default, `create_sandbox()` blocks until the sandbox is ready. For
|
||||
non-blocking creation, pass `wait_for_ready=False`:
|
||||
|
||||
```python
|
||||
# Returns immediately with status="provisioning"
|
||||
sb = client.create_sandbox(template_name="my-template", wait_for_ready=False)
|
||||
print(sb.status) # "provisioning"
|
||||
|
||||
# Poll until ready using the lightweight status endpoint
|
||||
sb = client.wait_for_sandbox(sb.name, timeout=120, poll_interval=1.0)
|
||||
print(sb.status) # "ready"
|
||||
|
||||
# Now the sandbox is usable
|
||||
result = sb.run("echo hello")
|
||||
```
|
||||
|
||||
You can also poll manually for more control:
|
||||
|
||||
```python
|
||||
sb = client.create_sandbox(template_name="my-template", wait_for_ready=False)
|
||||
|
||||
while True:
|
||||
status = client.get_sandbox_status(sb.name)
|
||||
if status.status == "ready":
|
||||
sb = client.get_sandbox(sb.name)
|
||||
break
|
||||
if status.status == "failed":
|
||||
print(f"Failed: {status.status_message}")
|
||||
break
|
||||
time.sleep(1)
|
||||
```
|
||||
|
||||
> **Note:** Operations like `run()`, `write()`, and `read()` will raise
|
||||
> `SandboxNotReadyError` if called on a sandbox that isn't ready yet.
|
||||
|
||||
## Async Support
|
||||
|
||||
Full async support for all operations:
|
||||
|
||||
```python
|
||||
from langsmith.sandbox import AsyncSandboxClient
|
||||
|
||||
async def main():
|
||||
async with AsyncSandboxClient() as client:
|
||||
# Create a template first
|
||||
await client.create_template(name="async-python", image="python:3.12-slim")
|
||||
|
||||
# Use the template
|
||||
async with await client.sandbox(template_name="async-python") as sb:
|
||||
result = await sb.run("python -c 'print(1 + 1)'")
|
||||
print(result.stdout) # "2\n"
|
||||
|
||||
await sb.write("/app/test.txt", "async content")
|
||||
content = await sb.read("/app/test.txt")
|
||||
print(content.decode())
|
||||
```
|
||||
|
||||
### Async Streaming
|
||||
|
||||
```python
|
||||
async with await client.sandbox(template_name="async-python") as sb:
|
||||
handle = await sb.run("make build", timeout=600, wait=False)
|
||||
|
||||
async for chunk in handle:
|
||||
print(chunk.data, end="")
|
||||
|
||||
result = await handle.result
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The module provides type-based exceptions with a `resource_type` attribute for specific handling:
|
||||
|
||||
```python
|
||||
from langsmith.sandbox import (
|
||||
SandboxClientError, # Base exception for all sandbox errors
|
||||
ResourceCreationError, # Resource provisioning failed (check resource_type, error_type)
|
||||
ResourceNotFoundError, # Resource doesn't exist (check resource_type)
|
||||
ResourceTimeoutError, # Operation timed out (check resource_type)
|
||||
SandboxNotReadyError, # Sandbox not ready for operations yet
|
||||
SandboxConnectionError, # Network/WebSocket error
|
||||
CommandTimeoutError, # Command exceeded its timeout (extends SandboxOperationError)
|
||||
QuotaExceededError, # Quota limit reached
|
||||
)
|
||||
|
||||
try:
|
||||
with client.sandbox(template_name="my-sandbox") as sb:
|
||||
result = sb.run("sleep 999", timeout=10)
|
||||
except CommandTimeoutError as e:
|
||||
print(f"Command timed out: {e}")
|
||||
except ResourceCreationError as e:
|
||||
print(f"{e.resource_type} creation failed: {e}")
|
||||
except ResourceNotFoundError as e:
|
||||
print(f"{e.resource_type} not found: {e}")
|
||||
except ResourceTimeoutError as e:
|
||||
print(f"Timeout waiting for {e.resource_type}: {e}")
|
||||
except SandboxConnectionError as e:
|
||||
print(f"Connection error: {e}")
|
||||
except SandboxClientError as e:
|
||||
print(f"Error: {e}")
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### SandboxClient
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `sandbox(template_name, ...)` | Create a sandbox (auto-deleted on context exit) |
|
||||
| `create_sandbox(template_name, *, wait_for_ready=True, ...)` | Create a sandbox (requires explicit delete). Pass `wait_for_ready=False` for async creation. |
|
||||
| `get_sandbox(name)` | Get an existing sandbox by name |
|
||||
| `get_sandbox_status(name)` | Get lightweight provisioning status (`ResourceStatus`) |
|
||||
| `wait_for_sandbox(name, *, timeout=120, poll_interval=1.0)` | Poll until sandbox is ready or failed |
|
||||
| `list_sandboxes()` | List all sandboxes |
|
||||
| `update_sandbox(name, *, new_name)` | Update a sandbox's display name |
|
||||
| `delete_sandbox(name)` | Delete a sandbox |
|
||||
| `create_template(name, image, ...)` | Create a template |
|
||||
| `list_templates()` | List all templates |
|
||||
| `get_template(name)` | Get template by name |
|
||||
| `update_template(name, *, new_name)` | Update a template's display name |
|
||||
| `delete_template(name)` | Delete a template |
|
||||
| `create_volume(name, size)` | Create a persistent volume |
|
||||
| `list_volumes()` | List all volumes |
|
||||
| `update_volume(name, *, new_name, size)` | Update a volume's name or size |
|
||||
| `delete_volume(name)` | Delete a volume |
|
||||
| `create_pool(name, template_name, replicas)` | Create a pool |
|
||||
| `list_pools()` | List all pools |
|
||||
| `update_pool(name, *, replicas, new_name)` | Update pool replicas or name |
|
||||
| `delete_pool(name)` | Delete a pool |
|
||||
|
||||
### Sandbox
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `name` | Display name |
|
||||
| `template_name` | Template used to create this sandbox |
|
||||
| `status` | Lifecycle status: `"provisioning"`, `"ready"`, or `"failed"` |
|
||||
| `status_message` | Human-readable details when status is `"failed"`, `None` otherwise |
|
||||
| `dataplane_url` | URL for runtime operations (only functional when status is `"ready"`) |
|
||||
| `id` | Unique identifier (UUID) |
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `run(command, *, timeout=60, on_stdout=None, on_stderr=None, wait=True)` | Execute a shell command. Returns `ExecutionResult` or `CommandHandle` (when `wait=False`). |
|
||||
| `reconnect(command_id, *, stdout_offset=0, stderr_offset=0)` | Reconnect to a running command. Returns `CommandHandle`. |
|
||||
| `write(path, content)` | Write file (str or bytes) |
|
||||
| `read(path)` | Read file (returns bytes) |
|
||||
|
||||
### ExecutionResult
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `stdout` | Standard output (str) |
|
||||
| `stderr` | Standard error (str) |
|
||||
| `exit_code` | Exit code (int) |
|
||||
| `success` | True if exit_code == 0 |
|
||||
|
||||
### ResourceStatus
|
||||
|
||||
Returned by `client.get_sandbox_status()`.
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `status` | Lifecycle status: `"provisioning"`, `"ready"`, or `"failed"` |
|
||||
| `status_message` | Human-readable details when `"failed"`, `None` otherwise |
|
||||
|
||||
### CommandHandle
|
||||
|
||||
Returned by `sb.run(wait=False)`. Iterable, yielding `OutputChunk` objects.
|
||||
|
||||
| Property / Method | Description |
|
||||
|-------------------|-------------|
|
||||
| `command_id` | Server-assigned command ID |
|
||||
| `pid` | Process ID on the sandbox |
|
||||
| `result` | Final `ExecutionResult` (blocks until complete) |
|
||||
| `kill()` | Send SIGKILL to the running command |
|
||||
| `send_input(data)` | Write string data to the command's stdin |
|
||||
| `reconnect()` | Reconnect from last known offsets |
|
||||
|
||||
### OutputChunk
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `stream` | `"stdout"` or `"stderr"` |
|
||||
| `data` | Text content of this chunk (str) |
|
||||
| `offset` | Byte offset within the stream (int) |
|
||||
Reference in New Issue
Block a user