Skip to content

Commit 5933766

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
fix: resolve AttributeError by supporting both Pydantic and Protobuf AgentCard serialization
PiperOrigin-RevId: 927226105
1 parent 34a2650 commit 5933766

7 files changed

Lines changed: 557 additions & 80 deletions

File tree

agentplatform/_genai/_agent_engines_utils.py

Lines changed: 140 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,30 @@
111111

112112

113113
try:
114-
from a2a.types import AgentCard
114+
from a2a.types import (
115+
AgentCard,
116+
TransportProtocol,
117+
Message,
118+
TaskIdParams,
119+
TaskQueryParams,
120+
)
115121
from a2a.client import ClientConfig, ClientFactory
116-
from a2a.utils.constants import TransportProtocol
122+
123+
AgentCard = AgentCard
124+
TransportProtocol = TransportProtocol
125+
Message = Message
126+
ClientConfig = ClientConfig
127+
ClientFactory = ClientFactory
128+
TaskIdParams = TaskIdParams
129+
TaskQueryParams = TaskQueryParams
117130
except (ImportError, AttributeError):
118131
AgentCard = None
119132
TransportProtocol = None
133+
Message = None
120134
ClientConfig = None
121135
ClientFactory = None
122-
SendMessageRequest = None
123-
GetTaskRequest = None
124-
CancelTaskRequest = None
125-
GetExtendedAgentCardRequest = None
136+
TaskIdParams = None
137+
TaskQueryParams = None
126138
try:
127139
from autogen.agentchat import chat
128140

