langchain-e2b 0.0.1__py3-none-any.whl
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/sandbox.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""E2B sandbox backend implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import e2b
|
|
6
|
+
from deepagents.backends.protocol import (
|
|
7
|
+
ExecuteResponse,
|
|
8
|
+
FileDownloadResponse,
|
|
9
|
+
FileUploadResponse,
|
|
10
|
+
)
|
|
11
|
+
from deepagents.backends.sandbox import BaseSandbox
|
|
12
|
+
|
|
13
|
+
DEFAULT_WORKDIR = "/home/user"
|
|
14
|
+
TIMEOUT_EXIT_CODE = 124
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _combine_output(stdout: str | None, stderr: str | None) -> str:
|
|
18
|
+
output = stdout or ""
|
|
19
|
+
if stderr:
|
|
20
|
+
output += "\n" + stderr if output else stderr
|
|
21
|
+
return output
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class E2BSandbox(BaseSandbox):
|
|
25
|
+
"""Sandbox backend that operates on an existing E2B sandbox."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
*,
|
|
30
|
+
sandbox: e2b.Sandbox,
|
|
31
|
+
workdir: str = DEFAULT_WORKDIR,
|
|
32
|
+
timeout: int = 30 * 60,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Create a backend wrapping an existing E2B sandbox.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
sandbox: Existing E2B sandbox instance to wrap.
|
|
38
|
+
workdir: Working directory for command execution.
|
|
39
|
+
timeout: Default command timeout in seconds when `execute()` is
|
|
40
|
+
called without an explicit `timeout`.
|
|
41
|
+
"""
|
|
42
|
+
self._sandbox = sandbox
|
|
43
|
+
self._workdir = workdir
|
|
44
|
+
self._default_timeout = timeout
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def id(self) -> str:
|
|
48
|
+
"""Return the E2B sandbox id."""
|
|
49
|
+
return self._sandbox.sandbox_id
|
|
50
|
+
|
|
51
|
+
def execute(
|
|
52
|
+
self,
|
|
53
|
+
command: str,
|
|
54
|
+
*,
|
|
55
|
+
timeout: int | None = None,
|
|
56
|
+
) -> ExecuteResponse:
|
|
57
|
+
"""Execute a shell command inside the sandbox.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
command: Shell command string to execute.
|
|
61
|
+
timeout: Maximum time in seconds to wait for this command.
|
|
62
|
+
|
|
63
|
+
If None, uses the backend's default timeout.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
ExecuteResponse containing output, exit code, and truncation flag.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If `timeout` is negative.
|
|
70
|
+
"""
|
|
71
|
+
effective_timeout = timeout if timeout is not None else self._default_timeout
|
|
72
|
+
if effective_timeout < 0:
|
|
73
|
+
msg = f"timeout must be non-negative, got {effective_timeout}"
|
|
74
|
+
raise ValueError(msg)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
result = self._sandbox.commands.run(
|
|
78
|
+
command,
|
|
79
|
+
cwd=self._workdir,
|
|
80
|
+
timeout=effective_timeout,
|
|
81
|
+
)
|
|
82
|
+
except e2b.CommandExitException as exc:
|
|
83
|
+
return ExecuteResponse(
|
|
84
|
+
output=_combine_output(exc.stdout, exc.stderr),
|
|
85
|
+
exit_code=exc.exit_code,
|
|
86
|
+
truncated=False,
|
|
87
|
+
)
|
|
88
|
+
except e2b.TimeoutException:
|
|
89
|
+
return ExecuteResponse(
|
|
90
|
+
output=f"Command timed out after {effective_timeout} seconds",
|
|
91
|
+
exit_code=TIMEOUT_EXIT_CODE,
|
|
92
|
+
truncated=False,
|
|
93
|
+
)
|
|
94
|
+
except e2b.SandboxException as exc:
|
|
95
|
+
return ExecuteResponse(
|
|
96
|
+
output=f"Error executing command ({type(exc).__name__}): {exc}",
|
|
97
|
+
exit_code=1,
|
|
98
|
+
truncated=False,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return ExecuteResponse(
|
|
102
|
+
output=_combine_output(result.stdout, result.stderr),
|
|
103
|
+
exit_code=result.exit_code,
|
|
104
|
+
truncated=False,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _read_file(self, path: str) -> FileDownloadResponse:
|
|
108
|
+
if not path.startswith("/"):
|
|
109
|
+
return FileDownloadResponse(path=path, content=None, error="invalid_path")
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
info = self._sandbox.files.get_info(path)
|
|
113
|
+
if info.type == e2b.FileType.DIR:
|
|
114
|
+
return FileDownloadResponse(
|
|
115
|
+
path=path,
|
|
116
|
+
content=None,
|
|
117
|
+
error="is_directory",
|
|
118
|
+
)
|
|
119
|
+
content = bytes(self._sandbox.files.read(path, format="bytes"))
|
|
120
|
+
return FileDownloadResponse(path=path, content=content, error=None)
|
|
121
|
+
except e2b.FileNotFoundException:
|
|
122
|
+
return FileDownloadResponse(path=path, content=None, error="file_not_found")
|
|
123
|
+
except e2b.InvalidArgumentException:
|
|
124
|
+
return FileDownloadResponse(path=path, content=None, error="invalid_path")
|
|
125
|
+
except PermissionError:
|
|
126
|
+
return FileDownloadResponse(
|
|
127
|
+
path=path,
|
|
128
|
+
content=None,
|
|
129
|
+
error="permission_denied",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _write_file(self, path: str, content: bytes) -> FileUploadResponse:
|
|
133
|
+
if not path.startswith("/"):
|
|
134
|
+
return FileUploadResponse(path=path, error="invalid_path")
|
|
135
|
+
|
|
136
|
+
error: str | None = None
|
|
137
|
+
try:
|
|
138
|
+
info = self._sandbox.files.get_info(path)
|
|
139
|
+
if info.type == e2b.FileType.DIR:
|
|
140
|
+
error = "is_directory"
|
|
141
|
+
except e2b.FileNotFoundException:
|
|
142
|
+
pass
|
|
143
|
+
except e2b.InvalidArgumentException:
|
|
144
|
+
error = "invalid_path"
|
|
145
|
+
except PermissionError:
|
|
146
|
+
error = "permission_denied"
|
|
147
|
+
|
|
148
|
+
if error is None:
|
|
149
|
+
try:
|
|
150
|
+
self._sandbox.files.write(path, content)
|
|
151
|
+
except e2b.FileNotFoundException:
|
|
152
|
+
error = "file_not_found"
|
|
153
|
+
except e2b.InvalidArgumentException:
|
|
154
|
+
error = "invalid_path"
|
|
155
|
+
except PermissionError:
|
|
156
|
+
error = "permission_denied"
|
|
157
|
+
|
|
158
|
+
return FileUploadResponse(path=path, error=error)
|
|
159
|
+
|
|
160
|
+
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
|
161
|
+
"""Download files from the sandbox.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
paths: Absolute sandbox file paths to download.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Download responses in the same order as `paths`.
|
|
168
|
+
"""
|
|
169
|
+
return [self._read_file(path) for path in paths]
|
|
170
|
+
|
|
171
|
+
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
|
|
172
|
+
"""Upload files into the sandbox.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
files: `(path, content)` pairs to write.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Upload responses in the same order as `files`.
|
|
179
|
+
"""
|
|
180
|
+
return [self._write_file(path, content) for path, content in files]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-e2b
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: E2B sandbox integration for Deep Agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/e2b-dev/langchain-e2b
|
|
6
|
+
Project-URL: Repository, https://github.com/e2b-dev/langchain-e2b
|
|
7
|
+
Project-URL: Documentation, https://github.com/e2b-dev/langchain-e2b#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/e2b-dev/langchain-e2b/issues
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: <4.0,>=3.11
|
|
20
|
+
Requires-Dist: deepagents<0.7.0,>=0.6.0
|
|
21
|
+
Requires-Dist: e2b<3.0.0,>=2.25.1
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# langchain-e2b
|
|
25
|
+
|
|
26
|
+
[](https://pypi.org/project/langchain-e2b/#history)
|
|
27
|
+
[](https://opensource.org/licenses/MIT)
|
|
28
|
+
[](https://pypistats.org/packages/langchain-e2b)
|
|
29
|
+
|
|
30
|
+
## Quick Install
|
|
31
|
+
|
|
32
|
+
After the package is published:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install langchain-e2b
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from e2b import Sandbox
|
|
40
|
+
|
|
41
|
+
from langchain_e2b import E2BSandbox
|
|
42
|
+
|
|
43
|
+
sandbox = Sandbox.create()
|
|
44
|
+
backend = E2BSandbox(sandbox=sandbox)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
result = backend.execute("echo hello")
|
|
48
|
+
print(result.output)
|
|
49
|
+
finally:
|
|
50
|
+
sandbox.kill()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## What is this?
|
|
54
|
+
|
|
55
|
+
`langchain-e2b` adapts an existing E2B sandbox to the Deep Agents sandbox
|
|
56
|
+
protocol. It uses the low-level `e2b` SDK so Deep Agents can run shell commands
|
|
57
|
+
and move files through the standard Deep Agents backend interface.
|
|
58
|
+
|
|
59
|
+
This package intentionally does not hide E2B sandbox lifecycle management. Use
|
|
60
|
+
the E2B SDK to create, connect to, configure, and kill sandboxes, then pass the
|
|
61
|
+
connected sandbox object to `E2BSandbox`.
|
|
62
|
+
|
|
63
|
+
## Contributing
|
|
64
|
+
|
|
65
|
+
Contributions are welcome. Keep the adapter focused on implementing the Deep
|
|
66
|
+
Agents sandbox backend protocol over the official E2B SDK.
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv sync --group test
|
|
72
|
+
make test
|
|
73
|
+
make lint
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Integration tests require `E2B_API_KEY`:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
E2B_API_KEY=... make integration_tests
|
|
80
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
langchain_e2b/__init__.py,sha256=aBCl_SstOPDdsgMiJc7Zakw6AFxPkdV03eRBQIsIP2U,119
|
|
2
|
+
langchain_e2b/_version.py,sha256=ppf0d_xCU41RiniqB_Ksx2oWJ-Bbiglg-75x4BdUPoY,249
|
|
3
|
+
langchain_e2b/sandbox.py,sha256=kQ2QAFyd9hnmmgxftPAqElUVujKJayil-It8IPXJoHg,5972
|
|
4
|
+
langchain_e2b-0.0.1.dist-info/METADATA,sha256=qcK_KLHiJBd3ABb0EPr95p848ud-v1olFYT1gW35or8,2430
|
|
5
|
+
langchain_e2b-0.0.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
6
|
+
langchain_e2b-0.0.1.dist-info/licenses/LICENSE,sha256=TsZ-TKbmch26hJssqCJhWXyGph7iFLvyFBYAa3stBHg,1067
|
|
7
|
+
langchain_e2b-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) LangChain, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|