Skip to content

Commit 2a3539b

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
feat: GenAI SDK client - Support agent engine sandbox http request in genai sdk
PiperOrigin-RevId: 816865842
1 parent 11c3ff0 commit 2a3539b

File tree

8 files changed

+141
-21
lines changed

8 files changed

+141
-21
lines changed

.kokoro/build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export PROJECT_ID=$(cat "${KOKORO_GFILE_DIR}/project-id.json")
3737
python3 -m pip uninstall --yes --quiet nox-automation
3838

3939
# Install nox
40-
python3 -m pip install --upgrade --quiet nox
40+
python3 -m pip install --upgrade --quiet uv nox
4141
python3 -m nox --version
4242

4343
# If this is a continuous build, send the test log to the FlakyBot.

.kokoro/requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ twine
66
wheel
77
setuptools
88
nox>=2022.11.21 # required to remove dependency on py
9+
uv
910
charset-normalizer<3
1011
click<8.1.0
1112
build

.kokoro/requirements.txt

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# This file is autogenerated by pip-compile with Python 3.9
2+
# This file is autogenerated by pip-compile with Python 3.13
33
# by the following command:
44
#
55
# pip-compile --allow-unsafe --generate-hashes requirements.in
@@ -255,8 +255,6 @@ importlib-metadata==6.8.0 \
255255
--hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
256256
# via
257257
# -r requirements.in
258-
# build
259-
# keyring
260258
# twine
261259
jaraco-classes==3.3.0 \
262260
--hash=sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb \
@@ -491,12 +489,6 @@ six==1.16.0 \
491489
# via
492490
# gcp-docuploader
493491
# python-dateutil
494-
tomli==2.0.1 \
495-
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
496-
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
497-
# via
498-
# build
499-
# pyproject-hooks
500492
twine==4.0.2 \
501493
--hash=sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8 \
502494
--hash=sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8
@@ -511,6 +503,27 @@ urllib3==2.0.7 \
511503
# via
512504
# requests
513505
# twine
506+
uv==0.9.5 \
507+
--hash=sha256:02db727beb94a2137508cee5a785c3465d150954ca9abdff2d8157c76dea163e \
508+
--hash=sha256:0316493044035098666d6e99c14bd61b352555d9717d57269f4ce531855330fa \
509+
--hash=sha256:133e2614e1ff3b34c2606595d8ae55710473ebb7516bfa5708afc00315730cd1 \
510+
--hash=sha256:21452ece590ddb90e869a478ca4c2ba70be180ec0d6716985ee727b9394c8aa5 \
511+
--hash=sha256:3a4ecbfdcbd3dae4190428874762c791e05d2c97ff2872bf6c0a30ed5c4ea9ca \
512+
--hash=sha256:3eb31c9896dc2c88f6a9f1d693be2409fe2fc2e3d90827956e4341c2b2171289 \
513+
--hash=sha256:48a12390421f91af8a8993cf15c38297c0bb121936046286e287975b2fbf1789 \
514+
--hash=sha256:48a3542835d37882ff57d1ff91b757085525d98756712fa61cf9941d3dda8ebf \
515+
--hash=sha256:5bb4996329ba47e7e775baba4a47e85092aa491d708a66e63b564e9b306bfb7e \
516+
--hash=sha256:6452eb6257e37e1ebd97430b5f5e10419da2c3ca35b4086540ec4163b4b2f25c \
517+
--hash=sha256:6507bbbcd788553ec4ad5a96fa19364dc0f58b023e31d79868773559a83ec181 \
518+
--hash=sha256:6a046c2e833169bf26f461286aab58a2ba8d48ed2220bfcf119dcfaf87163116 \
519+
--hash=sha256:8603bb902e578463c50c3ddd4ee376ba4172ccdf4979787f8948747d1bb0e18b \
520+
--hash=sha256:922cd784cce36bbdc7754b590d28c276698c85791c18cd4c6a7e917db4480440 \
521+
--hash=sha256:9fc13b4b943d19adac52d7dcd2159e96ab2e837ac49a79e20714ed25f1f1b7f9 \
522+
--hash=sha256:c465f2e342cab908849b8ce83e14fd4cf75f5bed55802d0acf1399f9d02f92d9 \
523+
--hash=sha256:c966e3a4fe4de3b0a6279d0a835c79f9cddbb3693f52d140910cbbed177c5742 \
524+
--hash=sha256:d8835d2c034421ac2235fb658bb4f669a301a0f1eb00a8430148dd8461b65641 \
525+
--hash=sha256:f8eb34ebebac4b45334ce7082cca99293b71fb32b164651f1727c8a640e5b387
526+
# via -r requirements.in
514527
virtualenv==20.24.6 \
515528
--hash=sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af \
516529
--hash=sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381

