mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 06:53:14 +00:00
feat: migrate LangSmith sandbox creation to snapshot API (#1201)
* feat: migrate LangSmith sandbox creation to snapshot API Replaces the template-based sandbox flow (DEFAULT_SANDBOX_TEMPLATE_NAME / DEFAULT_SANDBOX_TEMPLATE_IMAGE) with the new snapshot-based flow. - New required env var DEFAULT_SANDBOX_SNAPSHOT_ID (UUID of a pre-built LangSmith snapshot; build out-of-band via UI or SandboxClient.create_snapshot) - Optional DEFAULT_SANDBOX_SNAPSHOT_FS_CAPACITY_BYTES overrides the root FS size at boot (default 32 GiB) - Startup-time validation via a FastAPI lifespan hook: the server refuses to boot with a clear ValueError if SANDBOX_TYPE=langsmith and DEFAULT_SANDBOX_SNAPSHOT_ID is unset, so failures surface in boot logs rather than on the first thread - Reconnect-to-existing-sandbox path unchanged - Docs (INSTALLATION.md, CUSTOMIZATION.md) updated to describe the new snapshot workflow * fix: format create_sandbox_snapshot.py to pass ruff --------- Co-authored-by: aran-yogesh <yogesh.mahendran@langchain.dev>
This commit is contained in:
parent
f1907521f3
commit
5925a90a95
7 changed files with 168 additions and 93 deletions
|
|
@ -24,13 +24,15 @@ return create_deep_agent(
|
||||||
|
|
||||||
By default, Open SWE runs each task in a [LangSmith cloud sandbox](https://docs.smith.langchain.com/) — an isolated Linux environment where the agent clones the repo and executes commands. Sandbox creation and connection is handled in `agent/integrations/langsmith.py`.
|
By default, Open SWE runs each task in a [LangSmith cloud sandbox](https://docs.smith.langchain.com/) — an isolated Linux environment where the agent clones the repo and executes commands. Sandbox creation and connection is handled in `agent/integrations/langsmith.py`.
|
||||||
|
|
||||||
### Using a custom sandbox template
|
### Using a custom sandbox snapshot
|
||||||
|
|
||||||
Set environment variables to use a custom Docker image:
|
Build a snapshot in LangSmith (UI or `SandboxClient.create_snapshot`) from your Docker image and point Open SWE at its UUID:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
DEFAULT_SANDBOX_TEMPLATE_NAME="my-template" # Template registered in LangSmith
|
DEFAULT_SANDBOX_SNAPSHOT_ID="<snapshot-uuid>" # Required
|
||||||
DEFAULT_SANDBOX_TEMPLATE_IMAGE="my-org/my-image:latest" # Docker image
|
DEFAULT_SANDBOX_SNAPSHOT_FS_CAPACITY_BYTES="34359738368" # Optional, default 32 GiB
|
||||||
|
DEFAULT_SANDBOX_VCPUS="4" # Optional, default 4
|
||||||
|
DEFAULT_SANDBOX_MEM_BYTES="16106127360" # Optional, default 15 GiB
|
||||||
```
|
```
|
||||||
|
|
||||||
This is useful for pre-installing languages, frameworks, or internal tools that your repos depend on — reducing setup time per agent run.
|
This is useful for pre-installing languages, frameworks, or internal tools that your repos depend on — reducing setup time per agent run.
|
||||||
|
|
|
||||||
|
|
@ -129,17 +129,37 @@ To set up per-user OAuth:
|
||||||
3. Enter the **Client ID** and **Client Secret** from your GitHub App (found on the GitHub App settings page under **OAuth credentials**)
|
3. Enter the **Client ID** and **Client Secret** from your GitHub App (found on the GitHub App settings page under **OAuth credentials**)
|
||||||
4. Save. You'll reference this Provider ID as `GITHUB_OAUTH_PROVIDER_ID` in your environment variables.
|
4. Save. You'll reference this Provider ID as `GITHUB_OAUTH_PROVIDER_ID` in your environment variables.
|
||||||
|
|
||||||
### 4c. Sandbox templates (optional)
|
### 4c. Sandbox snapshots
|
||||||
|
|
||||||
LangSmith sandboxes provide the isolated execution environment for each agent run. You can create a template using the same Docker image we use internally by visiting the sandbox page in LangSmith, and setting the following fields:
|
LangSmith sandboxes provide the isolated execution environment for each agent run. Open SWE boots each sandbox from a pre-built **snapshot** — you build the snapshot once (from a Docker image) and then reference it by UUID.
|
||||||
|
|
||||||
- `Name`: you can set this to whatever name you'd like, e.g. `open-swe`
|
Build a snapshot in the LangSmith UI (Sandboxes → Snapshots → New), or via the SDK:
|
||||||
- `Container Image`: `bracelangchain/deepagents-sandbox:v1` this contains the [Docker file in this repo](./Dockerfile)
|
|
||||||
- `CPU`: `500m`
|
|
||||||
- `Memory`: `4096Mi`
|
|
||||||
- `Ephemeral Storage`: `15Gi`
|
|
||||||
|
|
||||||
> If you don't set these, you can use a Python based docker image in the template.
|
```python
|
||||||
|
from langsmith.sandbox import SandboxClient
|
||||||
|
|
||||||
|
client = SandboxClient(api_key="<your key>")
|
||||||
|
snapshot = client.create_snapshot(
|
||||||
|
name="open-swe",
|
||||||
|
docker_image="bracelangchain/deepagents-sandbox:v1", # built from ./Dockerfile
|
||||||
|
fs_capacity_bytes=32 * 1024**3,
|
||||||
|
)
|
||||||
|
print(snapshot.id)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then set the resulting UUID in your environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEFAULT_SANDBOX_SNAPSHOT_ID="<snapshot-uuid>"
|
||||||
|
# Optional; overrides the snapshot's root FS size at sandbox boot. Default is 32 GiB.
|
||||||
|
DEFAULT_SANDBOX_SNAPSHOT_FS_CAPACITY_BYTES="34359738368"
|
||||||
|
# Optional; number of vCPUs per sandbox. Default is 4.
|
||||||
|
DEFAULT_SANDBOX_VCPUS="4"
|
||||||
|
# Optional; memory in bytes per sandbox. Default is 15 GiB.
|
||||||
|
DEFAULT_SANDBOX_MEM_BYTES="16106127360"
|
||||||
|
```
|
||||||
|
|
||||||
|
`DEFAULT_SANDBOX_SNAPSHOT_ID` is required when `SANDBOX_TYPE=langsmith`. The server validates this at startup and refuses to boot if it's missing.
|
||||||
|
|
||||||
## 5. Set up triggers
|
## 5. Set up triggers
|
||||||
|
|
||||||
|
|
@ -347,8 +367,10 @@ SLACK_SIGNING_SECRET=""
|
||||||
EXA_API_KEY="" # From https://dashboard.exa.ai
|
EXA_API_KEY="" # From https://dashboard.exa.ai
|
||||||
|
|
||||||
# === Sandbox (optional) ===
|
# === Sandbox (optional) ===
|
||||||
DEFAULT_SANDBOX_TEMPLATE_NAME="" # Custom sandbox template name (default: deepagents-cli)
|
DEFAULT_SANDBOX_SNAPSHOT_ID="" # Required when SANDBOX_TYPE=langsmith (see step 4c)
|
||||||
DEFAULT_SANDBOX_TEMPLATE_IMAGE="" # Custom Docker image (default: python:3)
|
DEFAULT_SANDBOX_SNAPSHOT_FS_CAPACITY_BYTES="" # Root FS size in bytes (default: 32 GiB)
|
||||||
|
DEFAULT_SANDBOX_VCPUS="" # vCPUs per sandbox (default: 4)
|
||||||
|
DEFAULT_SANDBOX_MEM_BYTES="" # Memory in bytes per sandbox (default: 15 GiB)
|
||||||
|
|
||||||
# === Token Encryption ===
|
# === Token Encryption ===
|
||||||
TOKEN_ENCRYPTION_KEY="" # Generate with: openssl rand -base64 32
|
TOKEN_ENCRYPTION_KEY="" # Generate with: openssl rand -base64 32
|
||||||
|
|
@ -442,8 +464,9 @@ The `langgraph.json` at the project root already defines the graph entry point a
|
||||||
|
|
||||||
- Verify `LANGSMITH_API_KEY_PROD` is set and valid
|
- Verify `LANGSMITH_API_KEY_PROD` is set and valid
|
||||||
- Check LangSmith sandbox quotas in your workspace settings
|
- Check LangSmith sandbox quotas in your workspace settings
|
||||||
- If you see `Failed to check template ''`, ensure either `DEFAULT_SANDBOX_TEMPLATE_NAME` is set or that your LangSmith API key has permissions to create sandbox templates
|
- If the server refuses to start with `DEFAULT_SANDBOX_SNAPSHOT_ID must be set`, build a snapshot (see step 4c) and export its UUID
|
||||||
- If you get a 403 Forbidden error on the sandbox templates endpoint, your LangSmith workspace may not have sandbox access enabled — contact LangSmith support
|
- If you see `Failed to create sandbox from snapshot '<id>'`, confirm the snapshot exists in your workspace and has status `ready`
|
||||||
|
- If you get a 403 Forbidden error on the sandbox endpoints, your LangSmith workspace may not have sandbox access enabled — contact LangSmith support
|
||||||
|
|
||||||
### Agent not responding to comments
|
### Agent not responding to comments
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,22 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import contextlib
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from deepagents.backends import LangSmithSandbox
|
from deepagents.backends import LangSmithSandbox
|
||||||
from deepagents.backends.protocol import SandboxBackendProtocol
|
from deepagents.backends.protocol import SandboxBackendProtocol
|
||||||
from langsmith.sandbox import SandboxClient, SandboxTemplate
|
from langsmith.sandbox import SandboxClient
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_SNAPSHOT_FS_CAPACITY_BYTES = 32 * 1024**3
|
||||||
|
DEFAULT_SANDBOX_VCPUS = 4
|
||||||
|
DEFAULT_SANDBOX_MEM_BYTES = 15 * 1024**3 # 15 GiB
|
||||||
|
|
||||||
|
|
||||||
def _get_langsmith_api_key() -> str | None:
|
def _get_langsmith_api_key() -> str | None:
|
||||||
"""Get LangSmith API key from environment.
|
"""Get LangSmith API key from environment.
|
||||||
|
|
@ -27,11 +29,16 @@ def _get_langsmith_api_key() -> str | None:
|
||||||
return os.environ.get("LANGSMITH_API_KEY") or os.environ.get("LANGSMITH_API_KEY_PROD")
|
return os.environ.get("LANGSMITH_API_KEY") or os.environ.get("LANGSMITH_API_KEY_PROD")
|
||||||
|
|
||||||
|
|
||||||
def _get_sandbox_template_config() -> tuple[str | None, str | None]:
|
def _get_sandbox_snapshot_config() -> tuple[str | None, int, int, int]:
|
||||||
"""Get sandbox template configuration from environment."""
|
"""Get sandbox snapshot configuration from environment."""
|
||||||
template_name = os.environ.get("DEFAULT_SANDBOX_TEMPLATE_NAME")
|
snapshot_id = os.environ.get("DEFAULT_SANDBOX_SNAPSHOT_ID")
|
||||||
template_image = os.environ.get("DEFAULT_SANDBOX_TEMPLATE_IMAGE")
|
raw_capacity = os.environ.get("DEFAULT_SANDBOX_SNAPSHOT_FS_CAPACITY_BYTES")
|
||||||
return template_name, template_image
|
fs_capacity_bytes = int(raw_capacity) if raw_capacity else DEFAULT_SNAPSHOT_FS_CAPACITY_BYTES
|
||||||
|
raw_vcpus = os.environ.get("DEFAULT_SANDBOX_VCPUS")
|
||||||
|
vcpus = int(raw_vcpus) if raw_vcpus else DEFAULT_SANDBOX_VCPUS
|
||||||
|
raw_mem = os.environ.get("DEFAULT_SANDBOX_MEM_BYTES")
|
||||||
|
mem_bytes = int(raw_mem) if raw_mem else DEFAULT_SANDBOX_MEM_BYTES
|
||||||
|
return snapshot_id, fs_capacity_bytes, vcpus, mem_bytes
|
||||||
|
|
||||||
|
|
||||||
def _configure_github_proxy(sandbox_name: str, github_token: str) -> None:
|
def _configure_github_proxy(sandbox_name: str, github_token: str) -> None:
|
||||||
|
|
@ -99,13 +106,15 @@ def create_langsmith_sandbox(
|
||||||
SandboxBackendProtocol instance
|
SandboxBackendProtocol instance
|
||||||
"""
|
"""
|
||||||
api_key = _get_langsmith_api_key()
|
api_key = _get_langsmith_api_key()
|
||||||
template_name, template_image = _get_sandbox_template_config()
|
snapshot_id, fs_capacity_bytes, vcpus, mem_bytes = _get_sandbox_snapshot_config()
|
||||||
|
|
||||||
provider = LangSmithProvider(api_key=api_key)
|
provider = LangSmithProvider(api_key=api_key)
|
||||||
backend = provider.get_or_create(
|
backend = provider.get_or_create(
|
||||||
sandbox_id=sandbox_id,
|
sandbox_id=sandbox_id,
|
||||||
template=template_name,
|
snapshot_id=snapshot_id,
|
||||||
template_image=template_image,
|
fs_capacity_bytes=fs_capacity_bytes,
|
||||||
|
vcpus=vcpus,
|
||||||
|
mem_bytes=mem_bytes,
|
||||||
)
|
)
|
||||||
_update_thread_sandbox_metadata(backend.id)
|
_update_thread_sandbox_metadata(backend.id)
|
||||||
|
|
||||||
|
|
@ -169,10 +178,6 @@ class SandboxProvider(ABC):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_TEMPLATE_NAME = "open-swe-new"
|
|
||||||
DEFAULT_TEMPLATE_IMAGE = "python:3"
|
|
||||||
|
|
||||||
|
|
||||||
class LangSmithProvider(SandboxProvider):
|
class LangSmithProvider(SandboxProvider):
|
||||||
"""LangSmith sandbox provider implementation."""
|
"""LangSmith sandbox provider implementation."""
|
||||||
|
|
||||||
|
|
@ -185,13 +190,32 @@ class LangSmithProvider(SandboxProvider):
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
self._client: SandboxClient = sandbox.SandboxClient(api_key=self._api_key)
|
self._client: SandboxClient = sandbox.SandboxClient(api_key=self._api_key)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_startup_config(cls) -> None:
|
||||||
|
"""Validate env-var configuration at server startup. Raises ValueError if invalid."""
|
||||||
|
if not os.environ.get("DEFAULT_SANDBOX_SNAPSHOT_ID"):
|
||||||
|
msg = "DEFAULT_SANDBOX_SNAPSHOT_ID must be set when SANDBOX_TYPE=langsmith"
|
||||||
|
raise ValueError(msg)
|
||||||
|
raw_capacity = os.environ.get("DEFAULT_SANDBOX_SNAPSHOT_FS_CAPACITY_BYTES")
|
||||||
|
if raw_capacity:
|
||||||
|
try:
|
||||||
|
int(raw_capacity)
|
||||||
|
except ValueError as e:
|
||||||
|
msg = (
|
||||||
|
"DEFAULT_SANDBOX_SNAPSHOT_FS_CAPACITY_BYTES must be an integer, "
|
||||||
|
f"got {raw_capacity!r}"
|
||||||
|
)
|
||||||
|
raise ValueError(msg) from e
|
||||||
|
|
||||||
def get_or_create(
|
def get_or_create(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
sandbox_id: str | None = None,
|
sandbox_id: str | None = None,
|
||||||
timeout: int = 180,
|
timeout: int = 180,
|
||||||
template: str | None = None,
|
snapshot_id: str | None = None,
|
||||||
template_image: str | None = None,
|
fs_capacity_bytes: int | None = None,
|
||||||
|
vcpus: int | None = None,
|
||||||
|
mem_bytes: int | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> SandboxBackendProtocol:
|
) -> SandboxBackendProtocol:
|
||||||
"""Get existing or create new LangSmith sandbox."""
|
"""Get existing or create new LangSmith sandbox."""
|
||||||
|
|
@ -206,74 +230,24 @@ class LangSmithProvider(SandboxProvider):
|
||||||
raise RuntimeError(msg) from e
|
raise RuntimeError(msg) from e
|
||||||
return LangSmithSandbox(sandbox)
|
return LangSmithSandbox(sandbox)
|
||||||
|
|
||||||
resolved_template_name, resolved_image_name = self._resolve_template(
|
if not snapshot_id:
|
||||||
template, template_image
|
msg = "DEFAULT_SANDBOX_SNAPSHOT_ID must be set when SANDBOX_TYPE=langsmith"
|
||||||
)
|
raise ValueError(msg)
|
||||||
|
|
||||||
self._ensure_template(resolved_template_name, resolved_image_name)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sandbox = self._client.create_sandbox(
|
sandbox = self._client.create_sandbox(
|
||||||
template_name=resolved_template_name, timeout=timeout
|
snapshot_id=snapshot_id,
|
||||||
|
fs_capacity_bytes=fs_capacity_bytes,
|
||||||
|
vcpus=vcpus,
|
||||||
|
mem_bytes=mem_bytes,
|
||||||
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = f"Failed to create sandbox from template '{resolved_template_name}': {e}"
|
msg = f"Failed to create sandbox from snapshot '{snapshot_id}': {e}"
|
||||||
raise RuntimeError(msg) from e
|
raise RuntimeError(msg) from e
|
||||||
|
|
||||||
for _ in range(timeout // 2):
|
|
||||||
try:
|
|
||||||
result = sandbox.run("echo ready", timeout=5)
|
|
||||||
if result.exit_code == 0:
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
time.sleep(2)
|
|
||||||
else:
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
self._client.delete_sandbox(sandbox.name)
|
|
||||||
msg = f"LangSmith sandbox failed to start within {timeout} seconds"
|
|
||||||
raise RuntimeError(msg)
|
|
||||||
|
|
||||||
return LangSmithSandbox(sandbox)
|
return LangSmithSandbox(sandbox)
|
||||||
|
|
||||||
def delete(self, *, sandbox_id: str, **kwargs: Any) -> None:
|
def delete(self, *, sandbox_id: str, **kwargs: Any) -> None:
|
||||||
"""Delete a LangSmith sandbox."""
|
"""Delete a LangSmith sandbox."""
|
||||||
self._client.delete_sandbox(sandbox_id)
|
self._client.delete_sandbox(sandbox_id)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _resolve_template(
|
|
||||||
template: SandboxTemplate | str | None,
|
|
||||||
template_image: str | None = None,
|
|
||||||
) -> tuple[str, str]:
|
|
||||||
"""Resolve template name and image from kwargs."""
|
|
||||||
resolved_image = template_image or DEFAULT_TEMPLATE_IMAGE
|
|
||||||
if template is None:
|
|
||||||
return DEFAULT_TEMPLATE_NAME, resolved_image
|
|
||||||
if isinstance(template, str):
|
|
||||||
return template, resolved_image
|
|
||||||
if template_image is None and template.image:
|
|
||||||
resolved_image = template.image
|
|
||||||
return template.name, resolved_image
|
|
||||||
|
|
||||||
def _ensure_template(
|
|
||||||
self,
|
|
||||||
template_name: str,
|
|
||||||
template_image: str,
|
|
||||||
) -> None:
|
|
||||||
"""Ensure template exists, creating it if needed."""
|
|
||||||
from langsmith.sandbox import ResourceNotFoundError
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._client.get_template(template_name)
|
|
||||||
except ResourceNotFoundError as e:
|
|
||||||
if e.resource_type != "template":
|
|
||||||
msg = f"Unexpected resource not found: {e}"
|
|
||||||
raise RuntimeError(msg) from e
|
|
||||||
try:
|
|
||||||
self._client.create_template(name=template_name, image=template_image)
|
|
||||||
except Exception as create_err:
|
|
||||||
msg = f"Failed to create template '{template_name}': {create_err}"
|
|
||||||
raise RuntimeError(msg) from create_err
|
|
||||||
except Exception as e:
|
|
||||||
msg = f"Failed to check template '{template_name}': {e}"
|
|
||||||
raise RuntimeError(msg) from e
|
|
||||||
|
|
|
||||||
|
|
@ -33,3 +33,17 @@ def create_sandbox(sandbox_id: str | None = None):
|
||||||
supported = ", ".join(sorted(SANDBOX_FACTORIES))
|
supported = ", ".join(sorted(SANDBOX_FACTORIES))
|
||||||
raise ValueError(f"Invalid sandbox type: {sandbox_type}. Supported types: {supported}")
|
raise ValueError(f"Invalid sandbox type: {sandbox_type}. Supported types: {supported}")
|
||||||
return factory(sandbox_id)
|
return factory(sandbox_id)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_sandbox_startup_config() -> None:
|
||||||
|
"""Validate the configured sandbox provider's env vars at server startup.
|
||||||
|
|
||||||
|
Raises ValueError if the active provider's configuration is invalid.
|
||||||
|
Called from the FastAPI lifespan hook so errors surface at boot rather
|
||||||
|
than on the first sandbox creation.
|
||||||
|
"""
|
||||||
|
sandbox_type = os.getenv("SANDBOX_TYPE", "langsmith")
|
||||||
|
if sandbox_type == "langsmith":
|
||||||
|
from agent.integrations.langsmith import LangSmithProvider
|
||||||
|
|
||||||
|
LangSmithProvider.validate_startup_config()
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
@ -39,6 +41,7 @@ from .utils.linear import post_linear_trace_comment
|
||||||
from .utils.linear_team_repo_map import LINEAR_TEAM_TO_REPO
|
from .utils.linear_team_repo_map import LINEAR_TEAM_TO_REPO
|
||||||
from .utils.multimodal import dedupe_urls, extract_image_urls, fetch_image_block
|
from .utils.multimodal import dedupe_urls, extract_image_urls, fetch_image_block
|
||||||
from .utils.repo import extract_repo_from_text
|
from .utils.repo import extract_repo_from_text
|
||||||
|
from .utils.sandbox import validate_sandbox_startup_config
|
||||||
from .utils.slack import (
|
from .utils.slack import (
|
||||||
add_slack_reaction,
|
add_slack_reaction,
|
||||||
fetch_slack_thread_messages,
|
fetch_slack_thread_messages,
|
||||||
|
|
@ -54,7 +57,14 @@ from .utils.slack import (
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
|
||||||
|
validate_sandbox_startup_config()
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
LINEAR_WEBHOOK_SECRET = os.environ.get("LINEAR_WEBHOOK_SECRET", "")
|
LINEAR_WEBHOOK_SECRET = os.environ.get("LINEAR_WEBHOOK_SECRET", "")
|
||||||
GITHUB_WEBHOOK_SECRET = os.environ.get("GITHUB_WEBHOOK_SECRET", "")
|
GITHUB_WEBHOOK_SECRET = os.environ.get("GITHUB_WEBHOOK_SECRET", "")
|
||||||
|
|
|
||||||
47
scripts/create_sandbox_snapshot.py
Normal file
47
scripts/create_sandbox_snapshot.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
"""Create a LangSmith sandbox snapshot for open-swe."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
from langsmith.sandbox import SandboxClient
|
||||||
|
|
||||||
|
DEFAULT_IMAGE = "bracelangchain/deepagents-sandbox:v1"
|
||||||
|
DEFAULT_FS_CAPACITY = 32 * 1024**3 # 32 GiB
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Create a LangSmith sandbox snapshot")
|
||||||
|
parser.add_argument(
|
||||||
|
"--name", default="open-swe-new", help="Snapshot name (default: open-swe-new)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--image", default=DEFAULT_IMAGE, help=f"Docker image (default: {DEFAULT_IMAGE})"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--fs-capacity",
|
||||||
|
type=int,
|
||||||
|
default=DEFAULT_FS_CAPACITY,
|
||||||
|
help="FS capacity in bytes (default: 32 GiB)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--api-key",
|
||||||
|
default=os.environ.get("LANGSMITH_API_KEY") or os.environ.get("LANGSMITH_API_KEY_PROD"),
|
||||||
|
help="LangSmith API key",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.api_key:
|
||||||
|
raise SystemExit("Set LANGSMITH_API_KEY or pass --api-key")
|
||||||
|
|
||||||
|
client = SandboxClient(api_key=args.api_key)
|
||||||
|
snapshot = client.create_snapshot(
|
||||||
|
name=args.name,
|
||||||
|
docker_image=args.image,
|
||||||
|
fs_capacity_bytes=args.fs_capacity,
|
||||||
|
)
|
||||||
|
print(f"Snapshot created: {snapshot.id}")
|
||||||
|
print(f"\nAdd to your .env:\n DEFAULT_SANDBOX_SNAPSHOT_ID={snapshot.id}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
5
scripts/list_snapshots.py
Normal file
5
scripts/list_snapshots.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from langsmith.sandbox import SandboxClient
|
||||||
|
|
||||||
|
c = SandboxClient()
|
||||||
|
for s in c.list_snapshots():
|
||||||
|
print(s.id, s.name, s.status)
|
||||||
Loading…
Add table
Reference in a new issue