agno-opensandbox-toolkit 0.1.0__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.
- agno_opensandbox_toolkit-0.1.0/PKG-INFO +111 -0
- agno_opensandbox_toolkit-0.1.0/README.md +87 -0
- agno_opensandbox_toolkit-0.1.0/pyproject.toml +69 -0
- agno_opensandbox_toolkit-0.1.0/src/agno_opensandbox_toolkit/__init__.py +3 -0
- agno_opensandbox_toolkit-0.1.0/src/agno_opensandbox_toolkit/_constants.py +38 -0
- agno_opensandbox_toolkit-0.1.0/src/agno_opensandbox_toolkit/py.typed +0 -0
- agno_opensandbox_toolkit-0.1.0/src/agno_opensandbox_toolkit/tools.py +556 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: agno-opensandbox-toolkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenSandbox support for the Agno agent framework
|
|
5
|
+
Keywords: agno,opensandbox,sandbox,ai-agent,toolkit
|
|
6
|
+
Author: Valeryi Savich
|
|
7
|
+
Author-email: Valeryi Savich <relrin78@gmail.com>
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Dist: agno==2.5.16
|
|
19
|
+
Requires-Dist: opensandbox==0.1.7
|
|
20
|
+
Requires-Dist: opensandbox-code-interpreter==0.1.2 ; extra == 'code-interpreter'
|
|
21
|
+
Requires-Python: >=3.10, <4
|
|
22
|
+
Provides-Extra: code-interpreter
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# agno-opensandbox-toolkit
|
|
26
|
+
|
|
27
|
+
[](https://github.com/Relrin/agno-opensandbox-toolkit/actions/workflows/ci.yml)
|
|
28
|
+
[](https://pypi.org/project/agno-opensandbox-toolkit/)
|
|
29
|
+
[](https://pypi.org/project/agno-opensandbox-toolkit/)
|
|
30
|
+
[](https://opensource.org/licenses/BSD-3-Clause)
|
|
31
|
+
|
|
32
|
+
[OpenSandbox](https://github.com/alibaba/OpenSandbox) support for the
|
|
33
|
+
[Agno](https://github.com/agno-agi/agno) agent framework.
|
|
34
|
+
|
|
35
|
+
Give your Agno agents the ability to execute shell commands, manage files, and run code
|
|
36
|
+
in secure, isolated OpenSandbox containers.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Base: shell commands + file operations
|
|
42
|
+
uv add agno-opensandbox-toolkit
|
|
43
|
+
|
|
44
|
+
# With code interpreter support (Python, JS, Java, Go, Bash)
|
|
45
|
+
uv add "agno-opensandbox-toolkit[code-interpreter]"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Prerequisites
|
|
49
|
+
|
|
50
|
+
You need a running OpenSandbox server. The quickest way:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv pip install opensandbox-server
|
|
54
|
+
opensandbox-server init-config ~/.sandbox.toml --example docker
|
|
55
|
+
opensandbox-server
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
See the [OpenSandbox docs](https://github.com/alibaba/OpenSandbox/blob/main/server/README.md)
|
|
59
|
+
for full setup instructions.
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from agno.agent import Agent
|
|
65
|
+
from agno.models.openai import OpenAIChat
|
|
66
|
+
from agno_opensandbox_toolkit import OpenSandboxTools
|
|
67
|
+
|
|
68
|
+
agent = Agent(
|
|
69
|
+
name="Sandbox Agent",
|
|
70
|
+
model=OpenAIChat(id="gpt-4o-mini"),
|
|
71
|
+
tools=[OpenSandboxTools()],
|
|
72
|
+
markdown=True,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
agent.print_response("Create a file /tmp/hello.py that prints 'Hello from OpenSandbox!', then run it")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
| Parameter | Env Variable | Default | Description |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| `domain` | `OPENSANDBOX_DOMAIN` | `localhost:8080` | OpenSandbox server address |
|
|
83
|
+
| `api_key` | `OPENSANDBOX_API_KEY` | `""` | API key for authentication |
|
|
84
|
+
| `protocol` | — | `http` | Connection protocol (`http` or `https`) |
|
|
85
|
+
| `image` | — | `ubuntu` | Container image |
|
|
86
|
+
| `timeout_minutes` | — | `10` | Sandbox TTL |
|
|
87
|
+
| `sandbox_env` | — | `{}` | Env vars inside sandbox |
|
|
88
|
+
| `sandbox_resources` | — | `None` | e.g. `{"cpu": "2", "memory": "4Gi"}` |
|
|
89
|
+
| `sandbox_entrypoint` | — | `None` | Custom entrypoint command list |
|
|
90
|
+
| `sandbox_metadata` | — | `None` | Metadata labels dict |
|
|
91
|
+
| `enable_code_interpreter` | — | `False` | Enable `run_code` tool |
|
|
92
|
+
| `persistent` | — | `True` | Reuse sandbox across calls |
|
|
93
|
+
| `sandbox_id` | — | `None` | Reconnect to existing sandbox |
|
|
94
|
+
|
|
95
|
+
## Available Tools
|
|
96
|
+
|
|
97
|
+
| Tool | Description |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `run_shell_command` | Execute bash commands |
|
|
100
|
+
| `create_file` | Create/update files |
|
|
101
|
+
| `read_file` | Read file content |
|
|
102
|
+
| `list_files` | List directory contents |
|
|
103
|
+
| `delete_file` | Delete files/directories |
|
|
104
|
+
| `change_directory` | Change working directory |
|
|
105
|
+
| `get_sandbox_status` | Get sandbox info |
|
|
106
|
+
| `shutdown_sandbox` | Kill the sandbox |
|
|
107
|
+
| `run_code` | Execute code via interpreter (optional) |
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
The project is published under the BSD 3-Clause license. For details see the [LICENSE](https://github.com/Relrin/agno-opensandbox-toolkit/blob/master/LICENSE) file.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# agno-opensandbox-toolkit
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Relrin/agno-opensandbox-toolkit/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/agno-opensandbox-toolkit/)
|
|
5
|
+
[](https://pypi.org/project/agno-opensandbox-toolkit/)
|
|
6
|
+
[](https://opensource.org/licenses/BSD-3-Clause)
|
|
7
|
+
|
|
8
|
+
[OpenSandbox](https://github.com/alibaba/OpenSandbox) support for the
|
|
9
|
+
[Agno](https://github.com/agno-agi/agno) agent framework.
|
|
10
|
+
|
|
11
|
+
Give your Agno agents the ability to execute shell commands, manage files, and run code
|
|
12
|
+
in secure, isolated OpenSandbox containers.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Base: shell commands + file operations
|
|
18
|
+
uv add agno-opensandbox-toolkit
|
|
19
|
+
|
|
20
|
+
# With code interpreter support (Python, JS, Java, Go, Bash)
|
|
21
|
+
uv add "agno-opensandbox-toolkit[code-interpreter]"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
You need a running OpenSandbox server. The quickest way:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv pip install opensandbox-server
|
|
30
|
+
opensandbox-server init-config ~/.sandbox.toml --example docker
|
|
31
|
+
opensandbox-server
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
See the [OpenSandbox docs](https://github.com/alibaba/OpenSandbox/blob/main/server/README.md)
|
|
35
|
+
for full setup instructions.
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from agno.agent import Agent
|
|
41
|
+
from agno.models.openai import OpenAIChat
|
|
42
|
+
from agno_opensandbox_toolkit import OpenSandboxTools
|
|
43
|
+
|
|
44
|
+
agent = Agent(
|
|
45
|
+
name="Sandbox Agent",
|
|
46
|
+
model=OpenAIChat(id="gpt-4o-mini"),
|
|
47
|
+
tools=[OpenSandboxTools()],
|
|
48
|
+
markdown=True,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
agent.print_response("Create a file /tmp/hello.py that prints 'Hello from OpenSandbox!', then run it")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
| Parameter | Env Variable | Default | Description |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| `domain` | `OPENSANDBOX_DOMAIN` | `localhost:8080` | OpenSandbox server address |
|
|
59
|
+
| `api_key` | `OPENSANDBOX_API_KEY` | `""` | API key for authentication |
|
|
60
|
+
| `protocol` | — | `http` | Connection protocol (`http` or `https`) |
|
|
61
|
+
| `image` | — | `ubuntu` | Container image |
|
|
62
|
+
| `timeout_minutes` | — | `10` | Sandbox TTL |
|
|
63
|
+
| `sandbox_env` | — | `{}` | Env vars inside sandbox |
|
|
64
|
+
| `sandbox_resources` | — | `None` | e.g. `{"cpu": "2", "memory": "4Gi"}` |
|
|
65
|
+
| `sandbox_entrypoint` | — | `None` | Custom entrypoint command list |
|
|
66
|
+
| `sandbox_metadata` | — | `None` | Metadata labels dict |
|
|
67
|
+
| `enable_code_interpreter` | — | `False` | Enable `run_code` tool |
|
|
68
|
+
| `persistent` | — | `True` | Reuse sandbox across calls |
|
|
69
|
+
| `sandbox_id` | — | `None` | Reconnect to existing sandbox |
|
|
70
|
+
|
|
71
|
+
## Available Tools
|
|
72
|
+
|
|
73
|
+
| Tool | Description |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `run_shell_command` | Execute bash commands |
|
|
76
|
+
| `create_file` | Create/update files |
|
|
77
|
+
| `read_file` | Read file content |
|
|
78
|
+
| `list_files` | List directory contents |
|
|
79
|
+
| `delete_file` | Delete files/directories |
|
|
80
|
+
| `change_directory` | Change working directory |
|
|
81
|
+
| `get_sandbox_status` | Get sandbox info |
|
|
82
|
+
| `shutdown_sandbox` | Kill the sandbox |
|
|
83
|
+
| `run_code` | Execute code via interpreter (optional) |
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
The project is published under the BSD 3-Clause license. For details see the [LICENSE](https://github.com/Relrin/agno-opensandbox-toolkit/blob/master/LICENSE) file.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agno-opensandbox-toolkit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "OpenSandbox support for the Agno agent framework"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Valeryi Savich", email = "relrin78@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10,<4"
|
|
10
|
+
keywords = ["agno", "opensandbox", "sandbox", "ai-agent", "toolkit"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: BSD License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Topic :: Software Development :: Libraries",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"agno==2.5.16",
|
|
25
|
+
"opensandbox==0.1.7",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
code-interpreter = [
|
|
30
|
+
"opensandbox-code-interpreter==0.1.2",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["uv_build>=0.10.12,<0.11.0"]
|
|
35
|
+
build-backend = "uv_build"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"mypy==1.20.0",
|
|
40
|
+
"pytest===9.0.3",
|
|
41
|
+
"pytest-asyncio==1.3.0",
|
|
42
|
+
"pytest-cov==7.1.0",
|
|
43
|
+
"respx==0.23.1",
|
|
44
|
+
"ruff==0.15.10",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
asyncio_mode = "auto"
|
|
49
|
+
testpaths = ["tests"]
|
|
50
|
+
markers = [
|
|
51
|
+
"integration: requires a running OpenSandbox server (deselect with '-m not integration')",
|
|
52
|
+
"e2e: end-to-end tests requiring LLM API key + OpenSandbox server",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
target-version = "py310"
|
|
57
|
+
line-length = 120
|
|
58
|
+
src = ["src", "tests"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
62
|
+
|
|
63
|
+
[tool.mypy]
|
|
64
|
+
python_version = "3.10"
|
|
65
|
+
strict = true
|
|
66
|
+
warn_return_any = true
|
|
67
|
+
warn_unused_configs = true
|
|
68
|
+
packages = ["agno_opensandbox_toolkit"]
|
|
69
|
+
mypy_path = "src"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from textwrap import dedent
|
|
2
|
+
|
|
3
|
+
DEFAULT_IMAGE = "ubuntu"
|
|
4
|
+
CODE_INTERPRETER_IMAGE = "opensandbox/code-interpreter:v1.0.2"
|
|
5
|
+
CODE_INTERPRETER_ENTRYPOINT = ["/opt/opensandbox/code-interpreter.sh"]
|
|
6
|
+
DEFAULT_WORKING_DIRECTORY = "/root"
|
|
7
|
+
|
|
8
|
+
DEFAULT_INSTRUCTIONS = dedent(
|
|
9
|
+
"""\
|
|
10
|
+
You have access to a persistent OpenSandbox sandbox for code execution.
|
|
11
|
+
The sandbox maintains state across interactions. And our goal is to provide working, executed code examples.
|
|
12
|
+
|
|
13
|
+
Available tools:
|
|
14
|
+
- `run_code`: Execute code in the sandbox (requires code-interpreter image)
|
|
15
|
+
- `run_shell_command`: Execute shell commands (bash)
|
|
16
|
+
- `create_file`: Create or update files
|
|
17
|
+
- `read_file`: Read file contents
|
|
18
|
+
- `list_files`: List directory contents
|
|
19
|
+
- `delete_file`: Delete files or directories
|
|
20
|
+
- `change_directory`: Change the working directory
|
|
21
|
+
- `get_sandbox_status`: Get the sandbox status
|
|
22
|
+
- `shutdown_sandbox`: Shut down the sandbox
|
|
23
|
+
|
|
24
|
+
When users ask for code (Python, JavaScript, TypeScript, etc.), you should:
|
|
25
|
+
1. Write the code
|
|
26
|
+
2. Execute it using run_code tool
|
|
27
|
+
3. Show the actual output/results
|
|
28
|
+
4. Never just provide code without executing it
|
|
29
|
+
|
|
30
|
+
Recommended workflow:
|
|
31
|
+
1. Before running any code, check if required packages for the target languages are installed
|
|
32
|
+
2. Install missing packages, e.g. run_shell_command("pip install package1 package2")
|
|
33
|
+
3. When running scripts, capture both output AND errors
|
|
34
|
+
4. If a script produces no output, check for errors or add print statements
|
|
35
|
+
|
|
36
|
+
IMPORTANT: Always use single quotes for the content parameter when creating files
|
|
37
|
+
"""
|
|
38
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import stat
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from os import getenv
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agno.agent import Agent
|
|
9
|
+
from agno.team import Team
|
|
10
|
+
from agno.tools import Toolkit
|
|
11
|
+
from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
12
|
+
|
|
13
|
+
from agno_opensandbox_toolkit._constants import (
|
|
14
|
+
CODE_INTERPRETER_ENTRYPOINT,
|
|
15
|
+
CODE_INTERPRETER_IMAGE,
|
|
16
|
+
DEFAULT_IMAGE,
|
|
17
|
+
DEFAULT_INSTRUCTIONS,
|
|
18
|
+
DEFAULT_WORKING_DIRECTORY,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from opensandbox import SandboxSync
|
|
23
|
+
from opensandbox.config import ConnectionConfigSync
|
|
24
|
+
from opensandbox.models.execd import Execution, RunCommandOpts
|
|
25
|
+
except ImportError as _err:
|
|
26
|
+
raise ImportError("`opensandbox` not installed. Please install using `pip install opensandbox`") from _err
|
|
27
|
+
|
|
28
|
+
_CODE_INTERPRETER_AVAILABLE = False
|
|
29
|
+
try:
|
|
30
|
+
from code_interpreter import CodeInterpreterSync
|
|
31
|
+
from code_interpreter.models.code import SupportedLanguage
|
|
32
|
+
|
|
33
|
+
_CODE_INTERPRETER_AVAILABLE = True
|
|
34
|
+
except ImportError:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OpenSandboxTools(Toolkit):
|
|
39
|
+
"""
|
|
40
|
+
OpenSandbox integration toolkit for Agno agents.
|
|
41
|
+
|
|
42
|
+
Provides sandboxed code execution, shell commands, and file operations
|
|
43
|
+
via the OpenSandbox platform. Follows the same patterns as the built-in
|
|
44
|
+
DaytonaTools and E2BTools integrations.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
domain: str | None = None,
|
|
50
|
+
api_key: str | None = None,
|
|
51
|
+
protocol: str = "http",
|
|
52
|
+
image: str | None = None,
|
|
53
|
+
timeout_minutes: int = 10,
|
|
54
|
+
sandbox_env: dict[str, str] | None = None,
|
|
55
|
+
sandbox_resources: dict[str, str] | None = None,
|
|
56
|
+
sandbox_metadata: dict[str, str] | None = None,
|
|
57
|
+
sandbox_entrypoint: list[str] | None = None,
|
|
58
|
+
sandbox_id: str | None = None,
|
|
59
|
+
persistent: bool = True,
|
|
60
|
+
enable_code_interpreter: bool = False,
|
|
61
|
+
instructions: str | None = None,
|
|
62
|
+
add_instructions: bool = False,
|
|
63
|
+
**kwargs: Any,
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Initialize OpenSandbox toolkit for Agno agents.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
domain: OpenSandbox server domain (default: env OPENSANDBOX_DOMAIN or "localhost:8080").
|
|
70
|
+
api_key: API key (default: env OPENSANDBOX_API_KEY or "").
|
|
71
|
+
protocol: Connection protocol, "http" or "https" (default: "http").
|
|
72
|
+
image: Container image for the sandbox (default: "ubuntu",
|
|
73
|
+
auto-set to code-interpreter image when enable_code_interpreter=True).
|
|
74
|
+
timeout_minutes: Sandbox TTL in minutes.
|
|
75
|
+
sandbox_env: Environment variables to set inside the sandbox.
|
|
76
|
+
sandbox_resources: Resource limits e.g. {"cpu": "2", "memory": "4Gi"}.
|
|
77
|
+
sandbox_metadata: User metadata labels for the sandbox.
|
|
78
|
+
sandbox_entrypoint: Custom entrypoint command list.
|
|
79
|
+
sandbox_id: Existing sandbox ID to reconnect to (skips creation).
|
|
80
|
+
persistent: If True, store sandbox ID in agent.session_state for reuse.
|
|
81
|
+
enable_code_interpreter: If True, adds `run_code` tool (requires
|
|
82
|
+
`opensandbox-code-interpreter` package).
|
|
83
|
+
instructions: Custom instructions string (replaces default).
|
|
84
|
+
add_instructions: If True, prepend instructions to agent system prompt.
|
|
85
|
+
"""
|
|
86
|
+
self.domain = domain or getenv("OPENSANDBOX_DOMAIN", "localhost:8080")
|
|
87
|
+
self.api_key = api_key or getenv("OPENSANDBOX_API_KEY", "")
|
|
88
|
+
|
|
89
|
+
self.sandbox_id = sandbox_id
|
|
90
|
+
self.persistent = persistent
|
|
91
|
+
self.timeout_minutes = timeout_minutes
|
|
92
|
+
self.sandbox_env = sandbox_env or {}
|
|
93
|
+
self.sandbox_resources = sandbox_resources
|
|
94
|
+
self.sandbox_metadata = sandbox_metadata or {}
|
|
95
|
+
self.sandbox_entrypoint = sandbox_entrypoint
|
|
96
|
+
self.enable_code_interpreter = enable_code_interpreter
|
|
97
|
+
|
|
98
|
+
if enable_code_interpreter:
|
|
99
|
+
self.image = image or CODE_INTERPRETER_IMAGE
|
|
100
|
+
if self.sandbox_entrypoint is None:
|
|
101
|
+
self.sandbox_entrypoint = CODE_INTERPRETER_ENTRYPOINT
|
|
102
|
+
else:
|
|
103
|
+
self.image = image or DEFAULT_IMAGE
|
|
104
|
+
|
|
105
|
+
if enable_code_interpreter and not _CODE_INTERPRETER_AVAILABLE:
|
|
106
|
+
raise ImportError(
|
|
107
|
+
"`opensandbox-code-interpreter` not installed. "
|
|
108
|
+
"Please install using `pip install opensandbox-code-interpreter`"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self._sandbox: SandboxSync | None = None
|
|
112
|
+
self._interpreter: Any | None = None
|
|
113
|
+
|
|
114
|
+
self._connection_config = ConnectionConfigSync(
|
|
115
|
+
domain=self.domain,
|
|
116
|
+
api_key=self.api_key,
|
|
117
|
+
protocol=protocol,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
_instructions = instructions or DEFAULT_INSTRUCTIONS
|
|
121
|
+
|
|
122
|
+
tools: list[Any] = [
|
|
123
|
+
self.run_shell_command,
|
|
124
|
+
self.create_file,
|
|
125
|
+
self.read_file,
|
|
126
|
+
self.list_files,
|
|
127
|
+
self.delete_file,
|
|
128
|
+
self.change_directory,
|
|
129
|
+
self.get_sandbox_status,
|
|
130
|
+
self.shutdown_sandbox,
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
if enable_code_interpreter:
|
|
134
|
+
tools.append(self.run_code)
|
|
135
|
+
|
|
136
|
+
super().__init__(
|
|
137
|
+
name="opensandbox_tools",
|
|
138
|
+
tools=tools,
|
|
139
|
+
instructions=_instructions,
|
|
140
|
+
add_instructions=add_instructions,
|
|
141
|
+
**kwargs,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _get_working_directory(self, agent: Agent | Team) -> str:
|
|
145
|
+
"""Get the current working directory from agent session state."""
|
|
146
|
+
if agent and hasattr(agent, "session_state"):
|
|
147
|
+
if agent.session_state is None:
|
|
148
|
+
agent.session_state = {}
|
|
149
|
+
return str(agent.session_state.get("working_directory", DEFAULT_WORKING_DIRECTORY))
|
|
150
|
+
return DEFAULT_WORKING_DIRECTORY
|
|
151
|
+
|
|
152
|
+
def _set_working_directory(self, agent: Agent | Team, directory: str) -> None:
|
|
153
|
+
"""Set the working directory in agent session state."""
|
|
154
|
+
if agent and hasattr(agent, "session_state"):
|
|
155
|
+
if agent.session_state is None:
|
|
156
|
+
agent.session_state = {}
|
|
157
|
+
agent.session_state["working_directory"] = directory
|
|
158
|
+
log_info(f"Updated working directory to: {directory}")
|
|
159
|
+
|
|
160
|
+
def _get_or_create_sandbox(self, agent: Agent | Team) -> SandboxSync:
|
|
161
|
+
"""Get existing sandbox or create new one. Stores ID in agent session_state."""
|
|
162
|
+
try:
|
|
163
|
+
sandbox = None
|
|
164
|
+
|
|
165
|
+
# Reuse cached sandbox instance
|
|
166
|
+
if self._sandbox is not None:
|
|
167
|
+
return self._sandbox
|
|
168
|
+
|
|
169
|
+
# Use explicit sandbox_id from constructor
|
|
170
|
+
if self.sandbox_id:
|
|
171
|
+
try:
|
|
172
|
+
sandbox = SandboxSync.connect(
|
|
173
|
+
self.sandbox_id,
|
|
174
|
+
connection_config=self._connection_config,
|
|
175
|
+
)
|
|
176
|
+
log_debug(f"Connected to explicit sandbox: {self.sandbox_id}")
|
|
177
|
+
except Exception as e:
|
|
178
|
+
log_debug(f"Failed to connect to sandbox {self.sandbox_id}: {e}")
|
|
179
|
+
sandbox = None
|
|
180
|
+
|
|
181
|
+
# Use persistent sandbox from session state
|
|
182
|
+
elif self.persistent and hasattr(agent, "session_state"):
|
|
183
|
+
if agent.session_state is None:
|
|
184
|
+
agent.session_state = {}
|
|
185
|
+
|
|
186
|
+
saved_id = agent.session_state.get("opensandbox_id")
|
|
187
|
+
if saved_id:
|
|
188
|
+
try:
|
|
189
|
+
sandbox = SandboxSync.connect(
|
|
190
|
+
saved_id,
|
|
191
|
+
connection_config=self._connection_config,
|
|
192
|
+
)
|
|
193
|
+
log_debug(f"Reconnected to persistent sandbox: {saved_id}")
|
|
194
|
+
except Exception as e:
|
|
195
|
+
log_debug(f"Failed to reconnect to sandbox {saved_id}: {e}")
|
|
196
|
+
sandbox = None
|
|
197
|
+
|
|
198
|
+
# Create new sandbox
|
|
199
|
+
if sandbox is None:
|
|
200
|
+
sandbox = self._create_new_sandbox(agent)
|
|
201
|
+
|
|
202
|
+
self._sandbox = sandbox
|
|
203
|
+
return sandbox
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
log_warning(f"Error in sandbox management, creating new sandbox: {e}") # type: ignore[no-untyped-call]
|
|
207
|
+
sandbox = self._create_new_sandbox(agent)
|
|
208
|
+
self._sandbox = sandbox
|
|
209
|
+
return sandbox
|
|
210
|
+
|
|
211
|
+
def _create_new_sandbox(self, agent: Agent | Team | None = None) -> SandboxSync:
|
|
212
|
+
"""Create a new OpenSandbox instance."""
|
|
213
|
+
try:
|
|
214
|
+
metadata = self.sandbox_metadata.copy()
|
|
215
|
+
metadata.setdefault("created_by", "agno_opensandbox_toolkit")
|
|
216
|
+
|
|
217
|
+
create_kwargs: dict[str, Any] = {
|
|
218
|
+
"connection_config": self._connection_config,
|
|
219
|
+
"timeout": timedelta(minutes=self.timeout_minutes),
|
|
220
|
+
"env": self.sandbox_env,
|
|
221
|
+
"metadata": metadata,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if self.sandbox_resources:
|
|
225
|
+
create_kwargs["resource"] = self.sandbox_resources
|
|
226
|
+
|
|
227
|
+
if self.sandbox_entrypoint:
|
|
228
|
+
create_kwargs["entrypoint"] = self.sandbox_entrypoint
|
|
229
|
+
|
|
230
|
+
sandbox = SandboxSync.create(self.image, **create_kwargs)
|
|
231
|
+
|
|
232
|
+
# Store sandbox ID for persistence
|
|
233
|
+
if self.persistent and agent and hasattr(agent, "session_state"):
|
|
234
|
+
if agent.session_state is None:
|
|
235
|
+
agent.session_state = {}
|
|
236
|
+
agent.session_state["opensandbox_id"] = sandbox.id
|
|
237
|
+
|
|
238
|
+
log_info(f"Created new OpenSandbox: {sandbox.id}")
|
|
239
|
+
return sandbox
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
log_error(f"Error creating OpenSandbox: {e}") # type: ignore[no-untyped-call]
|
|
243
|
+
raise
|
|
244
|
+
|
|
245
|
+
def _get_or_create_interpreter(self, sandbox: SandboxSync) -> Any:
|
|
246
|
+
"""Lazily create the code interpreter."""
|
|
247
|
+
if self._interpreter is not None:
|
|
248
|
+
return self._interpreter
|
|
249
|
+
self._interpreter = CodeInterpreterSync.create(sandbox=sandbox)
|
|
250
|
+
log_info("Created CodeInterpreter for sandbox")
|
|
251
|
+
return self._interpreter
|
|
252
|
+
|
|
253
|
+
def _format_execution(self, execution: Execution) -> str:
|
|
254
|
+
"""Format an Execution result into a readable string."""
|
|
255
|
+
parts: list[str] = []
|
|
256
|
+
|
|
257
|
+
# Combined stdout + result text via SDK property
|
|
258
|
+
text = execution.text
|
|
259
|
+
if text:
|
|
260
|
+
parts.append(text)
|
|
261
|
+
|
|
262
|
+
# Stderr (not included in .text)
|
|
263
|
+
if execution.logs and execution.logs.stderr:
|
|
264
|
+
stderr_lines = [msg.text for msg in execution.logs.stderr]
|
|
265
|
+
stderr_text = "\n".join(stderr_lines)
|
|
266
|
+
if stderr_text.strip():
|
|
267
|
+
parts.append(f"STDERR:\n{stderr_text}")
|
|
268
|
+
|
|
269
|
+
# Error info
|
|
270
|
+
if execution.error:
|
|
271
|
+
parts.append(f"ERROR: {execution.error}")
|
|
272
|
+
|
|
273
|
+
return "\n".join(parts) if parts else "Command executed successfully with no output."
|
|
274
|
+
|
|
275
|
+
def _resolve_path(self, file_path: str, cwd: str) -> str:
|
|
276
|
+
"""
|
|
277
|
+
Resolve a file path relative to the current working directory.
|
|
278
|
+
|
|
279
|
+
Uses PurePosixPath since the sandbox is always Linux.
|
|
280
|
+
"""
|
|
281
|
+
path = PurePosixPath(file_path)
|
|
282
|
+
if path.is_absolute():
|
|
283
|
+
return str(path)
|
|
284
|
+
return str(PurePosixPath(cwd) / path)
|
|
285
|
+
|
|
286
|
+
def run_code(self, agent: Agent | Team, code: str, language: str = "python") -> str:
|
|
287
|
+
"""
|
|
288
|
+
Execute code in the OpenSandbox code interpreter.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
agent: An Agno agent instance or a group of agents
|
|
292
|
+
code: The source code to execute.
|
|
293
|
+
language: Programming language — "python", "javascript", "typescript",
|
|
294
|
+
"java", "go", or "bash" (default: "python").
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Execution output as a string, or error details.
|
|
298
|
+
"""
|
|
299
|
+
try:
|
|
300
|
+
from agno.utils.code_execution import prepare_python_code
|
|
301
|
+
|
|
302
|
+
current_sandbox = self._get_or_create_sandbox(agent)
|
|
303
|
+
interpreter = self._get_or_create_interpreter(current_sandbox)
|
|
304
|
+
|
|
305
|
+
# Map string language to enum
|
|
306
|
+
lang_map = {
|
|
307
|
+
"python": SupportedLanguage.PYTHON,
|
|
308
|
+
"javascript": SupportedLanguage.JAVASCRIPT,
|
|
309
|
+
"typescript": SupportedLanguage.TYPESCRIPT,
|
|
310
|
+
"java": SupportedLanguage.JAVA,
|
|
311
|
+
"go": SupportedLanguage.GO,
|
|
312
|
+
"bash": SupportedLanguage.BASH,
|
|
313
|
+
}
|
|
314
|
+
lang_enum = lang_map.get(language.lower(), SupportedLanguage.PYTHON)
|
|
315
|
+
|
|
316
|
+
if lang_enum == SupportedLanguage.PYTHON:
|
|
317
|
+
code = prepare_python_code(code)
|
|
318
|
+
|
|
319
|
+
result = interpreter.codes.run(code, language=lang_enum)
|
|
320
|
+
return self._format_execution(result)
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
return json.dumps({"status": "error", "message": f"Error executing code: {e!s}"})
|
|
324
|
+
|
|
325
|
+
def run_shell_command(self, agent: Agent | Team, command: str) -> str:
|
|
326
|
+
"""
|
|
327
|
+
Execute a shell command in the OpenSandbox environment.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
agent: An Agno agent instance or a group of agents
|
|
331
|
+
command: Shell command to execute (e.g., "ls -la /tmp").
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Command output as a string.
|
|
335
|
+
"""
|
|
336
|
+
try:
|
|
337
|
+
current_sandbox = self._get_or_create_sandbox(agent)
|
|
338
|
+
cwd = self._get_working_directory(agent)
|
|
339
|
+
|
|
340
|
+
if command.strip().startswith("cd "):
|
|
341
|
+
new_dir = command.strip()[3:].strip()
|
|
342
|
+
new_path = PurePosixPath(new_dir)
|
|
343
|
+
|
|
344
|
+
if not new_path.is_absolute():
|
|
345
|
+
new_path = PurePosixPath(cwd) / new_path
|
|
346
|
+
|
|
347
|
+
new_path_str = str(new_path)
|
|
348
|
+
|
|
349
|
+
test_result = current_sandbox.commands.run(
|
|
350
|
+
f"test -d '{new_path_str}' && echo 'exists' || echo 'not found'",
|
|
351
|
+
opts=RunCommandOpts(working_directory="/"),
|
|
352
|
+
)
|
|
353
|
+
stdout_text = test_result.text
|
|
354
|
+
if "exists" in stdout_text:
|
|
355
|
+
self._set_working_directory(agent, new_path_str)
|
|
356
|
+
return f"Changed directory to: {new_path_str}"
|
|
357
|
+
else:
|
|
358
|
+
return f"Error: Directory {new_path_str} not found"
|
|
359
|
+
|
|
360
|
+
execution = current_sandbox.commands.run(
|
|
361
|
+
command,
|
|
362
|
+
opts=RunCommandOpts(working_directory=cwd),
|
|
363
|
+
)
|
|
364
|
+
return self._format_execution(execution)
|
|
365
|
+
|
|
366
|
+
except Exception as e:
|
|
367
|
+
return json.dumps({"status": "error", "message": f"Error executing command: {e!s}"})
|
|
368
|
+
|
|
369
|
+
def create_file(self, agent: Agent | Team, file_path: str, content: str) -> str:
|
|
370
|
+
"""
|
|
371
|
+
Create or update a file in the sandbox.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
agent: An Agno agent instance or a group of agents
|
|
375
|
+
file_path: Path to the file (relative to current directory or absolute).
|
|
376
|
+
content: Content to write to the file.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Success message or error.
|
|
380
|
+
"""
|
|
381
|
+
try:
|
|
382
|
+
current_sandbox = self._get_or_create_sandbox(agent)
|
|
383
|
+
cwd = self._get_working_directory(agent)
|
|
384
|
+
resolved = self._resolve_path(file_path, cwd)
|
|
385
|
+
|
|
386
|
+
# Create parent directories
|
|
387
|
+
parent_dir = str(PurePosixPath(resolved).parent)
|
|
388
|
+
if parent_dir and parent_dir != "/":
|
|
389
|
+
current_sandbox.commands.run(
|
|
390
|
+
f"mkdir -p '{parent_dir}'",
|
|
391
|
+
opts=RunCommandOpts(working_directory="/"),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Write file via SDK
|
|
395
|
+
current_sandbox.files.write_file(resolved, content)
|
|
396
|
+
|
|
397
|
+
return f"File created/updated: {resolved}"
|
|
398
|
+
|
|
399
|
+
except Exception as e:
|
|
400
|
+
return json.dumps({"status": "error", "message": f"Error creating file: {e!s}"})
|
|
401
|
+
|
|
402
|
+
def read_file(self, agent: Agent | Team, file_path: str) -> str:
|
|
403
|
+
"""
|
|
404
|
+
Read a file from the sandbox.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
agent: An Agno agent instance or a group of agents
|
|
408
|
+
file_path: Path to the file (relative to current directory or absolute).
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
File content or error message.
|
|
412
|
+
"""
|
|
413
|
+
try:
|
|
414
|
+
current_sandbox = self._get_or_create_sandbox(agent)
|
|
415
|
+
cwd = self._get_working_directory(agent)
|
|
416
|
+
resolved = self._resolve_path(file_path, cwd)
|
|
417
|
+
|
|
418
|
+
content = current_sandbox.files.read_file(resolved)
|
|
419
|
+
return content
|
|
420
|
+
|
|
421
|
+
except Exception as e:
|
|
422
|
+
return json.dumps({"status": "error", "message": f"Error reading file: {e!s}"})
|
|
423
|
+
|
|
424
|
+
def list_files(self, agent: Agent | Team, directory: str | None = None) -> str:
|
|
425
|
+
"""
|
|
426
|
+
List files in a directory.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
agent: An Agno agent instance or a group of agents
|
|
430
|
+
directory: Directory to list (defaults to current working directory).
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
List of files and directories as formatted string.
|
|
434
|
+
"""
|
|
435
|
+
try:
|
|
436
|
+
current_sandbox = self._get_or_create_sandbox(agent)
|
|
437
|
+
cwd = self._get_working_directory(agent)
|
|
438
|
+
|
|
439
|
+
dir_path = cwd if directory is None else self._resolve_path(directory, cwd)
|
|
440
|
+
|
|
441
|
+
execution = current_sandbox.commands.run(
|
|
442
|
+
f"ls -la '{dir_path}'",
|
|
443
|
+
opts=RunCommandOpts(working_directory="/"),
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
output = self._format_execution(execution)
|
|
447
|
+
return f"Contents of {dir_path}:\n{output}"
|
|
448
|
+
|
|
449
|
+
except Exception as e:
|
|
450
|
+
return json.dumps({"status": "error", "message": f"Error listing files: {e!s}"})
|
|
451
|
+
|
|
452
|
+
def delete_file(self, agent: Agent | Team, file_path: str) -> str:
|
|
453
|
+
"""
|
|
454
|
+
Delete a file or directory from the sandbox.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
agent: An Agno agent instance or a group of agents
|
|
458
|
+
file_path: Path to the file or directory (relative or absolute).
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Success message or error.
|
|
462
|
+
"""
|
|
463
|
+
try:
|
|
464
|
+
current_sandbox = self._get_or_create_sandbox(agent)
|
|
465
|
+
cwd = self._get_working_directory(agent)
|
|
466
|
+
resolved = self._resolve_path(file_path, cwd)
|
|
467
|
+
|
|
468
|
+
# Determine if target is a file or directory via file info
|
|
469
|
+
try:
|
|
470
|
+
info = current_sandbox.files.get_file_info([resolved])
|
|
471
|
+
if resolved in info:
|
|
472
|
+
entry_info = info[resolved]
|
|
473
|
+
# Check if directory using Unix mode bits
|
|
474
|
+
if stat.S_ISDIR(entry_info.mode):
|
|
475
|
+
current_sandbox.files.delete_directories([resolved])
|
|
476
|
+
else:
|
|
477
|
+
current_sandbox.files.delete_files([resolved])
|
|
478
|
+
else:
|
|
479
|
+
# Path not found in info, try both
|
|
480
|
+
current_sandbox.files.delete_files([resolved])
|
|
481
|
+
except Exception:
|
|
482
|
+
# Fallback to shell rm if SDK methods fail
|
|
483
|
+
current_sandbox.commands.run(
|
|
484
|
+
f"rm -rf '{resolved}'",
|
|
485
|
+
opts=RunCommandOpts(working_directory="/"),
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
return f"Deleted: {resolved}"
|
|
489
|
+
|
|
490
|
+
except Exception as e:
|
|
491
|
+
return json.dumps({"status": "error", "message": f"Error deleting file: {e!s}"})
|
|
492
|
+
|
|
493
|
+
def change_directory(self, agent: Agent | Team, directory: str) -> str:
|
|
494
|
+
"""
|
|
495
|
+
Change the current working directory.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
agent: An Agno agent instance or a group of agents
|
|
499
|
+
directory: Directory to change to (relative or absolute).
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Success message or error.
|
|
503
|
+
"""
|
|
504
|
+
try:
|
|
505
|
+
return self.run_shell_command(agent, f"cd {directory}")
|
|
506
|
+
except Exception as e:
|
|
507
|
+
return json.dumps({"status": "error", "message": f"Error changing directory: {e!s}"})
|
|
508
|
+
|
|
509
|
+
def get_sandbox_status(self, agent: Agent | Team) -> str:
|
|
510
|
+
"""
|
|
511
|
+
Get the current status of the sandbox.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
agent: An Agno agent instance or a group of agents
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Sandbox status information.
|
|
518
|
+
"""
|
|
519
|
+
try:
|
|
520
|
+
current_sandbox = self._get_or_create_sandbox(agent)
|
|
521
|
+
sandbox_id = getattr(current_sandbox, "id", "Unknown")
|
|
522
|
+
return json.dumps(
|
|
523
|
+
{
|
|
524
|
+
"status": "success",
|
|
525
|
+
"sandbox_id": sandbox_id,
|
|
526
|
+
"image": self.image,
|
|
527
|
+
"working_directory": self._get_working_directory(agent),
|
|
528
|
+
}
|
|
529
|
+
)
|
|
530
|
+
except Exception as e:
|
|
531
|
+
return json.dumps({"status": "error", "message": f"Error getting status: {e!s}"})
|
|
532
|
+
|
|
533
|
+
def shutdown_sandbox(self, agent: Agent | Team) -> str:
|
|
534
|
+
"""
|
|
535
|
+
Shut down the sandbox immediately.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
agent: An Agno agent instance or a group of agents
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
Success message or error.
|
|
542
|
+
"""
|
|
543
|
+
try:
|
|
544
|
+
current_sandbox = self._get_or_create_sandbox(agent)
|
|
545
|
+
current_sandbox.kill()
|
|
546
|
+
self._sandbox = None
|
|
547
|
+
self._interpreter = None
|
|
548
|
+
|
|
549
|
+
# Clear session state
|
|
550
|
+
if self.persistent and hasattr(agent, "session_state") and agent.session_state:
|
|
551
|
+
agent.session_state.pop("opensandbox_id", None)
|
|
552
|
+
agent.session_state.pop("working_directory", None)
|
|
553
|
+
|
|
554
|
+
return json.dumps({"status": "success", "message": "Sandbox shut down successfully"})
|
|
555
|
+
except Exception as e:
|
|
556
|
+
return json.dumps({"status": "error", "message": f"Error shutting down: {e!s}"})
|