@@ -653,9 +665,9 @@ def _generate_class_methods_spec_or_raise(
653665
class_method = _to_proto(schema_dict)
654666
class_method[_MODE_KEY_IN_SCHEMA] = mode
655667
if hasattr(agent, "agent_card"):
656-
class_method[_A2A_AGENT_CARD] = json_format.MessageToJson(
657-
getattr(agent, "agent_card")
658-
)
668+
card = getattr(agent, "agent_card")
669+
if card is not None:
670+
class_method[_A2A_AGENT_CARD] = _serialize_agent_card_to_json(card)
659671
class_methods_spec.append(class_method)
660672

661673
return class_methods_spec
@@ -1795,53 +1807,79 @@ def _wrap_a2a_operation(method_name: str, agent_card: str) -> Callable[..., list
17951807
Args:
17961808
method_name: The name of the Agent Engine method to call.
17971809
agent_card: The agent card to use for the A2A API call.
1798-
Example: { 'name': 'Sample Agent', 'description': ( 'A helpful
1799-
assistant agent that can answer questions.' ),
1800-
'supportedInterfaces': [{ 'url': 'http://localhost:8080/a2a/rest/',
1801-
'protocolBinding': 'HTTP+JSON', 'protocolVersion': '1.0', }],
1802-
'version': '1.0.0', 'capabilities': { 'streaming': True,
1803-
'pushNotifications': False, 'extendedAgentCard': True, },
1804-
'defaultInputModes': ['text'], 'defaultOutputModes': ['text'],
1805-
'skills': [{ 'id': 'question_answer', 'name': 'Q&A Agent',
1806-
'description': ( 'A helpful assistant agent that can answer
1807-
questions.' ), 'tags': ['Question-Answer'], 'examples': [ 'Who is
1808-
leading 2025 F1 Standings?', 'Where can i find an active volcano?',
1809-
], 'inputModes': ['text'], 'outputModes': ['text'], }], }
1810-
1810+
Example:
1811+
{'additionalInterfaces': None,
1812+
'capabilities': {'extensions': None,
1813+
'pushNotifications': None,
1814+
'stateTransitionHistory': None,
1815+
'streaming': False},
1816+
'defaultInputModes': ['text'],
1817+
'defaultOutputModes': ['text'],
1818+
'description': (
1819+
'A helpful assistant agent that can answer questions.'
1820+
),
1821+
'documentationUrl': None,
1822+
'iconUrl': None,
1823+
'name': 'Q&A Agent',
1824+
'preferredTransport': 'JSONRPC',
1825+
'protocolVersion': '0.3.0',
1826+
'provider': None,
1827+
'security': None,
1828+
'securitySchemes': None,
1829+
'signatures': None,
1830+
'skills': [{
1831+
'description': (
1832+
'A helpful assistant agent that can answer questions.'
1833+
),
1834+
'examples': ['Who is leading 2025 F1 Standings?',
1835+
'Where can i find an active volcano?'],
1836+
'id': 'question_answer',
1837+
'inputModes': None,
1838+
'name': 'Q&A Agent',
1839+
'outputModes': None,
1840+
'security': None,
1841+
'tags': ['Question-Answer']}],
1842+
'supportsAuthenticatedExtendedCard': True,
1843+
'url': 'http://localhost:8080/',
1844+
'version': '1.0.0'}
18111845
Returns:
18121846
A callable object that executes the method on the Agent Engine via
18131847
the A2A API.
18141848
"""
18151849

18161850
async def _method(self, **kwargs) -> Any: # type: ignore[no-untyped-def]
1851+
"""Wraps an Agent Engine method, creating a callable for A2A API."""
18171852
if not self.api_client:
18181853
raise ValueError("api_client is not initialized.")
18191854
if not self.api_resource:
18201855
raise ValueError("api_resource is not initialized.")
1856+
a2a_agent_card = AgentCard(**json.loads(agent_card))
1857+
# A2A + AE integration currently only supports Rest API.
1858+
if (
1859+
a2a_agent_card.preferred_transport
1860+
and a2a_agent_card.preferred_transport != TransportProtocol.http_json
1861+
):
1862+
raise ValueError(
1863+
"Only HTTP+JSON is supported for preferred transport on agent card "
1864+
)
18211865

1822-
a2a_agent_card = AgentCard()
1823-
json_format.ParseDict(
1824-
json.loads(agent_card), a2a_agent_card, ignore_unknown_fields=True
1825-
)
1866+
# Set preferred transport to HTTP+JSON if not set.
1867+
if not hasattr(a2a_agent_card, "preferred_transport"):
1868+
a2a_agent_card.preferred_transport = TransportProtocol.http_json
18261869

1827-
if a2a_agent_card.supported_interfaces:
1828-
interface = a2a_agent_card.supported_interfaces[0]
1829-
if interface.protocol_binding != TransportProtocol.HTTP_JSON:
1830-
raise ValueError(
1831-
"Only HTTP+JSON is supported for preferred transport on agent card"
1832-
)
1833-
else:
1834-
raise ValueError("Agent card does not define any supported interfaces.")
1870+
if not hasattr(a2a_agent_card.capabilities, "streaming"):
1871+
a2a_agent_card.capabilities.streaming = False
18351872

1873+
# agent_card is set on the class_methods before set_up is invoked.
1874+
# Ensure that the agent_card url is set correctly before the client is created.
18361875
base_url = self.api_client._api_client._http_options.base_url.rstrip("/")
18371876
api_version = self.api_client._api_client._http_options.api_version
1838-
a2a_agent_card.supported_interfaces[0].url = (
1839-
f"{base_url}/{api_version}/{self.api_resource.name}/a2a"
1840-
)
1877+
a2a_agent_card.url = f"{base_url}/{api_version}/{self.api_resource.name}/a2a"
18411878

1879+
# Using a2a client, inject the auth token from the global config.
18421880
config = ClientConfig(
1843-
supported_protocol_bindings=[
1844-
TransportProtocol.HTTP_JSON,
1881+
supported_transports=[
1882+
TransportProtocol.http_json,
18451883
],
18461884
use_client_preference=True,
18471885
httpx_client=httpx.AsyncClient(
@@ -1860,34 +1898,23 @@ async def _method(self, **kwargs) -> Any: # type: ignore[no-untyped-def]
18601898
factory = ClientFactory(config)
18611899
client = factory.create(a2a_agent_card)
18621900

1863-
context = kwargs.pop("context", None)
1864-
if context is not None:
1865-
from a2a.client.client import ClientCallContext
1866-
1867-
if not isinstance(context, ClientCallContext):
1868-
actual_context = ClientCallContext()
1869-
if hasattr(context, "state"):
1870-
actual_context.state = context.state
1871-
elif isinstance(context, dict):
1872-
actual_context.state = context
1873-
context = actual_context
1874-
1875-
req = kwargs["request"]
18761901
if method_name == "on_message_send":
1877-
response = client.send_message(req, context=context)
1902+
response = client.send_message(Message(**kwargs))
18781903
chunks = []
18791904
async for chunk in response:
18801905
chunks.append(chunk)
18811906
return chunks
18821907
elif method_name == "on_get_task":
1883-
return await client.get_task(req, context=context)
1908+
response = await client.get_task(TaskQueryParams(**kwargs))
18841909
elif method_name == "on_cancel_task":
1885-
return await client.cancel_task(req, context=context)
1886-
elif method_name == "on_get_extended_agent_card":
1887-
return await client.get_extended_agent_card(req, context=context)
1910+
response = await client.cancel_task(TaskIdParams(**kwargs))
1911+
elif method_name == "handle_authenticated_agent_card":
1912+
response = await client.get_card()
18881913
else:
18891914
raise ValueError(f"Unknown method name: {method_name}")
18901915

1916+
return response
1917+
18911918
return _method # type: ignore[return-value]
18921919

18931920

@@ -2263,3 +2290,59 @@ def _import_autogen_tools_or_warn() -> Optional[types.ModuleType]:
22632290
"call `pip install google-cloud-aiplatform[ag2]`."
22642291
)
22652292
return None
2293+
2294+
2295+
def _serialize_agent_card_to_dict(card: Any) -> Optional[Dict[str, Any]]:
2296+
"""Validates and serializes an AgentCard to a dictionary representation.
2297+
2298+
Args:
2299+
card: The AgentCard instance (Pydantic model or Protobuf Message).
2300+
2301+
Returns:
2302+
The serialized card as a dictionary.
2303+
2304+
Raises:
2305+
TypeError: If the card type is not supported.
2306+
"""
2307+
if card is None:
2308+
return None
2309+
2310+
if hasattr(card, "model_dump"):
2311+
return typing.cast(dict[str, Any], card.model_dump(exclude_none=True))
2312+
elif hasattr(card, "DESCRIPTOR"):
2313+
from google.protobuf import json_format
2314+
2315+
return typing.cast(dict[str, Any], json_format.MessageToDict(card))
2316+
else:
2317+
raise TypeError(
2318+
f"Unsupported AgentCard type: {type(card)}. "
2319+
"Only Pydantic models and Protobuf Messages are supported."
2320+
)
2321+
2322+
2323+
def _serialize_agent_card_to_json(card: Any) -> Optional[str]:
2324+
"""Validates and serializes an AgentCard to a JSON string representation.
2325+
2326+
Args:
2327+
card: The AgentCard instance (Pydantic model or Protobuf Message).
2328+
2329+
Returns:
2330+
The serialized card as a JSON string.
2331+
2332+
Raises:
2333+
TypeError: If the card type is not supported.
2334+
"""
2335+
if card is None:
2336+
return None
2337+
2338+
if hasattr(card, "model_dump_json"):
2339+
return typing.cast(str, card.model_dump_json())
2340+
elif hasattr(card, "DESCRIPTOR"):
2341+
from google.protobuf import json_format
2342+
2343+
return typing.cast(str, json_format.MessageToJson(card))
2344+
else:
2345+
raise TypeError(
2346+
f"Unsupported AgentCard type: {type(card)}. "
2347+
"Only Pydantic models and Protobuf Messages are supported."
2348+
)

agentplatform/_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(

agentplatform/agent_engines/_agent_engines.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2005,11 +2005,11 @@ def _generate_class_methods_spec_or_raise(
20052005
class_method[_MODE_KEY_IN_SCHEMA] = mode
20062006
# A2A agent card is a special case, when running in A2A mode,
20072007
if hasattr(agent_engine, "agent_card"):
2008-
from google.protobuf import json_format
2009-
2010-
class_method[_A2A_AGENT_CARD] = json_format.MessageToJson(
2011-
getattr(agent_engine, "agent_card")
2012-
)
2008+
card = getattr(agent_engine, "agent_card")
2009+
if card is not None:
2010+
class_method[_A2A_AGENT_CARD] = (
2011+
_agent_engines_utils._serialize_agent_card_to_json(card)
2012+
)
20132013
class_methods_spec.append(class_method)
20142014

20152015
return class_methods_spec

0 commit comments

Comments
 (0)