.kokoro/test-samples-impl.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ env | grep KOKORO
3434

3535
# Install nox
3636
# `virtualenv==20.26.6` is added for Python 3.7 compatibility
37-
python3 -m pip install --upgrade --quiet nox virtualenv==20.26.6
37+
python3 -m pip install --upgrade --quiet nox uv virtualenv==20.26.6
3838

3939
# Use secrets acessor service account to get secrets
4040
if [[ -f "${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" ]]; then

noxfile.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
import nox
2727

28+
nox.options.default_venv_backend = "uv"
29+
2830
FLAKE8_VERSION = "flake8==6.1.0"
2931
BLACK_VERSION = "black==24.8.0"
3032
ISORT_VERSION = "isort==5.10.1"
@@ -161,7 +163,7 @@ def format(session):
161163
@nox.session(python=DEFAULT_PYTHON_VERSION)
162164
def lint_setup_py(session):
163165
"""Verify that setup.py is valid (including RST check)."""
164-
session.install("docutils", "pygments")
166+
session.install("docutils", "pygments", "setuptools")
165167
session.run("python", "setup.py", "check", "--restructuredtext", "--strict")
166168

167169

tests/unit/aiplatform/test_training_jobs.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -662,15 +662,6 @@ def test_packager_creates_and_copies_python_package(self):
662662
f"{tsp._ROOT_MODULE}-{tsp._SETUP_PY_VERSION}.tar.gz"
663663
).is_file()
664664

