Skip to content

Commit 62bc783

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
chore: Add a2a framework test to agentplatform.
PiperOrigin-RevId: 949631389
1 parent a3179c2 commit 62bc783

3 files changed

Lines changed: 318 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Format: //devtools/kokoro/config/proto/build.proto
2+
3+
# Run unit tests for A2A on Python 3.14
4+
env_vars: {
5+
key: "NOX_SESSION"
6+
value: "unit_agentplatform_a2a-3.14"
7+
}
8+
9+
# Run unit tests in parallel, splitting up by file
10+
env_vars: {
11+
key: "PYTEST_ADDOPTS"
12+
value: "-n=auto --dist=loadscope"
13+
}

noxfile.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
"unit_agentplatform_langchain",
111111
"unit_agentplatform_ag2",
112112
"unit_agentplatform_llama_index",
113+
"unit_agentplatform_a2a",
113114
"unit_a2a",
114115
"system",
115116
"cover",
@@ -431,6 +432,35 @@ def unit_agentplatform_llama_index(session):
431432
)
432433

433434

435+
@nox.session(python=["3.14"])
436+
def unit_agentplatform_a2a(session):
437+
# Install all test dependencies, then install this package in-place.
438+
439+
constraints_path = str(CURRENT_DIRECTORY / "testing" / "constraints-a2a.txt")
440+
install_unittest_dependencies(session, "-c", constraints_path)
441+
session.install("a2a-sdk", "-c", constraints_path)
442+
443+
# Run py.test against the unit tests.
444+
session.run(
445+
"py.test",
446+
"--quiet",
447+
"--junitxml=unit_agentplatform_a2a_sponge_log.xml",
448+
"--cov=google",
449+
"--cov-append",
450+
"--cov-config=.coveragerc",
451+
"--cov-report=",
452+
"--cov-fail-under=0",
453+
os.path.join(
454+
"tests",
455+
"unit",
456+
"agentplatform",
457+
"frameworks",
458+
"test_frameworks_a2a.py",
459+
),
460+
*session.posargs,
461+
)
462+
463+
434464
@nox.session(python=["3.14"])
435465
def unit_a2a(session):
436466
# Install all test dependencies, then install this package in-place.
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
# Copyright 2026 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+
16+
import os
17+
import sys
18+
import tempfile
19+
from unittest import mock
20+
import pytest
21+
import cloudpickle
22+
import pydantic
23+
24+
from google import auth
25+
from google.api_core import operation as ga_operation
26+
from google.auth import credentials as auth_credentials
27+
from google.cloud import storage
28+
from google.cloud import aiplatform
29+
from google.cloud.aiplatform import base
30+
31+
from google.cloud.aiplatform_v1 import types
32+
from google.cloud.aiplatform_v1.services import reasoning_engine_service
33+
from vertexai import agent_engines
34+
from vertexai.agent_engines import _agent_engines
35+
from vertexai.agent_engines import _utils
36+
from google.protobuf import struct_pb2
37+
38+
39+
class CapitalizeEngine:
40+
"""A sample Agent Engine."""
41+
42+
def query(self, unused_arbitrary_string_name: str) -> str:
43+
"""Runs the engine."""
44+
return unused_arbitrary_string_name.upper()
45+
46+
47+
class CapitalizeEngineWithCard(CapitalizeEngine):
48+
49+
def __init__(self, card):
50+
self.agent_card = card
51+
52+
def __getstate__(self):
53+
state = self.__dict__.copy()
54+
if hasattr(self.agent_card, "DESCRIPTOR"):
55+
state["agent_card"] = None
56+
return state
57+
58+
def __setstate__(self, state):
59+
self.__dict__.update(state)
60+
61+
62+
def _create_empty_fake_package(package_name: str) -> str:
63+
temp_dir = tempfile.mkdtemp()
64+
package_dir = os.path.join(temp_dir, package_name)
65+
os.makedirs(package_dir)
66+
init_path = os.path.join(package_dir, "__init__.py")
67+
open(init_path, "w").close()
68+
return temp_dir
69+
70+
71+
_TEST_CREDENTIALS = mock.Mock(spec=auth_credentials.AnonymousCredentials())
72+
_TEST_STAGING_BUCKET = "gs://test-bucket"
73+
_TEST_LOCATION = "us-central1"
74+
_TEST_PROJECT = "test-project"
75+
_TEST_RESOURCE_ID = "1028944691210842416"
76+
_TEST_PARENT = f"projects/{_TEST_PROJECT}/locations/{_TEST_LOCATION}"
77+
_TEST_AGENT_ENGINE_RESOURCE_NAME = (
78+
f"{_TEST_PARENT}/reasoningEngines/{_TEST_RESOURCE_ID}"
79+
)
80+
_TEST_AGENT_ENGINE_DISPLAY_NAME = "Agent Engine Display Name"
81+
_TEST_GCS_DIR_NAME = _agent_engines._DEFAULT_GCS_DIR_NAME
82+
_TEST_BLOB_FILENAME = _agent_engines._BLOB_FILENAME
83+
_TEST_REQUIREMENTS_FILE = _agent_engines._REQUIREMENTS_FILE
84+
_TEST_EXTRA_PACKAGES_FILE = _agent_engines._EXTRA_PACKAGES_FILE
85+
_TEST_STANDARD_API_MODE = _agent_engines._STANDARD_API_MODE
86+
_TEST_DEFAULT_METHOD_NAME = _agent_engines._DEFAULT_METHOD_NAME
87+
_TEST_MODE_KEY_IN_SCHEMA = _agent_engines._MODE_KEY_IN_SCHEMA
88+
89+
_TEST_AGENT_ENGINE_EXTRA_PACKAGE = "fake.py"
90+
91+
_TEST_AGENT_ENGINE_EXTRA_PACKAGE_PATH = _create_empty_fake_package(
92+
_TEST_AGENT_ENGINE_EXTRA_PACKAGE
93+
)
94+
95+
_TEST_AGENT_ENGINE_REQUIREMENTS = [
96+
"google-cloud-aiplatform==1.29.0",
97+
"langchain",
98+
]
99+
100+
_TEST_AGENT_ENGINE_GCS_URI = "{}/{}/{}".format(
101+
_TEST_STAGING_BUCKET,
102+
_TEST_GCS_DIR_NAME,
103+
_TEST_BLOB_FILENAME,
104+
)
105+
_TEST_AGENT_ENGINE_DEPENDENCY_FILES_GCS_URI = "{}/{}/{}".format(
106+
_TEST_STAGING_BUCKET,
107+
_TEST_GCS_DIR_NAME,
108+
_TEST_EXTRA_PACKAGES_FILE,
109+
)
110+
_TEST_AGENT_ENGINE_REQUIREMENTS_GCS_URI = "{}/{}/{}".format(
111+
_TEST_STAGING_BUCKET,
112+
_TEST_GCS_DIR_NAME,
113+
_TEST_REQUIREMENTS_FILE,
114+
)
115+
116+
_TEST_AGENT_ENGINE_QUERY_SCHEMA = _utils.to_proto(
117+
_utils.generate_schema(
118+
CapitalizeEngine().query,
119+
schema_name=_TEST_DEFAULT_METHOD_NAME,
120+
)
121+
)
122+
_TEST_AGENT_ENGINE_QUERY_SCHEMA[_TEST_MODE_KEY_IN_SCHEMA] = _TEST_STANDARD_API_MODE
123+
124+
_TEST_AGENT_ENGINE_PACKAGE_SPEC = types.ReasoningEngineSpec.PackageSpec(
125+
python_version=f"{sys.version_info.major}.{sys.version_info.minor}",
126+
pickle_object_gcs_uri=_TEST_AGENT_ENGINE_GCS_URI,
127+
dependency_files_gcs_uri=_TEST_AGENT_ENGINE_DEPENDENCY_FILES_GCS_URI,
128+
requirements_gcs_uri=_TEST_AGENT_ENGINE_REQUIREMENTS_GCS_URI,
129+
)
130+
131+
_TEST_AGENT_ENGINE_OBJ = types.ReasoningEngine(
132+
name=_TEST_AGENT_ENGINE_RESOURCE_NAME,
133+
spec=types.ReasoningEngineSpec(
134+
package_spec=_TEST_AGENT_ENGINE_PACKAGE_SPEC,
135+
agent_framework=_agent_engines._DEFAULT_AGENT_FRAMEWORK,
136+
),
137+
)
138+
_TEST_AGENT_ENGINE_OBJ.spec.class_methods.append(_TEST_AGENT_ENGINE_QUERY_SCHEMA)
139+
140+
141+
@pytest.fixture(scope="module")
142+
def google_auth_mock():
143+
with mock.patch.object(auth, "default") as google_auth_mock:
144+
google_auth_mock.return_value = (
145+
auth_credentials.AnonymousCredentials(),
146+
_TEST_PROJECT,
147+
)
148+
yield google_auth_mock
149+
150+
151+
@pytest.fixture(scope="module")
152+
def cloud_storage_create_bucket_mock():
153+
with mock.patch.object(storage, "Client") as cloud_storage_mock:
154+
bucket_mock = mock.Mock(spec=storage.Bucket)
155+
bucket_mock.blob.return_value.open.return_value = "blob_file"
156+
bucket_mock.blob.return_value.upload_from_filename.return_value = None
157+
bucket_mock.blob.return_value.upload_from_string.return_value = None
158+
159+
cloud_storage_mock.get_bucket = mock.Mock(
160+
side_effect=ValueError("bucket not found")
161+
)
162+
cloud_storage_mock.bucket.return_value = bucket_mock
163+
cloud_storage_mock.create_bucket.return_value = bucket_mock
164+
165+
yield cloud_storage_mock
166+
167+
168+
@pytest.fixture(scope="module")
169+
def cloudpickle_load_mock():
170+
with mock.patch.object(cloudpickle, "load") as cloudpickle_load_mock:
171+
yield cloudpickle_load_mock
172+
173+
174+
@pytest.fixture(scope="module")
175+
def create_agent_engine_mock():
176+
with mock.patch.object(
177+
reasoning_engine_service.ReasoningEngineServiceClient,
178+
"create_reasoning_engine",
179+
) as create_agent_engine_mock:
180+
create_agent_engine_lro_mock = mock.Mock(spec=ga_operation.Operation)
181+
create_agent_engine_lro_mock.result.return_value = _TEST_AGENT_ENGINE_OBJ
182+
create_agent_engine_mock.return_value = create_agent_engine_lro_mock
183+
yield create_agent_engine_mock
184+
185+
186+
@pytest.fixture(scope="function")
187+
def get_gca_resource_mock():
188+
with mock.patch.object(
189+
base.VertexAiResourceNoun,
190+
"_get_gca_resource",
191+
) as get_gca_resource_mock:
192+
get_gca_resource_mock.return_value = _TEST_AGENT_ENGINE_OBJ
193+
yield get_gca_resource_mock
194+
195+
196+
@pytest.mark.usefixtures("google_auth_mock")
197+
class TestAgentEngineA2A:
198+
def setup_method(self):
199+
aiplatform.init(
200+
project=_TEST_PROJECT,
201+
location=_TEST_LOCATION,
202+
credentials=_TEST_CREDENTIALS,
203+
staging_bucket=_TEST_STAGING_BUCKET,
204+
)
205+
206+
def test_create_agent_engine_with_protobuf_agent_card(
207+
self,
208+
create_agent_engine_mock,
209+
cloud_storage_create_bucket_mock,
210+
cloudpickle_load_mock,
211+
get_gca_resource_mock,
212+
):
213+
a2a_pb2 = None
214+
# fmt: off
215+
try:
216+
try:
217+
from a2a.compat.v0_3 import a2a_v0_3_pb2 as a2a_pb2
218+
except ImportError:
219+
from a2a.grpc import a2a_pb2
220+
has_a2a_pb2 = True
221+
except (ImportError, TypeError):
222+
has_a2a_pb2 = False
223+
# fmt: on
224+
225+
if not has_a2a_pb2:
226+
pytest.skip("a2a_pb2 could not be imported.")
227+
228+
card = a2a_pb2.AgentCard(name="test_agent_card")
229+
agent = CapitalizeEngineWithCard(card)
230+
231+
agent_engines.create(
232+
agent,
233+
display_name=_TEST_AGENT_ENGINE_DISPLAY_NAME,
234+
requirements=_TEST_AGENT_ENGINE_REQUIREMENTS,
235+
extra_packages=[_TEST_AGENT_ENGINE_EXTRA_PACKAGE_PATH],
236+
)
237+
238+
expected_reasoning_engine = types.ReasoningEngine(
239+
display_name=_TEST_AGENT_ENGINE_DISPLAY_NAME,
240+
spec=types.ReasoningEngineSpec(
241+
package_spec=_TEST_AGENT_ENGINE_PACKAGE_SPEC,
242+
agent_framework=_agent_engines._DEFAULT_AGENT_FRAMEWORK,
243+
),
244+
)
245+
from google.protobuf import json_format
246+
247+
expected_class_method = struct_pb2.Struct()
248+
expected_class_method.CopyFrom(_TEST_AGENT_ENGINE_QUERY_SCHEMA)
249+
expected_class_method["a2a_agent_card"] = json_format.MessageToJson(card)
250+
expected_reasoning_engine.spec.class_methods.append(expected_class_method)
251+
252+
create_agent_engine_mock.assert_called_with(
253+
parent=_TEST_PARENT,
254+
reasoning_engine=expected_reasoning_engine,
255+
)
256+
257+
def test_create_agent_engine_with_invalid_agent_card(
258+
self,
259+
create_agent_engine_mock,
260+
cloud_storage_create_bucket_mock,
261+
cloudpickle_load_mock,
262+
get_gca_resource_mock,
263+
):
264+
agent = CapitalizeEngineWithCard(card="invalid_card_type_string")
265+
266+
with pytest.raises(
267+
TypeError,
268+
match="Unsupported AgentCard type",
269+
):
270+
agent_engines.create(
271+
agent,
272+
display_name=_TEST_AGENT_ENGINE_DISPLAY_NAME,
273+
requirements=_TEST_AGENT_ENGINE_REQUIREMENTS,
274+
extra_packages=[_TEST_AGENT_ENGINE_EXTRA_PACKAGE_PATH],
275+
)

0 commit comments

Comments
 (0)