Skip to content

Commit dab80e4

Browse files
lushasummercopybara-github
authored andcommitted
fix: Update agent_engine_sandbox_code_executor in ADK
1. For prototyping and testing purposes, sandbox name can be provided, and it will be used for all requests across the lifecycle of an agent 2. If no sandbox name is provided, agent engine name will be provided, and we will automatically create one sandbox per session, and the sandbox has TTL set for a year. If the sandbox stored in the session hits the TTL, it will not be in "STATE_RUNNING" so a new sandbox will be created. Co-authored-by: Lusha Wang <lusha@google.com> PiperOrigin-RevId: 874415933
1 parent 6d53d80 commit dab80e4

4 files changed

Lines changed: 179 additions & 19 deletions

File tree

contributing/samples/agent_engine_code_execution/README

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ This sample data science agent uses Agent Engine Code Execution Sandbox to execu
77

88
## How to use
99

10-
* 1. Follow https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/code-execution/overview to create a code execution sandbox environment.
10+
* 1. Follow https://docs.cloud.google.com/agent-builder/agent-engine/code-execution/quickstart#create-an-agent-engine-instance to create an agent engine instance. Replace the AGENT_ENGINE_RESOURCE_NAME with the one you just created. A new sandbox environment under this agent engine instance will be created for each session with TTL of 1 year. But sandbox can only main its state for up to 14 days. This is the recommended usage for production environments.
1111

12-
* 2. Replace the SANDBOX_RESOURCE_NAME with the one you just created. If you dont want to create a new sandbox environment directly, the Agent Engine Code Execution Sandbox will create one for you by default using the AGENT_ENGINE_RESOURCE_NAME you specified, however, please ensure to clean up sandboxes after use; otherwise, it will consume quotas.
12+
* 2. For testing or protyping purposes, create a sandbox environment by following this guide: https://docs.cloud.google.com/agent-builder/agent-engine/code-execution/quickstart#create_a_sandbox. Replace the SANDBOX_RESOURCE_NAME with the one you just created. This will be used as the default sandbox environment for all the code executions throughout the lifetime of the agent. As the sandbox is re-used across sessions, all sessions will share the same Python environment and variable values."
1313

1414

1515
## Sample prompt

contributing/samples/agent_engine_code_execution/agent.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,10 @@ def base_system_instruction():
8585
8686
""",
8787
code_executor=AgentEngineSandboxCodeExecutor(
88-
# Replace with your sandbox resource name if you already have one.
89-
sandbox_resource_name="SANDBOX_RESOURCE_NAME",
88+
# Replace with your sandbox resource name if you already have one. Only use it for testing or prototyping purposes, because this will use the same sandbox for all requests.
9089
# "projects/vertex-agent-loadtest/locations/us-central1/reasoningEngines/6842889780301135872/sandboxEnvironments/6545148628569161728",
91-
# Replace with agent engine resource name used for creating sandbox if
92-
# sandbox_resource_name is not set.
90+
sandbox_resource_name=None,
91+
# Replace with agent engine resource name used for creating sandbox environment.
9392
agent_engine_resource_name="AGENT_ENGINE_RESOURCE_NAME",
9493
),
9594
)

src/google/adk/code_executors/agent_engine_sandbox_code_executor.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from typing import Optional
2222

2323
from typing_extensions import override
24+
from vertexai import types
2425

2526
from ..agents.invocation_context import InvocationContext
2627
from .base_code_executor import BaseCodeExecutor
@@ -38,10 +39,15 @@ class AgentEngineSandboxCodeExecutor(BaseCodeExecutor):
3839
sandbox_resource_name: If set, load the existing resource name of the code
3940
interpreter extension instead of creating a new one. Format:
4041
projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789
42+
agent_engine_resource_name: The resource name of the agent engine to use
43+
to create the code execution sandbox. Format:
44+
projects/123/locations/us-central1/reasoningEngines/456
4145
"""
4246

4347
sandbox_resource_name: str = None
4448