665-
def test_created_package_module_is_installable_and_can_be_run(self):
666-
tsp = source_utils._TrainingScriptPythonPackager(_TEST_LOCAL_SCRIPT_FILE_PATH)
667-
source_dist_path = tsp.package_and_copy(copy_method=local_copy_method)
668-
subprocess.check_output(["pip3", "install", source_dist_path])
669-
module_output = subprocess.check_output(
670-
[source_utils._get_python_executable(), "-m", tsp.module_name]
671-
)
672-
assert "hello world" in module_output.decode()
673-
674665
def test_requirements_are_in_package(self):
675666
tsp = source_utils._TrainingScriptPythonPackager(
676667
_TEST_LOCAL_SCRIPT_FILE_PATH, requirements=_TEST_REQUIREMENTS
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# pylint: disable=protected-access,bad-continuation,missing-function-docstring
16+
17+
from tests.unit.vertexai.genai.replays import pytest_helper
18+
19+
20+
def test_send_command_sandbox(client):
21+
# agent_engine = client.agent_engines.create()
22+
# assert isinstance(agent_engine, types.AgentEngine)
23+
# assert isinstance(agent_engine.api_resource, types.ReasoningEngine)
24+
# agent_engine_name = "projects/254005681254/locations/us-central1/reasoningEngines/2112984271655272448"
25+
# operation = client.agent_engines.sandboxes.create(
26+
# # name=agent_engine.api_resource.name,
27+
# name=agent_engine_name,
28+
# spec={
29+
# "code_execution_environment": {
30+
# "machineConfig": "MACHINE_CONFIG_VCPU4_RAM4GIB"
31+
# }
32+
# },
33+
# config=types.CreateAgentEngineSandboxConfig(display_name="test_sandbox"),
34+
# )
35+
# assert isinstance(operation, types.AgentEngineSandboxOperation)
36+
37+
client._api_client.project = None
38+
client._api_client.location = None
39+
client._api_client.api_version = None
40+
token = client.agent_engines.sandboxes.generate_access_token(
41+
service_account_email="sign-verify-jwt@mariner-proxy.iam.gserviceaccount.com",
42+
sandbox_id="tenghuil-manual-sandbox-new",
43+
)
44+
response = client.agent_engines.sandboxes.send_command(
45+
http_method="GET",
46+
path="/",
47+
query_params={},
48+
access_token=token,
49+
headers={},
50+
request_dict={},
51+
sandbox_environment=None,
52+
)
53+
# assert token == "xxx"
54+
assert response.body == "Hello World"
55+
# assert response.outputs[0].mime_type == "application/json"
56+
57+
58+
pytestmark = pytest_helper.setup(
59+
file=__file__,
60+
globals_for_file=globals(),
61+
test_method="agent_engines.sandboxes.send_command",
62+
)

vertexai/_genai/sandboxes.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@
1919
import json
2020
import logging
2121
import mimetypes
22+
import random
23+
import time
2224
from typing import Any, Iterator, Optional, Union
2325
from urllib.parse import urlencode
2426

27+
from google import genai
28+
from google.cloud import iam_credentials_v1
2529
from google.genai import _api_module
2630
from google.genai import _common
31+
from google.genai import types as genai_types
2732
from google.genai._common import get_value_by_path as getv
2833
from google.genai._common import set_value_by_path as setv
2934
from google.genai.pagers import Pager
@@ -704,6 +709,52 @@ def delete(
704709
"""
705710
return self._delete(name=name, config=config)
706711

712+
def generate_access_token(
713+
self,
714+
service_account_email: str,
715+
sandbox_id: str,
716+
port: str = "8080",
717+
timeout: int = 3600,
718+
) -> str:
719+
"""Signs a JWT with a Google Cloud service account."""
720+
client = iam_credentials_v1.IAMCredentialsClient()
721+
name = f"projects/-/serviceAccounts/{service_account_email}"
722+
custom_claims = {"port": port, "sandbox_id": sandbox_id}
723+
payload = {
724+
"iat": int(time.time()),
725+
"exp": int(time.time()) + timeout,
726+
"iss": service_account_email,
727+
"nonce": random.randint(1, 1000000000),
728+
"aud": "vmaas-proxy-api", # default audience for sandbox proxy
729+
**custom_claims,
730+
}
731+
request = iam_credentials_v1.SignJwtRequest(
732+
name=name,
733+
payload=json.dumps(payload),
734+
)
735+
response = client.sign_jwt(request=request)
736+
return response.signed_jwt
737+
738+
def send_command(
739+
self,
740+
http_method: str,
741+
path: str,
742+
query_params: Any,
743+
access_token: str,
744+
headers: dict[str, str],
745+
request_dict: dict[str, object],
746+
sandbox_environment: Optional[types.SandboxEnvironment] = None,
747+
) -> str | None:
748+
"""Sends a command to the sandbox."""
749+
# TODO(tenghuil): Get connection info from sandbox environment when it's ready.
750+
endpoint = "https://test-us-central1.autopush-sandbox.vertexai.goog/" + path
751+
752+
headers = {"Authorization": f"Bearer {access_token}"}
753+
http_options = genai_types.HttpOptions(headers=headers, base_url=endpoint)
754+
http_client = genai.Client(vertexai=True, http_options=http_options)
755+
response = http_client._api_client.request(http_method, path, request_dict)
756+
return response
757+
707758

708759
class AsyncSandboxes(_api_module.BaseModule):
709760

0 commit comments

Comments
 (0)