Skip to content

Commit 6c34694

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Enhance AgentEngineSandboxCodeExecutor sample to automatically provision an Agent Engine if neither agent_engine_resource_name nor sandbox_resource_name is provided
The AgentEngineSandboxCodeExecutor now has three initialization modes: 1. Create both an Agent Engine and sandbox if neither resource name is provided. 2. Creating a new sandbox within a provided agent_engine_resource_name. 3. Using a provided sandbox_resource_name. PiperOrigin-RevId: 884088248
1 parent f8270c8 commit 6c34694

File tree

3 files changed

+138
-9
lines changed

3 files changed

+138
-9
lines changed

contributing/samples/agent_engine_code_execution/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,6 @@ def base_system_instruction():
8989
# "projects/vertex-agent-loadtest/locations/us-central1/reasoningEngines/6842889780301135872/sandboxEnvironments/6545148628569161728",
9090
sandbox_resource_name=None,
9191
# Replace with agent engine resource name used for creating sandbox environment.
92-
agent_engine_resource_name="AGENT_ENGINE_RESOURCE_NAME",
92+
agent_engine_resource_name=None,
9393
),
9494
)

src/google/adk/code_executors/agent_engine_sandbox_code_executor.py

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
import json
1818
import logging
1919
import mimetypes
20+
import os
2021
import re
22+
import threading
2123
from typing import Optional
2224

2325
from typing_extensions import override
@@ -46,6 +48,7 @@ class AgentEngineSandboxCodeExecutor(BaseCodeExecutor):
4648
sandbox_resource_name: str = None
4749

4850
agent_engine_resource_name: str = None
51+
_agent_engine_creation_lock: Optional[threading.Lock] = None
4952

