langchain-e2b 0.0.3__tar.gz → 0.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/CHANGELOG.md +5 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/PKG-INFO +21 -4
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/README.md +20 -3
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/langchain_e2b/_version.py +1 -1
- langchain_e2b-0.0.4/langchain_e2b/provider.py +215 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/pyproject.toml +5 -1
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/integration_tests/test_integration.py +42 -0
- langchain_e2b-0.0.4/tests/unit_tests/test_provider.py +310 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/uv.lock +1698 -57
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/.github/workflows/ci.yml +0 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/.github/workflows/release.yml +0 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/.gitignore +0 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/LICENSE +0 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/Makefile +0 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/langchain_e2b/__init__.py +0 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/langchain_e2b/sandbox.py +0 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/__init__.py +0 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/integration_tests/__init__.py +0 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/test_import.py +0 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/unit_tests/__init__.py +0 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/unit_tests/test_import.py +0 -0
- {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/unit_tests/test_sandbox.py +0 -0
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## 0.0.4
|
|
6
|
+
|
|
7
|
+
- Added a Deep Agents Code sandbox provider entry point so `dcode --sandbox e2b`
|
|
8
|
+
works after installing `langchain-e2b` into the `dcode` environment.
|
|
9
|
+
|
|
5
10
|
## 0.0.3
|
|
6
11
|
|
|
7
12
|
- Removed the `E2BProvider` lifecycle helper. Use the E2B SDK to create,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langchain-e2b
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: E2B sandbox integration for Deep Agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/e2b-dev/langchain-e2b
|
|
6
6
|
Project-URL: Repository, https://github.com/e2b-dev/langchain-e2b
|
|
@@ -33,6 +33,8 @@ Description-Content-Type: text/markdown
|
|
|
33
33
|
pip install langchain-e2b
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
## Deep Agents SDK
|
|
37
|
+
|
|
36
38
|
```python
|
|
37
39
|
from e2b import Sandbox
|
|
38
40
|
from langchain_e2b import E2BSandbox
|
|
@@ -47,15 +49,30 @@ finally:
|
|
|
47
49
|
e2b_sandbox.kill()
|
|
48
50
|
```
|
|
49
51
|
|
|
52
|
+
## Deep Agents Code
|
|
53
|
+
|
|
54
|
+
Install `langchain-e2b` into the `dcode` environment, then run with the E2B
|
|
55
|
+
sandbox provider:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
dcode --install langchain-e2b --package
|
|
59
|
+
export E2B_API_KEY=...
|
|
60
|
+
dcode --sandbox e2b
|
|
61
|
+
```
|
|
62
|
+
|
|
50
63
|
## What is this?
|
|
51
64
|
|
|
52
65
|
`langchain-e2b` adapts an existing E2B sandbox to the Deep Agents sandbox
|
|
53
66
|
protocol. It uses the low-level `e2b` SDK so Deep Agents can run shell commands
|
|
54
67
|
and move files through the standard Deep Agents sandbox interface.
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
the E2B SDK to create, connect to, configure, and kill
|
|
58
|
-
connected sandbox object to `E2BSandbox`.
|
|
69
|
+
For SDK use, this package intentionally does not hide E2B sandbox lifecycle
|
|
70
|
+
management. Use the E2B SDK to create, connect to, configure, and kill
|
|
71
|
+
sandboxes, then pass the connected sandbox object to `E2BSandbox`.
|
|
72
|
+
|
|
73
|
+
For Deep Agents Code, the package also exposes a `dcode` sandbox provider entry
|
|
74
|
+
point. The provider creates, reconnects to, and deletes E2B sandboxes for
|
|
75
|
+
`dcode --sandbox e2b`.
|
|
59
76
|
|
|
60
77
|
## Contributing
|
|
61
78
|
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
pip install langchain-e2b
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Deep Agents SDK
|
|
14
|
+
|
|
13
15
|
```python
|
|
14
16
|
from e2b import Sandbox
|
|
15
17
|
from langchain_e2b import E2BSandbox
|
|
@@ -24,15 +26,30 @@ finally:
|
|
|
24
26
|
e2b_sandbox.kill()
|
|
25
27
|
```
|
|
26
28
|
|
|
29
|
+
## Deep Agents Code
|
|
30
|
+
|
|
31
|
+
Install `langchain-e2b` into the `dcode` environment, then run with the E2B
|
|
32
|
+
sandbox provider:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
dcode --install langchain-e2b --package
|
|
36
|
+
export E2B_API_KEY=...
|
|
37
|
+
dcode --sandbox e2b
|
|
38
|
+
```
|
|
39
|
+
|
|
27
40
|
## What is this?
|
|
28
41
|
|
|
29
42
|
`langchain-e2b` adapts an existing E2B sandbox to the Deep Agents sandbox
|
|
30
43
|
protocol. It uses the low-level `e2b` SDK so Deep Agents can run shell commands
|
|
31
44
|
and move files through the standard Deep Agents sandbox interface.
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
the E2B SDK to create, connect to, configure, and kill
|
|
35
|
-
connected sandbox object to `E2BSandbox`.
|
|
46
|
+
For SDK use, this package intentionally does not hide E2B sandbox lifecycle
|
|
47
|
+
management. Use the E2B SDK to create, connect to, configure, and kill
|
|
48
|
+
sandboxes, then pass the connected sandbox object to `E2BSandbox`.
|
|
49
|
+
|
|
50
|
+
For Deep Agents Code, the package also exposes a `dcode` sandbox provider entry
|
|
51
|
+
point. The provider creates, reconnects to, and deletes E2B sandboxes for
|
|
52
|
+
`dcode --sandbox e2b`.
|
|
36
53
|
|
|
37
54
|
## Contributing
|
|
38
55
|
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""E2B sandbox lifecycle provider for Deep Agents Code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import NoReturn
|
|
8
|
+
|
|
9
|
+
import e2b
|
|
10
|
+
from deepagents_code.integrations.sandbox_provider import (
|
|
11
|
+
SandboxInstallHint,
|
|
12
|
+
SandboxNotFoundError,
|
|
13
|
+
SandboxProvider,
|
|
14
|
+
SandboxProviderMetadata,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from langchain_e2b.sandbox import DEFAULT_WORKDIR, E2BSandbox
|
|
18
|
+
|
|
19
|
+
DEFAULT_SANDBOX_TIMEOUT = 30 * 60
|
|
20
|
+
DEFAULT_COMMAND_TIMEOUT = 30 * 60
|
|
21
|
+
|
|
22
|
+
EnvResolver = Callable[[str], str | None]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _default_resolve_env_var(name: str) -> str | None:
|
|
26
|
+
if not name.startswith("DEEPAGENTS_CODE_"):
|
|
27
|
+
prefixed = f"DEEPAGENTS_CODE_{name}"
|
|
28
|
+
if prefixed in os.environ:
|
|
29
|
+
return os.environ[prefixed] or None
|
|
30
|
+
return os.environ.get(name) or None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve_optional_env_var(
|
|
34
|
+
resolve_env_var: EnvResolver,
|
|
35
|
+
name: str,
|
|
36
|
+
) -> str | None:
|
|
37
|
+
value = resolve_env_var(name)
|
|
38
|
+
return value or None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _resolve_int(
|
|
42
|
+
value: int | str | None,
|
|
43
|
+
*,
|
|
44
|
+
name: str,
|
|
45
|
+
default: int,
|
|
46
|
+
allow_zero: bool = False,
|
|
47
|
+
) -> int:
|
|
48
|
+
if value is None:
|
|
49
|
+
return default
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
resolved = int(value)
|
|
53
|
+
except (TypeError, ValueError) as exc:
|
|
54
|
+
msg = f"{name} must be an integer number of seconds"
|
|
55
|
+
raise ValueError(msg) from exc
|
|
56
|
+
|
|
57
|
+
lower_bound = 0 if allow_zero else 1
|
|
58
|
+
if resolved < lower_bound:
|
|
59
|
+
requirement = "non-negative" if allow_zero else "positive"
|
|
60
|
+
msg = f"{name} must be {requirement}"
|
|
61
|
+
raise ValueError(msg)
|
|
62
|
+
return resolved
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _raise_unsupported_kwargs(kwargs: dict[str, object]) -> NoReturn:
|
|
66
|
+
names = ", ".join(sorted(kwargs))
|
|
67
|
+
msg = f"Received unsupported arguments: {names}"
|
|
68
|
+
raise TypeError(msg)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class E2BProvider(SandboxProvider):
|
|
72
|
+
"""Manage E2B sandboxes for Deep Agents Code."""
|
|
73
|
+
|
|
74
|
+
_metadata = SandboxProviderMetadata(
|
|
75
|
+
name="e2b",
|
|
76
|
+
working_dir=DEFAULT_WORKDIR,
|
|
77
|
+
install=SandboxInstallHint(kind="package", name="langchain-e2b"),
|
|
78
|
+
supports_sandbox_id=True,
|
|
79
|
+
supports_snapshot_name=False,
|
|
80
|
+
backend_module="langchain_e2b",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
resolve_env_var: EnvResolver | None = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Initialize the provider without touching credentials.
|
|
89
|
+
|
|
90
|
+
Deep Agents Code may instantiate providers only to read metadata during
|
|
91
|
+
discovery, so credentials are resolved lazily by lifecycle methods.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
resolve_env_var: Environment resolver used for provider settings.
|
|
95
|
+
"""
|
|
96
|
+
self._resolve_env_var = resolve_env_var or _default_resolve_env_var
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def metadata(self) -> SandboxProviderMetadata:
|
|
100
|
+
"""Return metadata used by Deep Agents Code provider discovery."""
|
|
101
|
+
return self._metadata
|
|
102
|
+
|
|
103
|
+
def get_or_create(
|
|
104
|
+
self,
|
|
105
|
+
*,
|
|
106
|
+
sandbox_id: str | None = None,
|
|
107
|
+
timeout: int | str | None = None,
|
|
108
|
+
template: str | None = None,
|
|
109
|
+
workdir: str = DEFAULT_WORKDIR,
|
|
110
|
+
command_timeout: int | str | None = None,
|
|
111
|
+
**kwargs: object,
|
|
112
|
+
) -> E2BSandbox:
|
|
113
|
+
"""Get or create an E2B sandbox backend.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
sandbox_id: Existing E2B sandbox ID, or `None` to create one.
|
|
117
|
+
timeout: E2B sandbox lifetime in seconds.
|
|
118
|
+
template: E2B template for new sandboxes.
|
|
119
|
+
workdir: Working directory used by command execution.
|
|
120
|
+
command_timeout: Default command timeout for the backend.
|
|
121
|
+
**kwargs: Unsupported provider options.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
E2B sandbox backend.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
SandboxNotFoundError: If `sandbox_id` does not exist.
|
|
128
|
+
TypeError: If unsupported provider options are passed.
|
|
129
|
+
ValueError: If credentials or timeout settings are invalid.
|
|
130
|
+
"""
|
|
131
|
+
if kwargs:
|
|
132
|
+
_raise_unsupported_kwargs(kwargs)
|
|
133
|
+
|
|
134
|
+
api_key = self._resolve_api_key()
|
|
135
|
+
sandbox_timeout = self._resolve_sandbox_timeout(timeout)
|
|
136
|
+
default_command_timeout = _resolve_int(
|
|
137
|
+
command_timeout,
|
|
138
|
+
name="command_timeout",
|
|
139
|
+
default=DEFAULT_COMMAND_TIMEOUT,
|
|
140
|
+
allow_zero=True,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
if sandbox_id is not None:
|
|
145
|
+
sandbox = e2b.Sandbox.connect(
|
|
146
|
+
sandbox_id,
|
|
147
|
+
timeout=sandbox_timeout,
|
|
148
|
+
api_key=api_key,
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
resolved_template = template or _resolve_optional_env_var(
|
|
152
|
+
self._resolve_env_var,
|
|
153
|
+
"E2B_TEMPLATE",
|
|
154
|
+
)
|
|
155
|
+
if resolved_template:
|
|
156
|
+
sandbox = e2b.Sandbox.create(
|
|
157
|
+
template=resolved_template,
|
|
158
|
+
timeout=sandbox_timeout,
|
|
159
|
+
api_key=api_key,
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
sandbox = e2b.Sandbox.create(
|
|
163
|
+
timeout=sandbox_timeout,
|
|
164
|
+
api_key=api_key,
|
|
165
|
+
)
|
|
166
|
+
except e2b.SandboxNotFoundException as exc:
|
|
167
|
+
if sandbox_id is None:
|
|
168
|
+
raise
|
|
169
|
+
raise SandboxNotFoundError(sandbox_id) from exc
|
|
170
|
+
|
|
171
|
+
return E2BSandbox(
|
|
172
|
+
sandbox=sandbox,
|
|
173
|
+
workdir=workdir,
|
|
174
|
+
timeout=default_command_timeout,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def delete(self, *, sandbox_id: str, **kwargs: object) -> None:
|
|
178
|
+
"""Kill an E2B sandbox.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
sandbox_id: E2B sandbox ID.
|
|
182
|
+
**kwargs: Unsupported provider options.
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
SandboxNotFoundError: If `sandbox_id` does not exist.
|
|
186
|
+
TypeError: If unsupported provider options are passed.
|
|
187
|
+
ValueError: If credentials are missing.
|
|
188
|
+
"""
|
|
189
|
+
if kwargs:
|
|
190
|
+
_raise_unsupported_kwargs(kwargs)
|
|
191
|
+
try:
|
|
192
|
+
e2b.Sandbox.kill(sandbox_id, api_key=self._resolve_api_key())
|
|
193
|
+
except e2b.SandboxNotFoundException as exc:
|
|
194
|
+
raise SandboxNotFoundError(sandbox_id) from exc
|
|
195
|
+
|
|
196
|
+
def _resolve_api_key(self) -> str:
|
|
197
|
+
api_key = _resolve_optional_env_var(self._resolve_env_var, "E2B_API_KEY")
|
|
198
|
+
if not api_key:
|
|
199
|
+
msg = (
|
|
200
|
+
"No E2B API key found. Set E2B_API_KEY or DEEPAGENTS_CODE_E2B_API_KEY."
|
|
201
|
+
)
|
|
202
|
+
raise ValueError(msg)
|
|
203
|
+
return api_key
|
|
204
|
+
|
|
205
|
+
def _resolve_sandbox_timeout(self, timeout: int | str | None) -> int:
|
|
206
|
+
env_timeout = (
|
|
207
|
+
None
|
|
208
|
+
if timeout is not None
|
|
209
|
+
else _resolve_optional_env_var(self._resolve_env_var, "E2B_SANDBOX_TIMEOUT")
|
|
210
|
+
)
|
|
211
|
+
return _resolve_int(
|
|
212
|
+
timeout if timeout is not None else env_timeout,
|
|
213
|
+
name="timeout",
|
|
214
|
+
default=DEFAULT_SANDBOX_TIMEOUT,
|
|
215
|
+
)
|
|
@@ -19,13 +19,16 @@ classifiers = [
|
|
|
19
19
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
20
|
]
|
|
21
21
|
|
|
22
|
-
version = "0.0.
|
|
22
|
+
version = "0.0.4"
|
|
23
23
|
requires-python = ">=3.11,<4.0"
|
|
24
24
|
dependencies = [
|
|
25
25
|
"deepagents>=0.6.0,<0.7.0",
|
|
26
26
|
"e2b>=2.25.1,<3.0.0",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
|
+
[project.entry-points."deepagents_code.sandbox_providers"]
|
|
30
|
+
e2b = "langchain_e2b.provider:E2BProvider"
|
|
31
|
+
|
|
29
32
|
[tool.hatch.build.targets.wheel]
|
|
30
33
|
packages = ["langchain_e2b"]
|
|
31
34
|
|
|
@@ -46,6 +49,7 @@ test = [
|
|
|
46
49
|
"ruff>=0.13.1,<0.16.0",
|
|
47
50
|
"ty>=0.0.1,<1.0.0",
|
|
48
51
|
"langchain-tests>=1.1.6",
|
|
52
|
+
"deepagents-code>=0.1.19,<0.2.0",
|
|
49
53
|
]
|
|
50
54
|
|
|
51
55
|
[tool.uv]
|
|
@@ -11,6 +11,7 @@ from e2b.exceptions import SandboxException
|
|
|
11
11
|
from langchain_tests.integration_tests import SandboxIntegrationTests
|
|
12
12
|
|
|
13
13
|
from langchain_e2b import E2BSandbox
|
|
14
|
+
from langchain_e2b.provider import E2BProvider
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
16
17
|
from collections.abc import Iterator
|
|
@@ -47,6 +48,31 @@ class TestE2BSandboxStandard(SandboxIntegrationTests):
|
|
|
47
48
|
_kill_sandbox(sandbox)
|
|
48
49
|
|
|
49
50
|
|
|
51
|
+
def test_e2b_provider_creates_executes_and_deletes_sandbox() -> None:
|
|
52
|
+
api_key = os.environ.get("E2B_API_KEY")
|
|
53
|
+
if not api_key:
|
|
54
|
+
pytest.skip(
|
|
55
|
+
"Missing secrets for E2B provider integration test: set E2B_API_KEY"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
provider = E2BProvider()
|
|
59
|
+
sandbox_id: str | None = None
|
|
60
|
+
try:
|
|
61
|
+
backend = provider.get_or_create(
|
|
62
|
+
timeout=60 * 60,
|
|
63
|
+
template=os.environ.get("E2B_TEMPLATE"),
|
|
64
|
+
command_timeout=30,
|
|
65
|
+
)
|
|
66
|
+
sandbox_id = backend.id
|
|
67
|
+
result = backend.execute("echo provider-ready", timeout=10)
|
|
68
|
+
|
|
69
|
+
assert result.exit_code == 0
|
|
70
|
+
assert "provider-ready" in result.output
|
|
71
|
+
finally:
|
|
72
|
+
if sandbox_id is not None:
|
|
73
|
+
_delete_provider_sandbox(provider, sandbox_id)
|
|
74
|
+
|
|
75
|
+
|
|
50
76
|
def _kill_sandbox(sandbox: Sandbox) -> None:
|
|
51
77
|
last_error: BaseException | None = None
|
|
52
78
|
for attempt in range(KILL_ATTEMPTS):
|
|
@@ -61,3 +87,19 @@ def _kill_sandbox(sandbox: Sandbox) -> None:
|
|
|
61
87
|
|
|
62
88
|
msg = f"Failed to kill E2B sandbox {sandbox.sandbox_id!r}"
|
|
63
89
|
raise RuntimeError(msg) from last_error
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _delete_provider_sandbox(provider: E2BProvider, sandbox_id: str) -> None:
|
|
93
|
+
last_error: BaseException | None = None
|
|
94
|
+
for attempt in range(KILL_ATTEMPTS):
|
|
95
|
+
try:
|
|
96
|
+
provider.delete(sandbox_id=sandbox_id)
|
|
97
|
+
except (httpx.HTTPError, SandboxException) as exc:
|
|
98
|
+
last_error = exc
|
|
99
|
+
if attempt + 1 < KILL_ATTEMPTS:
|
|
100
|
+
time.sleep(KILL_RETRY_DELAY_SECONDS)
|
|
101
|
+
else:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
msg = f"Failed to delete E2B sandbox {sandbox_id!r}"
|
|
105
|
+
raise RuntimeError(msg) from last_error
|