49+
agent_engine_resource_name: str = None
50+
4551
def __init__(
4652
self,
4753
sandbox_resource_name: Optional[str] = None,
@@ -67,30 +73,19 @@ def __init__(
6773
agent_engine_resource_name_pattern = r'^projects/([a-zA-Z0-9-_]+)/locations/([a-zA-Z0-9-_]+)/reasoningEngines/(\d+)$'
6874

6975
if sandbox_resource_name is not None:
70-
self.sandbox_resource_name = sandbox_resource_name
7176
self._project_id, self._location = (
7277
self._get_project_id_and_location_from_resource_name(
7378
sandbox_resource_name, sandbox_resource_name_pattern
7479
)
7580
)
81+
self.sandbox_resource_name = sandbox_resource_name
7682
elif agent_engine_resource_name is not None:
77-
from vertexai import types
78-
7983
self._project_id, self._location = (
8084
self._get_project_id_and_location_from_resource_name(
8185
agent_engine_resource_name, agent_engine_resource_name_pattern
8286
)
8387
)
84-
# @TODO - Add TTL for sandbox creation after it is available
85-
# in SDK.
86-
operation = self._get_api_client().agent_engines.sandboxes.create(
87-
spec={'code_execution_environment': {}},
88-
name=agent_engine_resource_name,
89-
config=types.CreateAgentEngineSandboxConfig(
90-
display_name='default_sandbox'
91-
),
92-
)
93-
self.sandbox_resource_name = operation.response.name
88+
self.agent_engine_resource_name = agent_engine_resource_name
9489
else:
9590
raise ValueError(
9691
'Either sandbox_resource_name or agent_engine_resource_name must be'
@@ -103,6 +98,39 @@ def execute_code(
10398
invocation_context: InvocationContext,
10499
code_execution_input: CodeExecutionInput,
105100
) -> CodeExecutionResult:
101+
if self.sandbox_resource_name is None:
102+
sandbox_name = invocation_context.session.state.get('sandbox_name', None)
103+
create_new_sandbox = False
104+
if sandbox_name is None:
105+
create_new_sandbox = True
106+
else:
107+
# Check if the sandbox is still running OR already expired due to ttl.
108+
sandbox = self._get_api_client().agent_engines.sandboxes.get(
109+
name=sandbox_name
110+
)
111+
if not sandbox or sandbox.state != 'STATE_RUNNING':
112+
create_new_sandbox = True
113+
114+
if create_new_sandbox:
115+
operation = self._get_api_client().agent_engines.sandboxes.create(
116+
spec={'code_execution_environment': {}},
117+
name=self.agent_engine_resource_name,
118+
config=types.CreateAgentEngineSandboxConfig(
119+
# VertexAiSessionService has a default TTL of 1 year, so we set
120+
# the sandbox TTL to 1 year as well. For the current code
121+
# execution sandbox, if it hasn't been used for 14 days, the
122+
# state will be lost.
123+
display_name='default_sandbox',
124+
ttl='31536000s',
125+
),
126+
)
127+
self.sandbox_resource_name = operation.response.name
128+
invocation_context.session.state['sandbox_name'] = (
129+
self.sandbox_resource_name
130+
)
131+
else:
132+
self.sandbox_resource_name = sandbox_name
133+
106134
# Execute the code.
107135
input_data = {
108136
'code': code_execution_input.code,

tests/unittests/code_executors/test_agent_engine_sandbox_code_executor.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from google.adk.agents.invocation_context import InvocationContext
2020
from google.adk.code_executors.agent_engine_sandbox_code_executor import AgentEngineSandboxCodeExecutor
2121
from google.adk.code_executors.code_execution_utils import CodeExecutionInput
22+
from google.adk.sessions.session import Session
2223
import pytest
2324

2425

@@ -27,6 +28,10 @@ def mock_invocation_context() -> InvocationContext:
2728
"""Fixture for a mock InvocationContext."""
2829
mock = MagicMock(spec=InvocationContext)
2930
mock.invocation_id = "test-invocation-123"
31+
session = MagicMock(spec=Session)
32+
mock.session = session
33+
session.state = []
34+
3035
return mock
3136

3237

@@ -118,3 +123,131 @@ def test_execute_code_success(
118123
name="projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789",
119124
input_data={"code": 'print("hello world")'},
120125
)
126+
127+
@patch("vertexai.Client")
128+
def test_execute_code_recreates_sandbox_when_get_returns_none(
129+
self,
130+
mock_vertexai_client,
131+
mock_invocation_context,
132+
):
133+
# Setup Mocks
134+
mock_api_client = MagicMock()
135+
mock_vertexai_client.return_value = mock_api_client
136+
137+
# Existing sandbox name stored in session, but get() will return None
138+
existing_sandbox_name = "projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/old"
139+
mock_invocation_context.session.state = {
140+
"sandbox_name": existing_sandbox_name
141+
}
142+
143+
# Mock get to return None (simulating missing/expired sandbox)
144+
mock_api_client.agent_engines.sandboxes.get.return_value = None
145+
146+
# Mock create operation to return a new sandbox resource name
147+
operation_mock = MagicMock()
148+
created_sandbox_name = "projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789"
149+
operation_mock.response.name = created_sandbox_name
150+
mock_api_client.agent_engines.sandboxes.create.return_value = operation_mock
151+
152+
# Mock execute_code response
153+
mock_response = MagicMock()
154+
mock_json_output = MagicMock()
155+
mock_json_output.mime_type = "application/json"
156+
mock_json_output.data = json.dumps(
157+
{"stdout": "recreated sandbox run", "stderr": ""}
158+
).encode("utf-8")
159+
mock_json_output.metadata = None
160+
mock_response.outputs = [mock_json_output]
161+
mock_api_client.agent_engines.sandboxes.execute_code.return_value = (
162+
mock_response
163+
)
164+
165+
# Execute using agent_engine_resource_name so a sandbox can be created
166+
executor = AgentEngineSandboxCodeExecutor(
167+
agent_engine_resource_name=(
168+
"projects/123/locations/us-central1/reasoningEngines/456"
169+
)
170+
)
171+
code_input = CodeExecutionInput(code='print("hello world")')
172+
result = executor.execute_code(mock_invocation_context, code_input)
173+
174+
# Assert get was called for the existing sandbox
175+
mock_api_client.agent_engines.sandboxes.get.assert_called_once_with(
176+
name=existing_sandbox_name
177+
)
178+
179+
# Assert create was called and session updated with new sandbox
180+
mock_api_client.agent_engines.sandboxes.create.assert_called_once()
181+
assert executor.sandbox_resource_name == created_sandbox_name
182+
assert (
183+
mock_invocation_context.session.state["sandbox_name"]
184+
== created_sandbox_name
185+
)
186+
187+
# Assert execute_code used the created sandbox name
188+
mock_api_client.agent_engines.sandboxes.execute_code.assert_called_once_with(
189+
name=created_sandbox_name,
190+
input_data={"code": 'print("hello world")'},
191+
)
192+
193+
@patch("vertexai.Client")
194+
def test_execute_code_creates_sandbox_if_missing(
195+
self,
196+
mock_vertexai_client,
197+
mock_invocation_context,
198+
):
199+
# Setup Mocks
200+
mock_api_client = MagicMock()
201+
mock_vertexai_client.return_value = mock_api_client
202+
203+
# Mock create operation to return a sandbox resource name
204+
operation_mock = MagicMock()
205+
created_sandbox_name = "projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789"
206+
operation_mock.response.name = created_sandbox_name
207+
mock_api_client.agent_engines.sandboxes.create.return_value = operation_mock
208+
209+
# Mock execute_code response
210+
mock_response = MagicMock()
211+
mock_json_output = MagicMock()
212+
mock_json_output.mime_type = "application/json"
213+
mock_json_output.data = json.dumps(
214+
{"stdout": "created sandbox run", "stderr": ""}
215+
).encode("utf-8")
216+
mock_json_output.metadata = None
217+
mock_response.outputs = [mock_json_output]
218+
mock_api_client.agent_engines.sandboxes.execute_code.return_value = (
219+
mock_response
220+
)
221+
222+
# Ensure session.state behaves like a dict for storing sandbox_name
223+
mock_invocation_context.session.state = {}
224+
225+
# Execute using agent_engine_resource_name so a sandbox will be created
226+
executor = AgentEngineSandboxCodeExecutor(
227+
agent_engine_resource_name=(
228+
"projects/123/locations/us-central1/reasoningEngines/456"
229+
),
230+
sandbox_resource_name=None,
231+
)
232+
code_input = CodeExecutionInput(code='print("hello world")')
233+
result = executor.execute_code(mock_invocation_context, code_input)
234+
235+
# Assert sandbox creation was called and session state updated
236+
mock_api_client.agent_engines.sandboxes.create.assert_called_once()
237+
create_call_kwargs = (
238+
mock_api_client.agent_engines.sandboxes.create.call_args.kwargs
239+
)
240+
assert create_call_kwargs["name"] == (
241+
"projects/123/locations/us-central1/reasoningEngines/456"
242+
)
243+
assert executor.sandbox_resource_name == created_sandbox_name
244+
assert (
245+
mock_invocation_context.session.state["sandbox_name"]
246+
== created_sandbox_name
247+
)
248+
249+
# Assert execute_code used the created sandbox name
250+
mock_api_client.agent_engines.sandboxes.execute_code.assert_called_once_with(
251+
name=created_sandbox_name,
252+
input_data={"code": 'print("hello world")'},
253+
)

0 commit comments

Comments
 (0)