Skip to content

Commit 1be5e05

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
fix: resolve AttributeError by supporting both Pydantic and Protobuf AgentCard serialization
PiperOrigin-RevId: 918921714
1 parent 116b71c commit 1be5e05

4 files changed

Lines changed: 194 additions & 14 deletions

File tree

tests/unit/vertex_langchain/test_agent_engines.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,130 @@ def test_create_agent_engine(
10611061
retry=_TEST_RETRY,
10621062
)
10631063

1064+
def test_create_agent_engine_with_protobuf_agent_card(
1065+
self,
1066+
create_agent_engine_mock,
1067+
cloud_storage_create_bucket_mock,
1068+
tarfile_open_mock,
1069+
cloudpickle_dump_mock,
1070+
cloudpickle_load_mock,
1071+
importlib_metadata_version_mock,
1072+
get_agent_engine_mock,
1073+
get_gca_resource_mock,
1074+
):
1075+
class CapitalizeEngineWithCard(CapitalizeEngine):
1076+
def __init__(self, card):
1077+
self.agent_card = card
1078+
1079+
from google.protobuf import struct_pb2
1080+
1081+
card = struct_pb2.Struct()
1082+
card["name"] = "test_agent_card"
1083+
agent = CapitalizeEngineWithCard(card)
1084+
1085+
agent_engines.create(
1086+
agent,
1087+
display_name=_TEST_AGENT_ENGINE_DISPLAY_NAME,
1088+
requirements=_TEST_AGENT_ENGINE_REQUIREMENTS,
1089+
extra_packages=[_TEST_AGENT_ENGINE_EXTRA_PACKAGE_PATH],
1090+
)
1091+
1092+
expected_reasoning_engine = types.ReasoningEngine(
1093+
display_name=_TEST_AGENT_ENGINE_DISPLAY_NAME,
1094+
spec=types.ReasoningEngineSpec(
1095+
package_spec=_TEST_AGENT_ENGINE_PACKAGE_SPEC,
1096+
agent_framework=_agent_engines._DEFAULT_AGENT_FRAMEWORK,
1097+
),
1098+
)
1099+
from google.protobuf import json_format
1100+
1101+
expected_class_method = struct_pb2.Struct()
1102+
expected_class_method.CopyFrom(_TEST_AGENT_ENGINE_QUERY_SCHEMA)
1103+
expected_class_method["a2a_agent_card"] = json_format.MessageToJson(card)
1104+
expected_reasoning_engine.spec.class_methods.append(expected_class_method)
1105+
1106+
create_agent_engine_mock.assert_called_with(
1107+
parent=_TEST_PARENT,
1108+
reasoning_engine=expected_reasoning_engine,
1109+
)
1110+
1111+
def test_create_agent_engine_with_pydantic_agent_card(
1112+
self,
1113+
create_agent_engine_mock,
1114+
cloud_storage_create_bucket_mock,
1115+
tarfile_open_mock,
1116+
cloudpickle_dump_mock,
1117+
cloudpickle_load_mock,
1118+
importlib_metadata_version_mock,
1119+
get_agent_engine_mock,
1120+
get_gca_resource_mock,
1121+
):
1122+
class CapitalizeEngineWithCard(CapitalizeEngine):
1123+
def __init__(self, card):
1124+
self.agent_card = card
1125+
1126+
import pydantic
1127+
1128+
class DummyPydanticCard(pydantic.BaseModel):
1129+
name: str = "test_pydantic_card"
1130+
1131+
card = DummyPydanticCard()
1132+
agent = CapitalizeEngineWithCard(card)
1133+
1134+
agent_engines.create(
1135+
agent,
1136+
display_name=_TEST_AGENT_ENGINE_DISPLAY_NAME,
1137+
requirements=_TEST_AGENT_ENGINE_REQUIREMENTS,
1138+
extra_packages=[_TEST_AGENT_ENGINE_EXTRA_PACKAGE_PATH],
1139+
)
1140+
1141+
expected_reasoning_engine = types.ReasoningEngine(
1142+
display_name=_TEST_AGENT_ENGINE_DISPLAY_NAME,
1143+
spec=types.ReasoningEngineSpec(
1144+
package_spec=_TEST_AGENT_ENGINE_PACKAGE_SPEC,
1145+
agent_framework=_agent_engines._DEFAULT_AGENT_FRAMEWORK,
1146+
),
1147+
)
1148+
from google.protobuf import struct_pb2
1149+
1150+
expected_class_method = struct_pb2.Struct()
1151+
expected_class_method.CopyFrom(_TEST_AGENT_ENGINE_QUERY_SCHEMA)
1152+
expected_class_method["a2a_agent_card"] = card.model_dump_json()
1153+
expected_reasoning_engine.spec.class_methods.append(expected_class_method)
1154+
1155+
create_agent_engine_mock.assert_called_with(
1156+
parent=_TEST_PARENT,
1157+
reasoning_engine=expected_reasoning_engine,
1158+
)
1159+
1160+
def test_create_agent_engine_with_invalid_agent_card(
1161+
self,
1162+
create_agent_engine_mock,
1163+
cloud_storage_create_bucket_mock,
1164+
tarfile_open_mock,
1165+
cloudpickle_dump_mock,
1166+
cloudpickle_load_mock,
1167+
importlib_metadata_version_mock,
1168+
get_agent_engine_mock,
1169+
get_gca_resource_mock,
1170+
):
1171+
class CapitalizeEngineWithCard(CapitalizeEngine):
1172+
def __init__(self, card):
1173+
self.agent_card = card
1174+
1175+
agent = CapitalizeEngineWithCard(card="invalid_card_type_string")
1176+
1177+
with pytest.raises(
1178+
TypeError,
1179+
match="Unsupported AgentCard type",
1180+
):
1181+
agent_engines.create(
1182+
agent,
1183+
display_name=_TEST_AGENT_ENGINE_DISPLAY_NAME,
1184+
requirements=_TEST_AGENT_ENGINE_REQUIREMENTS,
1185+
extra_packages=[_TEST_AGENT_ENGINE_EXTRA_PACKAGE_PATH],
1186+
)
1187+
10641188
def test_create_agent_engine_requirements_from_file(
10651189
self,
10661190
create_agent_engine_mock,

vertexai/_genai/_agent_engines_utils.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -652,10 +652,9 @@ def _generate_class_methods_spec_or_raise(
652652

653653
class_method = _to_proto(schema_dict)
654654
class_method[_MODE_KEY_IN_SCHEMA] = mode
655-
if hasattr(agent, "agent_card"):
656-
class_method[_A2A_AGENT_CARD] = json_format.MessageToJson(
657-
getattr(agent, "agent_card")
658-
)
655+
card = getattr(agent, "agent_card", None)
656+
if card is not None:
657+
class_method[_A2A_AGENT_CARD] = _serialize_agent_card_to_json(card)
659658
class_methods_spec.append(class_method)
660659

661660
return class_methods_spec
@@ -2148,3 +2147,59 @@ def _add_telemetry_enablement_env(
21482147
return env_vars
21492148

21502149
return env_vars | env_to_add
2150+
2151+
2152+
def _serialize_agent_card_to_dict(card: Any) -> Optional[Dict[str, Any]]:
2153+
"""Validates and serializes an AgentCard to a dictionary representation.
2154+
2155+
Args:
2156+
card: The AgentCard instance (Pydantic model or Protobuf Message).
2157+
2158+
Returns:
2159+
The serialized card as a dictionary.
2160+
2161+
Raises:
2162+
TypeError: If the card type is not supported.
2163+
"""
2164+
if card is None:
2165+
return None
2166+
2167+
if hasattr(card, "model_dump"):
2168+
return typing.cast(dict[str, Any], card.model_dump(exclude_none=True))
2169+
elif hasattr(card, "DESCRIPTOR"):
2170+
from google.protobuf import json_format
2171+
2172+
return typing.cast(dict[str, Any], json_format.MessageToDict(card))
2173+
else:
2174+
raise TypeError(
2175+
f"Unsupported AgentCard type: {type(card)}. "
2176+
"Only Pydantic models and Protobuf Messages are supported."
2177+
)
2178+
2179+
2180+
def _serialize_agent_card_to_json(card: Any) -> Optional[str]:
2181+
"""Validates and serializes an AgentCard to a JSON string representation.
2182+
2183+
Args:
2184+
card: The AgentCard instance (Pydantic model or Protobuf Message).
2185+
2186+
Returns:
2187+
The serialized card as a JSON string.
2188+
2189+
Raises:
2190+
TypeError: If the card type is not supported.
2191+
"""
2192+
if card is None:
2193+
return None
2194+
2195+
if hasattr(card, "model_dump_json"):
2196+
return typing.cast(str, card.model_dump_json())
2197+
elif hasattr(card, "DESCRIPTOR"):
2198+
from google.protobuf import json_format
2199+
2200+
return typing.cast(str, json_format.MessageToJson(card))
2201+
else:
2202+
raise TypeError(
2203+
f"Unsupported AgentCard type: {type(card)}. "
2204+
"Only Pydantic models and Protobuf Messages are supported."
2205+
)

vertexai/_genai/agent_engines.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2498,12 +2498,12 @@ def _create_config(
24982498

24992499
if hasattr(agent, "agent_card"):
25002500
agent_card = getattr(agent, "agent_card")
2501-
if agent_card:
2501+
if agent_card is not None:
25022502
try:
2503-
from google.protobuf import json_format
2504-
2505-
agent_engine_spec["agent_card"] = json_format.MessageToDict(
2506-
agent_card
2503+
agent_engine_spec["agent_card"] = (
2504+
_agent_engines_utils._serialize_agent_card_to_dict(
2505+
agent_card
2506+
)
25072507
)
25082508
except Exception as e:
25092509
raise ValueError(

vertexai/agent_engines/_agent_engines.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from google.cloud.aiplatform_v1 import types as aip_types
4747
from google.cloud.aiplatform_v1.types import reasoning_engine_service
4848
from vertexai.agent_engines import _utils
49+
from vertexai._genai import _agent_engines_utils
4950
import httpx
5051
import proto
5152

@@ -1997,11 +1998,11 @@ def _generate_class_methods_spec_or_raise(
19971998
class_method[_MODE_KEY_IN_SCHEMA] = mode
19981999
# A2A agent card is a special case, when running in A2A mode,
19992000
if hasattr(agent_engine, "agent_card"):
2000-
from google.protobuf import json_format
2001-
2002-
class_method[_A2A_AGENT_CARD] = json_format.MessageToJson(
2003-
getattr(agent_engine, "agent_card")
2004-
)
2001+
card = getattr(agent_engine, "agent_card")
2002+
if card is not None:
2003+
class_method[_A2A_AGENT_CARD] = (
2004+
_agent_engines_utils._serialize_agent_card_to_json(card)
2005+
)
20052006
class_methods_spec.append(class_method)
20062007

20072008
return class_methods_spec

0 commit comments

Comments
 (0)