5053
def __init__(
5154
self,
@@ -61,42 +64,69 @@ def __init__(
6164
projects/123/locations/us-central1/reasoningEngines/456/
6265
sandboxEnvironments/789
6366
agent_engine_resource_name: The resource name of the agent engine to use
64-
to create the code execution sandbox. Format:
67+
to create the code execution sandbox. If not set, a new Agent Engine
68+
will be created automatically. Format:
6569
projects/123/locations/us-central1/reasoningEngines/456, when both
6670
sandbox_resource_name and agent_engine_resource_name are set,
6771
agent_engine_resource_name will be ignored.
6872
**data: Additional keyword arguments to be passed to the base class.
6973
"""
7074
super().__init__(**data)
75+
self._agent_engine_creation_lock = threading.Lock()
7176
sandbox_resource_name_pattern = r'^projects/([a-zA-Z0-9-_]+)/locations/([a-zA-Z0-9-_]+)/reasoningEngines/(\d+)/sandboxEnvironments/(\d+)$'
7277
agent_engine_resource_name_pattern = r'^projects/([a-zA-Z0-9-_]+)/locations/([a-zA-Z0-9-_]+)/reasoningEngines/(\d+)$'
7378

79+
# Case 1: sandbox_resource_name is provided.
7480
if sandbox_resource_name is not None:
7581
self._project_id, self._location = (
7682
self._get_project_id_and_location_from_resource_name(
7783
sandbox_resource_name, sandbox_resource_name_pattern
7884
)
7985
)
8086
self.sandbox_resource_name = sandbox_resource_name
81-
elif agent_engine_resource_name is not None:
87+
88+
# Case 2: Agent Engine resource name is not provided.
89+
elif agent_engine_resource_name is None:
90+
# The Agent Engine will be auto-created lazily within execute_code().
91+
self._project_id = os.environ.get('GOOGLE_CLOUD_PROJECT')
92+
self._location = os.environ.get('GOOGLE_CLOUD_LOCATION', 'us-central1')
93+
self.agent_engine_resource_name = None
94+
95+
# Case 3: Use the provided agent_engine_resource_name.
96+
else:
8297
self._project_id, self._location = (
8398
self._get_project_id_and_location_from_resource_name(
84-
agent_engine_resource_name, agent_engine_resource_name_pattern
99+
agent_engine_resource_name,
100+
agent_engine_resource_name_pattern,
85101
)
86102
)
87103
self.agent_engine_resource_name = agent_engine_resource_name
88-
else:
89-
raise ValueError(
90-
'Either sandbox_resource_name or agent_engine_resource_name must be'
91-
' set.'
92-
)
93104

94105
@override
95106
def execute_code(
96107
self,
97108
invocation_context: InvocationContext,
98109
code_execution_input: CodeExecutionInput,
99110
) -> CodeExecutionResult:
111+
if (
112+
self.sandbox_resource_name is None
113+
and self.agent_engine_resource_name is None
114+
):
115+
with self._agent_engine_creation_lock:
116+
if self.agent_engine_resource_name is None:
117+
logger.info(
118+
'No Agent Engine resource name provided. Creating a new one...'
119+
)
120+
try:
121+
# Create a default Agent Engine.
122+
created_engine = self._get_api_client().agent_engines.create()
123+
self.agent_engine_resource_name = created_engine.api_resource.name
124+
logger.info(
125+
'Created Agent Engine: %s', self.agent_engine_resource_name
126+
)
127+
except Exception as e:
128+
logger.error('Failed to auto-create Agent Engine: %s', e)
129+
raise
100130
# default to the sandbox resource name if set.
101131
sandbox_name = self.sandbox_resource_name
102132
if self.sandbox_resource_name is None:

tests/unittests/code_executors/test_agent_engine_sandbox_code_executor.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import json
16+
import os
1617
from unittest.mock import MagicMock
1718
from unittest.mock import patch
1819

@@ -249,3 +250,101 @@ def test_execute_code_creates_sandbox_if_missing(
249250
name=created_sandbox_name,
250251
input_data={"code": 'print("hello world")'},
251252
)
253+
254+
def test_init_with_agent_engine_resource_name(self):
255+
"""Tests init when only agent_engine_resource_name is provided."""
256+
agent_engine_name = (
257+
"projects/123/locations/us-central1/reasoningEngines/456"
258+
)
259+
260+
executor = AgentEngineSandboxCodeExecutor(
261+
agent_engine_resource_name=agent_engine_name
262+
)
263+
264+
# Verify the engine name is set, and sandbox remains None.
265+
assert executor.agent_engine_resource_name == agent_engine_name
266+
assert executor.sandbox_resource_name is None
267+
assert executor._project_id == "123"
268+
assert executor._location == "us-central1"
269+
270+
@patch("vertexai.Client")
271+
@patch.dict(
272+
os.environ,
273+
{
274+
"GOOGLE_CLOUD_PROJECT": "test-project-456",
275+
"GOOGLE_CLOUD_LOCATION": "us-central1",
276+
},
277+
)
278+
def test_execute_code_with_auto_create_agent_engine(
279+
self, mock_vertexai_client, mock_invocation_context
280+
):
281+
"""Tests that Agent Engine is created lazily in execute_code."""
282+
# Setup Mocks
283+
mock_api_client = MagicMock()
284+
mock_vertexai_client.return_value = mock_api_client
285+
286+
# Mock Engine Creation
287+
mock_created_engine = MagicMock()
288+
mock_created_engine.api_resource.name = "projects/test-project-456/locations/us-central1/reasoningEngines/auto-created-ae-1"
289+
mock_api_client.agent_engines.create.return_value = mock_created_engine
290+
291+
# Mock create operation to return a sandbox resource name
292+
operation_mock = MagicMock()
293+
created_sandbox_name = "projects/test-project-456/locations/us-central1/reasoningEngines/auto-created-ae-1/sandboxEnvironments/789"
294+
operation_mock.response.name = created_sandbox_name
295+
mock_api_client.agent_engines.sandboxes.create.return_value = operation_mock
296+
297+
# Mock execute_code response
298+
mock_response = MagicMock()
299+
mock_json_output = MagicMock()
300+
mock_json_output.mime_type = "application/json"
301+
mock_json_output.data = json.dumps(
302+
{"stdout": "created sandbox run", "stderr": ""}
303+
).encode("utf-8")
304+
mock_json_output.metadata = None
305+
mock_response.outputs = [mock_json_output]
306+
mock_api_client.agent_engines.sandboxes.execute_code.return_value = (
307+
mock_response
308+
)
309+
310+
# Execute
311+
executor = AgentEngineSandboxCodeExecutor()
312+
code_input = CodeExecutionInput(code='print("hello world")')
313+
executor.execute_code(mock_invocation_context, code_input)
314+
315+
# Assert
316+
mock_api_client.agent_engines.create.assert_called_once()
317+
assert (
318+
executor.agent_engine_resource_name
319+
== "projects/test-project-456/locations/us-central1/reasoningEngines/auto-created-ae-1"
320+
)
321+
assert executor.sandbox_resource_name is None
322+
mock_api_client.agent_engines.sandboxes.create.assert_called_once()
323+
assert (
324+
mock_invocation_context.session.state["sandbox_name"]
325+
== created_sandbox_name
326+
)
327+
328+
@patch("vertexai.Client")
329+
@patch.dict(
330+
os.environ,
331+
{
332+
"GOOGLE_CLOUD_PROJECT": "test-project-456",
333+
"GOOGLE_CLOUD_LOCATION": "us-central1",
334+
},
335+
)
336+
def test_execute_code_auto_create_agent_engine_fails(
337+
self, mock_vertexai_client, mock_invocation_context
338+
):
339+
"""Tests error handling when auto-creating Agent Engine fails."""
340+
mock_api_client = MagicMock()
341+
mock_vertexai_client.return_value = mock_api_client
342+
mock_api_client.agent_engines.create.side_effect = Exception(
343+
"Failed to auto-create Agent Engine"
344+
)
345+
346+
executor = AgentEngineSandboxCodeExecutor()
347+
code_input = CodeExecutionInput(code='print("hello world")')
348+
349+
with pytest.raises(Exception, match="Failed to auto-create Agent Engine"):
350+
executor.execute_code(mock_invocation_context, code_input)

0 commit comments

Comments
 (0)