Skip to content

Commit dff4c44

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: 876450610
1 parent 1206add commit dff4c44

File tree

4 files changed

+183
-20
lines changed

4 files changed

+183
-20
lines changed

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: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,15 @@ class AgentEngineSandboxCodeExecutor(BaseCodeExecutor):
3838
sandbox_resource_name: If set, load the existing resource name of the code
3939
interpreter extension instead of creating a new one. Format:
4040
projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789
41+
agent_engine_resource_name: The resource name of the agent engine to use
42+
to create the code execution sandbox. Format:
43+
projects/123/locations/us-central1/reasoningEngines/456
4144
"""
4245

4346
sandbox_resource_name: str = None
4447

48+
agent_engine_resource_name: str = None
49+
4550
def __init__(
4651
self,
4752
sandbox_resource_name: Optional[str] = None,
@@ -67,30 +72,19 @@ def __init__(
6772
agent_engine_resource_name_pattern = r'^projects/([a-zA-Z0-9-_]+)/locations/([a-zA-Z0-9-_]+)/reasoningEngines/(\d+)$'
6873

6974
if sandbox_resource_name is not None:
70-
self.sandbox_resource_name = sandbox_resource_name
7175
self._project_id, self._location = (
7276
self._get_project_id_and_location_from_resource_name(
7377
sandbox_resource_name, sandbox_resource_name_pattern
7478
)
7579
)
80+
self.sandbox_resource_name = sandbox_resource_name
7681
elif agent_engine_resource_name is not None:
77-
from vertexai import types
78-
7982
self._project_id, self._location = (
8083
self._get_project_id_and_location_from_resource_name(
8184
agent_engine_resource_name, agent_engine_resource_name_pattern
8285
)
8386
)
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
87+
self.agent_engine_resource_name = agent_engine_resource_name
9488
else:
9589
raise ValueError(
9690
'Either sandbox_resource_name or agent_engine_resource_name must be'
@@ -103,6 +97,45 @@ def execute_code(
10397
invocation_context: InvocationContext,
10498
code_execution_input: CodeExecutionInput,
10599
) -> CodeExecutionResult:
100+
# default to the sandbox resource name if set.
101+
sandbox_name = self.sandbox_resource_name
102+
if self.sandbox_resource_name is None:
103+
from google.api_core import exceptions
104+
from vertexai import types
105+
106+
# use sandbox name stored in session if available.
107+
sandbox_name = invocation_context.session.state.get('sandbox_name', None)
108+
create_new_sandbox = False
109+
if sandbox_name is None:
110+
create_new_sandbox = True
111+
else:
112+
# Check if the sandbox is still running OR already expired due to ttl.
113+
try:
114+
sandbox = self._get_api_client().agent_engines.sandboxes.get(
115+
name=sandbox_name
116+
)
117+
if sandbox is None or sandbox.state != 'STATE_RUNNING':
118+
create_new_sandbox = True
119+
except exceptions.NotFound:
120+
create_new_sandbox = True
121+
122+
if create_new_sandbox:
123+
# Create a new sandbox and assign it to sandbox_name.
124+
operation = self._get_api_client().agent_engines.sandboxes.create(
125+
spec={'code_execution_environment': {}},
126+
name=self.agent_engine_resource_name,
127+
config=types.CreateAgentEngineSandboxConfig(
128+
# VertexAiSessionService has a default TTL of 1 year, so we set
129+
# the sandbox TTL to 1 year as well. For the current code
130+
# execution sandbox, if it hasn't been used for 14 days, the
131+
# state will be lost.
132+
display_name='default_sandbox',
133+
ttl='31536000s',
134+
),
135+
)
136+
sandbox_name = operation.response.name
137+
invocation_context.session.state['sandbox_name'] = sandbox_name
138+
106139
# Execute the code.
107140
input_data = {
108141
'code': code_execution_input.code,
@@ -119,7 +152,7 @@ def execute_code(
119152

120153
code_execution_response = (
121154
self._get_api_client().agent_engines.sandboxes.execute_code(
122-
name=self.sandbox_resource_name,
155+
name=sandbox_name,
123156
input_data=input_data,
124157
)
125158
)

tests/unittests/code_executors/test_agent_engine_sandbox_code_executor.py

Lines changed: 131 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,129 @@ 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 (
182+
mock_invocation_context.session.state["sandbox_name"]
183+
== created_sandbox_name
184+
)
185+
186+
# Assert execute_code used the created sandbox name
187+
mock_api_client.agent_engines.sandboxes.execute_code.assert_called_once_with(
188+
name=created_sandbox_name,
189+
input_data={"code": 'print("hello world")'},
190+
)
191+
192+
@patch("vertexai.Client")
193+
def test_execute_code_creates_sandbox_if_missing(
194+
self,
195+
mock_vertexai_client,
196+
mock_invocation_context,
197+
):
198+
# Setup Mocks
199+
mock_api_client = MagicMock()
200+
mock_vertexai_client.return_value = mock_api_client
201+
202+
# Mock create operation to return a sandbox resource name
203+
operation_mock = MagicMock()
204+
created_sandbox_name = "projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789"
205+
operation_mock.response.name = created_sandbox_name
206+
mock_api_client.agent_engines.sandboxes.create.return_value = operation_mock
207+
208+
# Mock execute_code response
209+
mock_response = MagicMock()
210+
mock_json_output = MagicMock()
211+
mock_json_output.mime_type = "application/json"
212+
mock_json_output.data = json.dumps(
213+
{"stdout": "created sandbox run", "stderr": ""}
214+
).encode("utf-8")
215+
mock_json_output.metadata = None
216+
mock_response.outputs = [mock_json_output]
217+
mock_api_client.agent_engines.sandboxes.execute_code.return_value = (
218+
mock_response
219+
)
220+
221+
# Ensure session.state behaves like a dict for storing sandbox_name
222+
mock_invocation_context.session.state = {}
223+
224+
# Execute using agent_engine_resource_name so a sandbox will be created
225+
executor = AgentEngineSandboxCodeExecutor(
226+
agent_engine_resource_name=(
227+
"projects/123/locations/us-central1/reasoningEngines/456"
228+
),
229+
sandbox_resource_name=None,
230+
)
231+
code_input = CodeExecutionInput(code='print("hello world")')
232+
result = executor.execute_code(mock_invocation_context, code_input)
233+
234+
# Assert sandbox creation was called and session state updated
235+
mock_api_client.agent_engines.sandboxes.create.assert_called_once()
236+
create_call_kwargs = (
237+
mock_api_client.agent_engines.sandboxes.create.call_args.kwargs
238+
)
239+
assert create_call_kwargs["name"] == (
240+
"projects/123/locations/us-central1/reasoningEngines/456"
241+
)
242+
assert (
243+
mock_invocation_context.session.state["sandbox_name"]
244+
== created_sandbox_name
245+
)
246+
247+
# Assert execute_code used the created sandbox name
248+
mock_api_client.agent_engines.sandboxes.execute_code.assert_called_once_with(
249+
name=created_sandbox_name,
250+
input_data={"code": 'print("hello world")'},
251+
)

0 commit comments

Comments
 (0)