Skip to content

Commit 62464d7

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
chore: Add a2a framework test to agentplatform.
FUTURE_COPYBARA_INTEGRATE_REVIEW=#6967 from googleapis:release-please--branches--main 4a0d1d2 PiperOrigin-RevId: 948475753
1 parent 3335750 commit 62464d7

3 files changed

Lines changed: 299 additions & 11 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.11
4+
env_vars: {
5+
key: "NOX_SESSION"
6+
value: "unit_agentplatform_a2a-3.11"
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: 32 additions & 11 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",
@@ -397,24 +398,44 @@ def unit_agentplatform_ag2(session):
397398

398399
@nox.session(python=UNIT_TEST_TEMPLATES_PYTHON_VERSIONS)
399400
def unit_agentplatform_llama_index(session):
400-
# Install all test dependencies, then install this package in-place.
401+
constraints_path = str(CURRENT_DIRECTORY / "testing" / "constraints-a2a.txt")
402+
install_unittest_dependencies(session, "-c", constraints_path)
403+
session.install("a2a-sdk", "-c", constraints_path)
401404

402-
constraints_path = str(
403-
CURRENT_DIRECTORY / "testing" / "constraints-llama-index.txt"
405+
# Run py.test against the unit tests.
406+
session.run(
407+
"py.test",
408+
"--quiet",
409+
"--junitxml=unit_agentplatform_a2a_sponge_log.xml",
410+
"--cov=google",
411+
"--cov-append",
412+
"--cov-config=.coveragerc",
413+
"--cov-report=",
414+
"--cov-fail-under=0",
415+
os.path.join(
416+
"tests",
417+
"unit",
418+
"agentplatform",
419+
"frameworks",
420+
"test_frameworks_a2a.py",
421+
),
422+
*session.posargs,
404423
)
405-
standard_deps = UNIT_TEST_STANDARD_DEPENDENCIES + UNIT_TEST_DEPENDENCIES
406-
session.install(*standard_deps, "-c", constraints_path)
407424

408-
# Install llama_index extras
409-
session.install("-e", ".[llama_index_testing]", "-c", constraints_path)
425+
426+
@nox.session(python=UNIT_TEST_TEMPLATES_PYTHON_VERSIONS)
427+
def unit_agentplatform_a2a(session):
428+
# Install all test dependencies, then install this package in-place.
429+
430+
constraints_path = str(CURRENT_DIRECTORY / "testing" / "constraints-a2a.txt")
431+
install_unittest_dependencies(session, "-c", constraints_path)
432+
session.install("a2a-sdk", "-c", constraints_path)
410433

411434
# Run py.test against the unit tests.
412435
session.run(
413436
"py.test",
414-
"-n",
415-
"auto", # Use all available CPU cores
416437
"--quiet",
417-
"--junitxml=unit_agentplatform_llama_index_sponge_log.xml",
438+
"--junitxml=unit_agentplatform_a2a_sponge_log.xml",
418439
"--cov=google",
419440
"--cov-append",
420441
"--cov-config=.coveragerc",
@@ -425,7 +446,7 @@ def unit_agentplatform_llama_index(session):
425446
"unit",
426447
"agentplatform",
427448
"frameworks",
428-
"test_frameworks_llama_index.py",
449+
"test_frameworks_a2a.py",
429450
),
430451
*session.posargs,
431452
)
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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+
21+
import cloudpickle
22+
from google import auth
23+
from google.api_core import operation as ga_operation
24+
from google.auth import credentials as auth_credentials
25+
from google.cloud import storage
26+
import agentplatform
27+
from agentplatform import agent_engines
28+
from agentplatform._genai import _agent_engines_utils
29+
from agentplatform.agent_engines import _agent_engines
30+
from google.cloud.aiplatform import base
31+
from google.cloud.aiplatform import initializer
32+
from google.cloud.aiplatform_v1 import types
33+
from google.cloud.aiplatform_v1.services import reasoning_engine_service
34+
import pytest
35+
36+
from google.protobuf import struct_pb2
37+
from google.protobuf import json_format
38+
39+
40+
class CapitalizeEngine:
41+
"""A sample Agent Engine."""
42+
43+
def query(self, unused_arbitrary_string_name: str) -> str:
44+
"""Runs the engine."""
45+
return unused_arbitrary_string_name.upper()
46+
47+
48+
class CapitalizeEngineWithCard(CapitalizeEngine):
49+
50+
def __init__(self, card):
51+
self.agent_card = card
52+
53+
def __getstate__(self):
54+
state = self.__dict__.copy()
55+
if hasattr(self.agent_card, "DESCRIPTOR"):
56+
state["agent_card"] = None
57+
return state
58+
59+
def __setstate__(self, state):
60+
self.__dict__.update(state)
61+
62+
63+
def _create_empty_fake_package(package_name: str) -> str:
64+
temp_dir = tempfile.mkdtemp()
65+
package_dir = os.path.join(temp_dir, package_name)
66+
os.makedirs(package_dir)
67+
init_path = os.path.join(package_dir, "__init__.py")
68+
open(init_path, "w").close()
69+
return temp_dir
70+
71+
72+
_TEST_CREDENTIALS = mock.Mock(spec=auth_credentials.AnonymousCredentials())
73+
_TEST_STAGING_BUCKET = "gs://test-bucket"
74+
_TEST_LOCATION = "us-central1"
75+
_TEST_PROJECT = "test-project"
76+
_TEST_RESOURCE_ID = "1028944691210842416"
77+
_TEST_PARENT = f"projects/{_TEST_PROJECT}/locations/{_TEST_LOCATION}"
78+
_TEST_AGENT_ENGINE_RESOURCE_NAME = (
79+
f"{_TEST_PARENT}/reasoningEngines/{_TEST_RESOURCE_ID}"
80+
)
81+
_TEST_AGENT_ENGINE_DISPLAY_NAME = "Agent Engine Display Name"
82+
_TEST_GCS_DIR_NAME = _agent_engines._DEFAULT_GCS_DIR_NAME
83+
_TEST_BLOB_FILENAME = _agent_engines._BLOB_FILENAME
84+
_TEST_REQUIREMENTS_FILE = _agent_engines._REQUIREMENTS_FILE
85+
_TEST_EXTRA_PACKAGES_FILE = _agent_engines._EXTRA_PACKAGES_FILE
86+
_TEST_STANDARD_API_MODE = _agent_engines._STANDARD_API_MODE
87+
_TEST_DEFAULT_METHOD_NAME = _agent_engines._DEFAULT_METHOD_NAME
88+
_TEST_MODE_KEY_IN_SCHEMA = _agent_engines._MODE_KEY_IN_SCHEMA
89+
90+
_TEST_AGENT_ENGINE_EXTRA_PACKAGE = "fake.py"
91+
92+
_TEST_AGENT_ENGINE_EXTRA_PACKAGE_PATH = _create_empty_fake_package(
93+
_TEST_AGENT_ENGINE_EXTRA_PACKAGE
94+
)
95+
96+
_TEST_AGENT_ENGINE_REQUIREMENTS = [
97+
"google-cloud-aiplatform==1.29.0",
98+
"langchain",
99+
]
100+
101+
_TEST_AGENT_ENGINE_GCS_URI = "{}/{}/{}".format(
102+
_TEST_STAGING_BUCKET,
103+
_TEST_GCS_DIR_NAME,
104+
_TEST_BLOB_FILENAME,
105+
)
106+
_TEST_AGENT_ENGINE_DEPENDENCY_FILES_GCS_URI = "{}/{}/{}".format(
107+
_TEST_STAGING_BUCKET,
108+
_TEST_GCS_DIR_NAME,
109+
_TEST_EXTRA_PACKAGES_FILE,
110+
)
111+
_TEST_AGENT_ENGINE_REQUIREMENTS_GCS_URI = "{}/{}/{}".format(
112+
_TEST_STAGING_BUCKET,
113+
_TEST_GCS_DIR_NAME,
114+
_TEST_REQUIREMENTS_FILE,
115+
)
116+
117+
_TEST_AGENT_ENGINE_QUERY_SCHEMA = _agent_engines_utils._to_proto(
118+
_agent_engines_utils._generate_schema(
119+
CapitalizeEngine().query,
120+
schema_name=_TEST_DEFAULT_METHOD_NAME,
121+
)
122+
)
123+
_TEST_AGENT_ENGINE_QUERY_SCHEMA[_TEST_MODE_KEY_IN_SCHEMA] = (
124+
_TEST_STANDARD_API_MODE
125+
)
126+
127+
_TEST_AGENT_ENGINE_PACKAGE_SPEC = types.ReasoningEngineSpec.PackageSpec(
128+
python_version=f"{sys.version_info.major}.{sys.version_info.minor}",
129+
pickle_object_gcs_uri=_TEST_AGENT_ENGINE_GCS_URI,
130+
dependency_files_gcs_uri=_TEST_AGENT_ENGINE_DEPENDENCY_FILES_GCS_URI,
131+
requirements_gcs_uri=_TEST_AGENT_ENGINE_REQUIREMENTS_GCS_URI,
132+
)
133+
134+
_TEST_AGENT_ENGINE_OBJ = types.ReasoningEngine(
135+
name=_TEST_AGENT_ENGINE_RESOURCE_NAME,
136+
spec=types.ReasoningEngineSpec(
137+
package_spec=_TEST_AGENT_ENGINE_PACKAGE_SPEC,
138+
agent_framework=_agent_engines._DEFAULT_AGENT_FRAMEWORK,
139+
),
140+
)
141+
_TEST_AGENT_ENGINE_OBJ.spec.class_methods.append(
142+
_TEST_AGENT_ENGINE_QUERY_SCHEMA
143+
)
144+
145+
146+
@pytest.fixture(scope="module")
147+
def google_auth_mock():
148+
with mock.patch.object(auth, "default") as google_auth_mock:
149+
google_auth_mock.return_value = (
150+
auth_credentials.AnonymousCredentials(),
151+
_TEST_PROJECT,
152+
)
153+
yield google_auth_mock
154+
155+
156+
@pytest.fixture(scope="module")
157+
def cloud_storage_create_bucket_mock():
158+
with mock.patch.object(storage, "Client") as cloud_storage_mock:
159+
bucket_mock = mock.Mock(spec=storage.Bucket)
160+
bucket_mock.blob.return_value.open.return_value = "blob_file"
161+
bucket_mock.blob.return_value.upload_from_filename.return_value = None
162+
bucket_mock.blob.return_value.upload_from_string.return_value = None
163+
164+
cloud_storage_mock.get_bucket = mock.Mock(
165+
side_effect=ValueError("bucket not found")
166+
)
167+
cloud_storage_mock.bucket.return_value = bucket_mock
168+
cloud_storage_mock.create_bucket.return_value = bucket_mock
169+
170+
yield cloud_storage_mock
171+
172+
173+
@pytest.fixture(scope="module")
174+
def cloudpickle_load_mock():
175+
with mock.patch.object(cloudpickle, "load") as cloudpickle_load_mock:
176+
yield cloudpickle_load_mock
177+
178+
179+
@pytest.fixture(scope="module")
180+
def create_agent_engine_mock():
181+
with mock.patch.object(
182+
reasoning_engine_service.ReasoningEngineServiceClient,
183+
"create_reasoning_engine",
184+
) as create_agent_engine_mock:
185+
create_agent_engine_lro_mock = mock.Mock(spec=ga_operation.Operation)
186+
create_agent_engine_lro_mock.result.return_value = _TEST_AGENT_ENGINE_OBJ
187+
create_agent_engine_mock.return_value = create_agent_engine_lro_mock
188+
yield create_agent_engine_mock
189+
190+
191+
@pytest.fixture(scope="function")
192+
def get_gca_resource_mock():
193+
with mock.patch.object(
194+
base.VertexAiResourceNoun,
195+
"_get_gca_resource",
196+
) as get_gca_resource_mock:
197+
get_gca_resource_mock.return_value = _TEST_AGENT_ENGINE_OBJ
198+
yield get_gca_resource_mock
199+
200+
201+
@pytest.mark.usefixtures("google_auth_mock")
202+
class TestAgentEngineA2A:
203+
204+
def setup_method(self):
205+
agentplatform.init(
206+
project=_TEST_PROJECT,
207+
location=_TEST_LOCATION,
208+
credentials=_TEST_CREDENTIALS,
209+
staging_bucket=_TEST_STAGING_BUCKET,
210+
)
211+
212+
def teardown_method(self):
213+
initializer.global_pool.shutdown(wait=True)
214+
215+
def test_create_agent_engine_with_protobuf_agent_card(
216+
self,
217+
create_agent_engine_mock,
218+
cloud_storage_create_bucket_mock,
219+
cloudpickle_load_mock,
220+
get_gca_resource_mock,
221+
):
222+
try:
223+
# pylint: disable-next=g-import-not-at-top
224+
from google3.third_party.a2a.specification.grpc import a2a_pb2
225+
except (ImportError, TypeError):
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+
246+
expected_class_method = struct_pb2.Struct()
247+
expected_class_method.CopyFrom(_TEST_AGENT_ENGINE_QUERY_SCHEMA)
248+
expected_class_method["a2a_agent_card"] = json_format.MessageToJson(card)
249+
expected_reasoning_engine.spec.class_methods.append(expected_class_method)
250+
251+
create_agent_engine_mock.assert_called_with(
252+
parent=_TEST_PARENT,
253+
reasoning_engine=expected_reasoning_engine,
254+
)

0 commit comments

Comments
 (0)