adcp 2.12.1__py3-none-any.whl → 2.12.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adcp/__init__.py +2 -1
- adcp/protocols/a2a.py +315 -204
- adcp/protocols/mcp.py +95 -16
- adcp/types/_generated.py +476 -97
- adcp/types/aliases.py +1 -3
- adcp/types/generated_poc/adagents.py +1 -1
- adcp/types/generated_poc/core/activation_key.py +1 -1
- adcp/types/generated_poc/core/assets/audio_asset.py +1 -1
- adcp/types/generated_poc/core/assets/css_asset.py +1 -1
- adcp/types/generated_poc/core/assets/daast_asset.py +1 -1
- adcp/types/generated_poc/core/assets/html_asset.py +1 -1
- adcp/types/generated_poc/core/assets/image_asset.py +1 -1
- adcp/types/generated_poc/core/assets/javascript_asset.py +1 -1
- adcp/types/generated_poc/core/assets/text_asset.py +1 -1
- adcp/types/generated_poc/core/assets/url_asset.py +1 -1
- adcp/types/generated_poc/core/assets/vast_asset.py +1 -1
- adcp/types/generated_poc/core/assets/video_asset.py +1 -1
- adcp/types/generated_poc/core/assets/webhook_asset.py +1 -1
- adcp/types/generated_poc/core/brand_manifest.py +1 -1
- adcp/types/generated_poc/core/context.py +1 -1
- adcp/types/generated_poc/core/creative_asset.py +1 -1
- adcp/types/generated_poc/core/creative_assignment.py +1 -1
- adcp/types/generated_poc/core/creative_filters.py +1 -1
- adcp/types/generated_poc/core/creative_manifest.py +1 -1
- adcp/types/generated_poc/core/creative_policy.py +1 -1
- adcp/types/generated_poc/core/delivery_metrics.py +1 -1
- adcp/types/generated_poc/core/deployment.py +1 -1
- adcp/types/generated_poc/core/destination.py +1 -1
- adcp/types/generated_poc/core/dimensions.py +1 -1
- adcp/types/generated_poc/core/error.py +1 -1
- adcp/types/generated_poc/core/ext.py +1 -1
- adcp/types/generated_poc/core/format.py +1 -1
- adcp/types/generated_poc/core/format_id.py +1 -1
- adcp/types/generated_poc/core/frequency_cap.py +1 -1
- adcp/types/generated_poc/core/measurement.py +1 -1
- adcp/types/generated_poc/core/media_buy.py +1 -1
- adcp/types/generated_poc/core/package.py +1 -1
- adcp/types/generated_poc/core/performance_feedback.py +1 -1
- adcp/types/generated_poc/core/placement.py +1 -1
- adcp/types/generated_poc/core/product.py +1 -1
- adcp/types/generated_poc/core/product_filters.py +1 -1
- adcp/types/generated_poc/core/promoted_offerings.py +1 -1
- adcp/types/generated_poc/core/promoted_products.py +1 -1
- adcp/types/generated_poc/core/property.py +1 -1
- adcp/types/generated_poc/core/property_id.py +1 -1
- adcp/types/generated_poc/core/property_tag.py +1 -1
- adcp/types/generated_poc/core/protocol_envelope.py +1 -1
- adcp/types/generated_poc/core/publisher_property_selector.py +1 -1
- adcp/types/generated_poc/core/push_notification_config.py +1 -1
- adcp/types/generated_poc/core/reporting_capabilities.py +1 -1
- adcp/types/generated_poc/core/response.py +1 -1
- adcp/types/generated_poc/core/signal_filters.py +1 -1
- adcp/types/generated_poc/core/sub_asset.py +1 -1
- adcp/types/generated_poc/core/targeting.py +1 -1
- adcp/types/generated_poc/core/webhook_payload.py +1 -1
- adcp/types/generated_poc/creative/list_creative_formats_request.py +1 -1
- adcp/types/generated_poc/creative/list_creative_formats_response.py +1 -1
- adcp/types/generated_poc/creative/preview_creative_request.py +1 -1
- adcp/types/generated_poc/creative/preview_creative_response.py +1 -1
- adcp/types/generated_poc/creative/preview_render.py +1 -1
- adcp/types/generated_poc/enums/adcp_domain.py +1 -1
- adcp/types/generated_poc/enums/asset_content_type.py +1 -1
- adcp/types/generated_poc/enums/auth_scheme.py +1 -1
- adcp/types/generated_poc/enums/available_metric.py +1 -1
- adcp/types/generated_poc/enums/channels.py +1 -1
- adcp/types/generated_poc/enums/co_branding_requirement.py +1 -1
- adcp/types/generated_poc/enums/creative_action.py +1 -1
- adcp/types/generated_poc/enums/creative_agent_capability.py +1 -1
- adcp/types/generated_poc/enums/creative_sort_field.py +1 -1
- adcp/types/generated_poc/enums/creative_status.py +1 -1
- adcp/types/generated_poc/enums/daast_tracking_event.py +1 -1
- adcp/types/generated_poc/enums/daast_version.py +1 -1
- adcp/types/generated_poc/enums/delivery_type.py +1 -1
- adcp/types/generated_poc/enums/dimension_unit.py +1 -1
- adcp/types/generated_poc/enums/feed_format.py +1 -1
- adcp/types/generated_poc/enums/feedback_source.py +1 -1
- adcp/types/generated_poc/enums/format_category.py +1 -1
- adcp/types/generated_poc/enums/format_id_parameter.py +1 -1
- adcp/types/generated_poc/enums/frequency_cap_scope.py +1 -1
- adcp/types/generated_poc/enums/history_entry_type.py +1 -1
- adcp/types/generated_poc/enums/http_method.py +1 -1
- adcp/types/generated_poc/enums/identifier_types.py +1 -1
- adcp/types/generated_poc/enums/javascript_module_type.py +1 -1
- adcp/types/generated_poc/enums/landing_page_requirement.py +1 -1
- adcp/types/generated_poc/enums/markdown_flavor.py +1 -1
- adcp/types/generated_poc/enums/media_buy_status.py +1 -1
- adcp/types/generated_poc/enums/metric_type.py +1 -1
- adcp/types/generated_poc/enums/notification_type.py +1 -1
- adcp/types/generated_poc/enums/pacing.py +1 -1
- adcp/types/generated_poc/enums/preview_output_format.py +1 -1
- adcp/types/generated_poc/enums/pricing_model.py +1 -1
- adcp/types/generated_poc/enums/property_type.py +1 -1
- adcp/types/generated_poc/enums/publisher_identifier_types.py +1 -1
- adcp/types/generated_poc/enums/reporting_frequency.py +1 -1
- adcp/types/generated_poc/enums/signal_catalog_type.py +1 -1
- adcp/types/generated_poc/enums/sort_direction.py +1 -1
- adcp/types/generated_poc/enums/standard_format_ids.py +1 -1
- adcp/types/generated_poc/enums/task_status.py +1 -1
- adcp/types/generated_poc/enums/task_type.py +1 -1
- adcp/types/generated_poc/enums/update_frequency.py +1 -1
- adcp/types/generated_poc/enums/url_asset_type.py +1 -1
- adcp/types/generated_poc/enums/validation_mode.py +1 -1
- adcp/types/generated_poc/enums/vast_tracking_event.py +1 -1
- adcp/types/generated_poc/enums/vast_version.py +1 -1
- adcp/types/generated_poc/enums/webhook_response_type.py +1 -1
- adcp/types/generated_poc/enums/webhook_security_method.py +1 -1
- adcp/types/generated_poc/media_buy/build_creative_request.py +1 -1
- adcp/types/generated_poc/media_buy/build_creative_response.py +1 -1
- adcp/types/generated_poc/media_buy/create_media_buy_request.py +1 -1
- adcp/types/generated_poc/media_buy/create_media_buy_response.py +1 -1
- adcp/types/generated_poc/media_buy/get_media_buy_delivery_request.py +1 -1
- adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py +1 -1
- adcp/types/generated_poc/media_buy/get_products_request.py +1 -1
- adcp/types/generated_poc/media_buy/get_products_response.py +1 -1
- adcp/types/generated_poc/media_buy/list_authorized_properties_request.py +1 -1
- adcp/types/generated_poc/media_buy/list_authorized_properties_response.py +1 -1
- adcp/types/generated_poc/media_buy/list_creative_formats_request.py +1 -1
- adcp/types/generated_poc/media_buy/list_creative_formats_response.py +1 -1
- adcp/types/generated_poc/media_buy/list_creatives_request.py +1 -1
- adcp/types/generated_poc/media_buy/list_creatives_response.py +1 -1
- adcp/types/generated_poc/media_buy/package_request.py +1 -1
- adcp/types/generated_poc/media_buy/provide_performance_feedback_request.py +1 -1
- adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py +1 -1
- adcp/types/generated_poc/media_buy/sync_creatives_request.py +1 -1
- adcp/types/generated_poc/media_buy/sync_creatives_response.py +1 -1
- adcp/types/generated_poc/media_buy/update_media_buy_request.py +1 -1
- adcp/types/generated_poc/media_buy/update_media_buy_response.py +1 -1
- adcp/types/generated_poc/pricing_options/cpc_option.py +1 -1
- adcp/types/generated_poc/pricing_options/cpcv_option.py +1 -1
- adcp/types/generated_poc/pricing_options/cpm_auction_option.py +1 -1
- adcp/types/generated_poc/pricing_options/cpm_fixed_option.py +1 -1
- adcp/types/generated_poc/pricing_options/cpp_option.py +1 -1
- adcp/types/generated_poc/pricing_options/cpv_option.py +1 -1
- adcp/types/generated_poc/pricing_options/flat_rate_option.py +1 -1
- adcp/types/generated_poc/pricing_options/vcpm_auction_option.py +1 -1
- adcp/types/generated_poc/pricing_options/vcpm_fixed_option.py +1 -1
- adcp/types/generated_poc/protocols/adcp_extension.py +1 -1
- adcp/types/generated_poc/signals/activate_signal_request.py +1 -1
- adcp/types/generated_poc/signals/activate_signal_response.py +1 -1
- adcp/types/generated_poc/signals/get_signals_request.py +1 -1
- adcp/types/generated_poc/signals/get_signals_response.py +1 -1
- {adcp-2.12.1.dist-info → adcp-2.12.2.dist-info}/METADATA +1 -1
- adcp-2.12.2.dist-info/RECORD +176 -0
- adcp-2.12.1.dist-info/RECORD +0 -176
- {adcp-2.12.1.dist-info → adcp-2.12.2.dist-info}/WHEEL +0 -0
- {adcp-2.12.1.dist-info → adcp-2.12.2.dist-info}/entry_points.txt +0 -0
- {adcp-2.12.1.dist-info → adcp-2.12.2.dist-info}/licenses/LICENSE +0 -0
- {adcp-2.12.1.dist-info → adcp-2.12.2.dist-info}/top_level.txt +0 -0
adcp/protocols/a2a.py
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
"""A2A protocol adapter using
|
|
4
|
-
|
|
5
|
-
The official a2a-sdk is primarily for building A2A servers. For client functionality,
|
|
6
|
-
we implement the A2A protocol using HTTP requests as per the A2A specification.
|
|
7
|
-
"""
|
|
3
|
+
"""A2A protocol adapter using the official a2a-sdk client."""
|
|
8
4
|
|
|
9
5
|
import logging
|
|
10
6
|
import time
|
|
@@ -12,6 +8,17 @@ from typing import Any
|
|
|
12
8
|
from uuid import uuid4
|
|
13
9
|
|
|
14
10
|
import httpx
|
|
11
|
+
from a2a.client import A2ACardResolver, A2AClient
|
|
12
|
+
from a2a.types import (
|
|
13
|
+
DataPart,
|
|
14
|
+
Message,
|
|
15
|
+
MessageSendParams,
|
|
16
|
+
Part,
|
|
17
|
+
Role,
|
|
18
|
+
SendMessageRequest,
|
|
19
|
+
Task,
|
|
20
|
+
TextPart,
|
|
21
|
+
)
|
|
15
22
|
|
|
16
23
|
from adcp.exceptions import (
|
|
17
24
|
ADCPAuthenticationError,
|
|
@@ -25,157 +32,258 @@ logger = logging.getLogger(__name__)
|
|
|
25
32
|
|
|
26
33
|
|
|
27
34
|
class A2AAdapter(ProtocolAdapter):
|
|
28
|
-
"""Adapter for A2A protocol
|
|
35
|
+
"""Adapter for A2A protocol using official a2a-sdk client."""
|
|
29
36
|
|
|
30
37
|
def __init__(self, agent_config: AgentConfig):
|
|
31
|
-
"""Initialize A2A adapter with
|
|
38
|
+
"""Initialize A2A adapter with official A2A client."""
|
|
32
39
|
super().__init__(agent_config)
|
|
33
|
-
self.
|
|
40
|
+
self._httpx_client: httpx.AsyncClient | None = None
|
|
41
|
+
self._a2a_client: A2AClient | None = None
|
|
34
42
|
|
|
35
|
-
async def
|
|
43
|
+
async def _get_httpx_client(self) -> httpx.AsyncClient:
|
|
36
44
|
"""Get or create the HTTP client with connection pooling."""
|
|
37
|
-
if self.
|
|
38
|
-
# Configure connection pooling for better performance
|
|
45
|
+
if self._httpx_client is None:
|
|
39
46
|
limits = httpx.Limits(
|
|
40
47
|
max_keepalive_connections=10,
|
|
41
48
|
max_connections=20,
|
|
42
49
|
keepalive_expiry=30.0,
|
|
43
50
|
)
|
|
44
|
-
|
|
51
|
+
|
|
52
|
+
headers = {}
|
|
53
|
+
if self.agent_config.auth_token:
|
|
54
|
+
if self.agent_config.auth_type == "bearer":
|
|
55
|
+
headers["Authorization"] = f"Bearer {self.agent_config.auth_token}"
|
|
56
|
+
else:
|
|
57
|
+
headers[self.agent_config.auth_header] = self.agent_config.auth_token
|
|
58
|
+
|
|
59
|
+
self._httpx_client = httpx.AsyncClient(
|
|
60
|
+
limits=limits,
|
|
61
|
+
headers=headers,
|
|
62
|
+
timeout=self.agent_config.timeout,
|
|
63
|
+
)
|
|
45
64
|
logger.debug(
|
|
46
65
|
f"Created HTTP client with connection pooling for agent {self.agent_config.id}"
|
|
47
66
|
)
|
|
48
|
-
return self.
|
|
67
|
+
return self._httpx_client
|
|
68
|
+
|
|
69
|
+
async def _get_a2a_client(self) -> A2AClient:
|
|
70
|
+
"""Get or create the A2A client."""
|
|
71
|
+
if self._a2a_client is None:
|
|
72
|
+
httpx_client = await self._get_httpx_client()
|
|
73
|
+
|
|
74
|
+
# Use A2ACardResolver to fetch the agent card
|
|
75
|
+
card_resolver = A2ACardResolver(
|
|
76
|
+
httpx_client=httpx_client,
|
|
77
|
+
base_url=self.agent_config.agent_uri,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
agent_card = await card_resolver.get_agent_card()
|
|
82
|
+
logger.debug(f"Fetched agent card for {self.agent_config.id}")
|
|
83
|
+
except httpx.HTTPStatusError as e:
|
|
84
|
+
status_code = e.response.status_code
|
|
85
|
+
if status_code in (401, 403):
|
|
86
|
+
raise ADCPAuthenticationError(
|
|
87
|
+
f"Authentication failed: HTTP {status_code}",
|
|
88
|
+
agent_id=self.agent_config.id,
|
|
89
|
+
agent_uri=self.agent_config.agent_uri,
|
|
90
|
+
) from e
|
|
91
|
+
else:
|
|
92
|
+
raise ADCPConnectionError(
|
|
93
|
+
f"Failed to fetch agent card: HTTP {status_code}",
|
|
94
|
+
agent_id=self.agent_config.id,
|
|
95
|
+
agent_uri=self.agent_config.agent_uri,
|
|
96
|
+
) from e
|
|
97
|
+
except httpx.TimeoutException as e:
|
|
98
|
+
raise ADCPTimeoutError(
|
|
99
|
+
f"Timeout fetching agent card: {e}",
|
|
100
|
+
agent_id=self.agent_config.id,
|
|
101
|
+
agent_uri=self.agent_config.agent_uri,
|
|
102
|
+
timeout=self.agent_config.timeout,
|
|
103
|
+
) from e
|
|
104
|
+
except httpx.HTTPError as e:
|
|
105
|
+
raise ADCPConnectionError(
|
|
106
|
+
f"Failed to fetch agent card: {e}",
|
|
107
|
+
agent_id=self.agent_config.id,
|
|
108
|
+
agent_uri=self.agent_config.agent_uri,
|
|
109
|
+
) from e
|
|
110
|
+
|
|
111
|
+
self._a2a_client = A2AClient(
|
|
112
|
+
httpx_client=httpx_client,
|
|
113
|
+
agent_card=agent_card,
|
|
114
|
+
)
|
|
115
|
+
logger.debug(f"Created A2A client for agent {self.agent_config.id}")
|
|
116
|
+
|
|
117
|
+
return self._a2a_client
|
|
49
118
|
|
|
50
119
|
async def close(self) -> None:
|
|
51
120
|
"""Close the HTTP client and clean up resources."""
|
|
52
|
-
if self.
|
|
121
|
+
if self._httpx_client is not None:
|
|
53
122
|
logger.debug(f"Closing A2A adapter client for agent {self.agent_config.id}")
|
|
54
|
-
await self.
|
|
55
|
-
self.
|
|
123
|
+
await self._httpx_client.aclose()
|
|
124
|
+
self._httpx_client = None
|
|
125
|
+
self._a2a_client = None
|
|
56
126
|
|
|
57
|
-
async def _call_a2a_tool(
|
|
127
|
+
async def _call_a2a_tool(
|
|
128
|
+
self, tool_name: str, params: dict[str, Any], use_explicit_skill: bool = True
|
|
129
|
+
) -> TaskResult[Any]:
|
|
58
130
|
"""
|
|
59
|
-
Call a tool using A2A protocol.
|
|
131
|
+
Call a tool using A2A protocol via official a2a-sdk client.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
tool_name: Name of the skill/tool to invoke
|
|
135
|
+
params: Parameters to pass to the skill
|
|
136
|
+
use_explicit_skill: If True, use explicit skill invocation (deterministic).
|
|
137
|
+
If False, use natural language (flexible).
|
|
60
138
|
|
|
61
|
-
|
|
62
|
-
|
|
139
|
+
The default is explicit skill invocation for predictable, repeatable behavior.
|
|
140
|
+
See: https://docs.adcontextprotocol.org/docs/protocols/a2a-guide
|
|
63
141
|
"""
|
|
64
142
|
start_time = time.time() if self.agent_config.debug else None
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
message = {
|
|
78
|
-
"role": "user",
|
|
79
|
-
"parts": [
|
|
80
|
-
{
|
|
81
|
-
"type": "text",
|
|
82
|
-
"text": self._format_tool_request(tool_name, params),
|
|
143
|
+
a2a_client = await self._get_a2a_client()
|
|
144
|
+
|
|
145
|
+
# Build A2A message
|
|
146
|
+
message_id = str(uuid4())
|
|
147
|
+
|
|
148
|
+
if use_explicit_skill:
|
|
149
|
+
# Explicit skill invocation (deterministic)
|
|
150
|
+
# Use DataPart with skill name and parameters
|
|
151
|
+
data_part = DataPart(
|
|
152
|
+
data={
|
|
153
|
+
"skill": tool_name,
|
|
154
|
+
"parameters": params,
|
|
83
155
|
}
|
|
84
|
-
|
|
85
|
-
|
|
156
|
+
)
|
|
157
|
+
message = Message(
|
|
158
|
+
message_id=message_id,
|
|
159
|
+
role=Role.user,
|
|
160
|
+
parts=[Part(root=data_part)],
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
# Natural language invocation (flexible)
|
|
164
|
+
# Agent interprets intent from text
|
|
165
|
+
text_part = TextPart(text=self._format_tool_request(tool_name, params))
|
|
166
|
+
message = Message(
|
|
167
|
+
message_id=message_id,
|
|
168
|
+
role=Role.user,
|
|
169
|
+
parts=[Part(root=text_part)],
|
|
170
|
+
)
|
|
86
171
|
|
|
87
|
-
#
|
|
88
|
-
|
|
172
|
+
# Build request params
|
|
173
|
+
params_obj = MessageSendParams(message=message)
|
|
89
174
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
175
|
+
# Build request
|
|
176
|
+
request = SendMessageRequest(
|
|
177
|
+
id=str(uuid4()),
|
|
178
|
+
params=params_obj,
|
|
179
|
+
)
|
|
94
180
|
|
|
95
181
|
debug_info = None
|
|
182
|
+
debug_request: dict[str, Any] = {}
|
|
96
183
|
if self.agent_config.debug:
|
|
97
184
|
debug_request = {
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
v
|
|
103
|
-
if k.lower() not in ("authorization", self.agent_config.auth_header.lower())
|
|
104
|
-
else "***"
|
|
105
|
-
)
|
|
106
|
-
for k, v in headers.items()
|
|
107
|
-
},
|
|
108
|
-
"body": request_data,
|
|
185
|
+
"method": "send_message",
|
|
186
|
+
"message_id": message_id,
|
|
187
|
+
"tool": tool_name,
|
|
188
|
+
"params": params,
|
|
109
189
|
}
|
|
110
190
|
|
|
111
191
|
try:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
118
|
-
|
|
192
|
+
# Use official A2A client
|
|
193
|
+
sdk_response = await a2a_client.send_message(request)
|
|
194
|
+
|
|
195
|
+
# SendMessageResponse is a RootModel union - unwrap it to get the actual response
|
|
196
|
+
# (either JSONRPCSuccessResponse or JSONRPCErrorResponse)
|
|
197
|
+
response = sdk_response.root if hasattr(sdk_response, "root") else sdk_response
|
|
198
|
+
|
|
199
|
+
# Handle JSON-RPC error response
|
|
200
|
+
if hasattr(response, "error"):
|
|
201
|
+
error_msg = response.error.message if response.error.message else "Unknown error"
|
|
202
|
+
if self.agent_config.debug and start_time:
|
|
203
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
204
|
+
debug_info = DebugInfo(
|
|
205
|
+
request=debug_request,
|
|
206
|
+
response={"error": response.error.model_dump()},
|
|
207
|
+
duration_ms=duration_ms,
|
|
208
|
+
)
|
|
209
|
+
return TaskResult[Any](
|
|
210
|
+
status=TaskStatus.FAILED,
|
|
211
|
+
error=error_msg,
|
|
212
|
+
success=False,
|
|
213
|
+
debug_info=debug_info,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Handle success response
|
|
217
|
+
if hasattr(response, "result"):
|
|
218
|
+
result = response.result
|
|
219
|
+
|
|
220
|
+
if self.agent_config.debug and start_time:
|
|
221
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
222
|
+
debug_info = DebugInfo(
|
|
223
|
+
request=debug_request,
|
|
224
|
+
response={"result": result.model_dump()},
|
|
225
|
+
duration_ms=duration_ms,
|
|
226
|
+
)
|
|
119
227
|
|
|
120
|
-
|
|
228
|
+
# Result can be either Task or Message
|
|
229
|
+
if isinstance(result, Task):
|
|
230
|
+
return self._process_task_response(result, debug_info)
|
|
231
|
+
else:
|
|
232
|
+
# Message response (shouldn't happen for send_message, but handle it)
|
|
233
|
+
agent_id = self.agent_config.id
|
|
234
|
+
logger.warning(f"Received Message instead of Task from A2A agent {agent_id}")
|
|
235
|
+
return TaskResult[Any](
|
|
236
|
+
status=TaskStatus.COMPLETED,
|
|
237
|
+
data=None,
|
|
238
|
+
message="Received message response",
|
|
239
|
+
success=True,
|
|
240
|
+
debug_info=debug_info,
|
|
241
|
+
)
|
|
121
242
|
|
|
243
|
+
# Shouldn't reach here
|
|
244
|
+
return TaskResult[Any](
|
|
245
|
+
status=TaskStatus.FAILED,
|
|
246
|
+
error="Invalid response from A2A client",
|
|
247
|
+
success=False,
|
|
248
|
+
debug_info=debug_info,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
except httpx.HTTPStatusError as e:
|
|
252
|
+
status_code = e.response.status_code
|
|
122
253
|
if self.agent_config.debug and start_time:
|
|
123
254
|
duration_ms = (time.time() - start_time) * 1000
|
|
124
255
|
debug_info = DebugInfo(
|
|
125
256
|
request=debug_request,
|
|
126
|
-
response={"
|
|
257
|
+
response={"error": str(e), "status_code": status_code},
|
|
127
258
|
duration_ms=duration_ms,
|
|
128
259
|
)
|
|
129
260
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
task_status = data.get("status")
|
|
133
|
-
|
|
134
|
-
if task_status == "completed":
|
|
135
|
-
# Extract the result from the artifacts array
|
|
136
|
-
result_data = self._extract_result(data)
|
|
137
|
-
|
|
138
|
-
# Check for task-level errors in the payload
|
|
139
|
-
errors = result_data.get("errors", []) if isinstance(result_data, dict) else []
|
|
140
|
-
has_errors = bool(errors)
|
|
141
|
-
|
|
142
|
-
return TaskResult[Any](
|
|
143
|
-
status=TaskStatus.COMPLETED,
|
|
144
|
-
data=result_data,
|
|
145
|
-
message=self._extract_text_part(data),
|
|
146
|
-
success=not has_errors,
|
|
147
|
-
metadata={
|
|
148
|
-
"task_id": data.get("taskId"),
|
|
149
|
-
"context_id": data.get("contextId"),
|
|
150
|
-
},
|
|
151
|
-
debug_info=debug_info,
|
|
152
|
-
)
|
|
153
|
-
elif task_status == "failed":
|
|
154
|
-
# Protocol-level failure - extract error message from TextPart
|
|
155
|
-
error_msg = self._extract_text_part(data) or "Task failed"
|
|
156
|
-
return TaskResult[Any](
|
|
157
|
-
status=TaskStatus.FAILED,
|
|
158
|
-
error=error_msg,
|
|
159
|
-
success=False,
|
|
160
|
-
debug_info=debug_info,
|
|
161
|
-
)
|
|
261
|
+
if status_code in (401, 403):
|
|
262
|
+
error_msg = f"Authentication failed: HTTP {status_code}"
|
|
162
263
|
else:
|
|
163
|
-
|
|
164
|
-
# These don't need to have structured AdCP content - only completed responses do
|
|
165
|
-
return TaskResult[Any](
|
|
166
|
-
status=TaskStatus.SUBMITTED,
|
|
167
|
-
data=None, # Interim responses may not have structured AdCP content
|
|
168
|
-
message=self._extract_text_part(data),
|
|
169
|
-
success=True,
|
|
170
|
-
metadata={
|
|
171
|
-
"task_id": data.get("taskId"),
|
|
172
|
-
"context_id": data.get("contextId"),
|
|
173
|
-
"status": task_status, # submitted, working, pending, input-required, etc.
|
|
174
|
-
},
|
|
175
|
-
debug_info=debug_info,
|
|
176
|
-
)
|
|
264
|
+
error_msg = f"HTTP {status_code} error: {e}"
|
|
177
265
|
|
|
178
|
-
|
|
266
|
+
return TaskResult[Any](
|
|
267
|
+
status=TaskStatus.FAILED,
|
|
268
|
+
error=error_msg,
|
|
269
|
+
success=False,
|
|
270
|
+
debug_info=debug_info,
|
|
271
|
+
)
|
|
272
|
+
except httpx.TimeoutException as e:
|
|
273
|
+
if self.agent_config.debug and start_time:
|
|
274
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
275
|
+
debug_info = DebugInfo(
|
|
276
|
+
request=debug_request,
|
|
277
|
+
response={"error": str(e)},
|
|
278
|
+
duration_ms=duration_ms,
|
|
279
|
+
)
|
|
280
|
+
return TaskResult[Any](
|
|
281
|
+
status=TaskStatus.FAILED,
|
|
282
|
+
error=f"Timeout: {e}",
|
|
283
|
+
success=False,
|
|
284
|
+
debug_info=debug_info,
|
|
285
|
+
)
|
|
286
|
+
except Exception as e:
|
|
179
287
|
if self.agent_config.debug and start_time:
|
|
180
288
|
duration_ms = (time.time() - start_time) * 1000
|
|
181
289
|
debug_info = DebugInfo(
|
|
@@ -190,16 +298,62 @@ class A2AAdapter(ProtocolAdapter):
|
|
|
190
298
|
debug_info=debug_info,
|
|
191
299
|
)
|
|
192
300
|
|
|
301
|
+
def _process_task_response(self, task: Task, debug_info: DebugInfo | None) -> TaskResult[Any]:
|
|
302
|
+
"""Process a Task response from A2A into our TaskResult format."""
|
|
303
|
+
task_state = task.status.state
|
|
304
|
+
|
|
305
|
+
if task_state == "completed":
|
|
306
|
+
# Extract the result from the artifacts array
|
|
307
|
+
result_data = self._extract_result_from_task(task)
|
|
308
|
+
|
|
309
|
+
# Check for task-level errors in the payload
|
|
310
|
+
errors = result_data.get("errors", []) if isinstance(result_data, dict) else []
|
|
311
|
+
has_errors = bool(errors)
|
|
312
|
+
|
|
313
|
+
return TaskResult[Any](
|
|
314
|
+
status=TaskStatus.COMPLETED,
|
|
315
|
+
data=result_data,
|
|
316
|
+
message=self._extract_text_from_task(task),
|
|
317
|
+
success=not has_errors,
|
|
318
|
+
metadata={
|
|
319
|
+
"task_id": task.id,
|
|
320
|
+
"context_id": task.context_id,
|
|
321
|
+
},
|
|
322
|
+
debug_info=debug_info,
|
|
323
|
+
)
|
|
324
|
+
elif task_state == "failed":
|
|
325
|
+
# Protocol-level failure - extract error message from TextPart
|
|
326
|
+
error_msg = self._extract_text_from_task(task) or "Task failed"
|
|
327
|
+
return TaskResult[Any](
|
|
328
|
+
status=TaskStatus.FAILED,
|
|
329
|
+
error=error_msg,
|
|
330
|
+
success=False,
|
|
331
|
+
debug_info=debug_info,
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
# Handle all interim states (submitted, working, input-required, etc.)
|
|
335
|
+
return TaskResult[Any](
|
|
336
|
+
status=TaskStatus.SUBMITTED,
|
|
337
|
+
data=None, # Interim responses may not have structured AdCP content
|
|
338
|
+
message=self._extract_text_from_task(task),
|
|
339
|
+
success=True,
|
|
340
|
+
metadata={
|
|
341
|
+
"task_id": task.id,
|
|
342
|
+
"context_id": task.context_id,
|
|
343
|
+
"status": task_state,
|
|
344
|
+
},
|
|
345
|
+
debug_info=debug_info,
|
|
346
|
+
)
|
|
347
|
+
|
|
193
348
|
def _format_tool_request(self, tool_name: str, params: dict[str, Any]) -> str:
|
|
194
349
|
"""Format tool request as natural language for A2A."""
|
|
195
|
-
# For AdCP tools, we format as a structured request
|
|
196
350
|
import json
|
|
197
351
|
|
|
198
352
|
return f"Execute tool: {tool_name}\nParameters: {json.dumps(params, indent=2)}"
|
|
199
353
|
|
|
200
|
-
def
|
|
354
|
+
def _extract_result_from_task(self, task: Task) -> Any:
|
|
201
355
|
"""
|
|
202
|
-
Extract result data from A2A
|
|
356
|
+
Extract result data from A2A Task following canonical format.
|
|
203
357
|
|
|
204
358
|
Per A2A response spec:
|
|
205
359
|
- Responses MUST include at least one DataPart (kind: "data")
|
|
@@ -207,68 +361,55 @@ class A2AAdapter(ProtocolAdapter):
|
|
|
207
361
|
- When multiple artifacts exist, use the last one (most recent in streaming)
|
|
208
362
|
- DataParts contain structured AdCP payload
|
|
209
363
|
"""
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
logger.warning("A2A response missing required artifacts array")
|
|
214
|
-
return response_data
|
|
364
|
+
if not task.artifacts:
|
|
365
|
+
logger.warning("A2A Task missing required artifacts array")
|
|
366
|
+
return {}
|
|
215
367
|
|
|
216
368
|
# Use last artifact (most recent in streaming scenarios)
|
|
217
|
-
|
|
218
|
-
target_artifact = artifacts[-1]
|
|
369
|
+
target_artifact = task.artifacts[-1]
|
|
219
370
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
logger.warning("A2A response artifact has no parts")
|
|
224
|
-
return response_data
|
|
371
|
+
if not target_artifact.parts:
|
|
372
|
+
logger.warning("A2A Task artifact has no parts")
|
|
373
|
+
return {}
|
|
225
374
|
|
|
226
375
|
# Find all DataParts (kind: "data")
|
|
227
|
-
|
|
376
|
+
# Note: Parts are wrapped in a Part union type, access via .root
|
|
377
|
+
from a2a.types import DataPart
|
|
378
|
+
|
|
379
|
+
data_parts = [p.root for p in target_artifact.parts if isinstance(p.root, DataPart)]
|
|
228
380
|
|
|
229
381
|
if not data_parts:
|
|
230
|
-
logger.warning("A2A
|
|
231
|
-
return
|
|
382
|
+
logger.warning("A2A Task missing required DataPart (kind: 'data')")
|
|
383
|
+
return {}
|
|
232
384
|
|
|
233
385
|
# Use last DataPart as authoritative (handles streaming scenarios within an artifact)
|
|
234
386
|
last_data_part = data_parts[-1]
|
|
235
|
-
data = last_data_part.
|
|
387
|
+
data = last_data_part.data
|
|
236
388
|
|
|
237
389
|
# Some A2A implementations (e.g., ADK) wrap the response in {"response": {...}}
|
|
238
390
|
# Unwrap it to get the actual AdCP payload if present
|
|
239
|
-
# ADK is inconsistent - some DataParts have the wrapper, others don't
|
|
240
391
|
if isinstance(data, dict) and "response" in data:
|
|
241
392
|
# If response is the only key, unwrap completely
|
|
242
393
|
if len(data) == 1:
|
|
243
394
|
return data["response"]
|
|
244
395
|
# If there are other keys alongside response, prefer the wrapped content
|
|
245
|
-
# but keep it flexible for edge cases
|
|
246
396
|
return data["response"]
|
|
247
397
|
|
|
248
398
|
return data
|
|
249
399
|
|
|
250
|
-
def
|
|
251
|
-
"""
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
Uses last artifact (same logic as _extract_result).
|
|
255
|
-
"""
|
|
256
|
-
artifacts = response_data.get("artifacts", [])
|
|
257
|
-
|
|
258
|
-
if not artifacts:
|
|
400
|
+
def _extract_text_from_task(self, task: Task) -> str | None:
|
|
401
|
+
"""Extract human-readable message from TextPart if present."""
|
|
402
|
+
if not task.artifacts:
|
|
259
403
|
return None
|
|
260
404
|
|
|
261
405
|
# Use last artifact (most recent in streaming scenarios)
|
|
262
|
-
|
|
263
|
-
target_artifact = artifacts[-1]
|
|
264
|
-
|
|
265
|
-
parts = target_artifact.get("parts", [])
|
|
406
|
+
target_artifact = task.artifacts[-1]
|
|
266
407
|
|
|
267
408
|
# Find TextPart (kind: "text")
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return
|
|
409
|
+
# Note: Parts are wrapped in a Part union type, access via .root
|
|
410
|
+
for part in target_artifact.parts:
|
|
411
|
+
if isinstance(part.root, TextPart):
|
|
412
|
+
return part.root.text
|
|
272
413
|
|
|
273
414
|
return None
|
|
274
415
|
|
|
@@ -332,36 +473,17 @@ class A2AAdapter(ProtocolAdapter):
|
|
|
332
473
|
"""
|
|
333
474
|
List available tools from A2A agent.
|
|
334
475
|
|
|
335
|
-
|
|
336
|
-
their capabilities through the agent card. For AdCP, we rely on the
|
|
337
|
-
standard AdCP tool set.
|
|
476
|
+
Uses A2A client which already fetched the agent card during initialization.
|
|
338
477
|
"""
|
|
339
|
-
client
|
|
340
|
-
|
|
341
|
-
headers = {"Content-Type": "application/json"}
|
|
342
|
-
|
|
343
|
-
if self.agent_config.auth_token:
|
|
344
|
-
# Support custom auth headers and types
|
|
345
|
-
if self.agent_config.auth_type == "bearer":
|
|
346
|
-
headers[self.agent_config.auth_header] = f"Bearer {self.agent_config.auth_token}"
|
|
347
|
-
else:
|
|
348
|
-
headers[self.agent_config.auth_header] = self.agent_config.auth_token
|
|
349
|
-
|
|
350
|
-
# Try to fetch agent card from standard A2A location
|
|
351
|
-
# A2A spec uses /.well-known/agent.json for agent card
|
|
352
|
-
url = f"{self.agent_config.agent_uri}/.well-known/agent.json"
|
|
353
|
-
|
|
354
|
-
logger.debug(f"Fetching A2A agent card for {self.agent_config.id} from {url}")
|
|
478
|
+
# Get the A2A client (which already fetched the agent card)
|
|
479
|
+
a2a_client = await self._get_a2a_client()
|
|
355
480
|
|
|
481
|
+
# Fetch the agent card using the official method
|
|
356
482
|
try:
|
|
357
|
-
|
|
358
|
-
response.raise_for_status()
|
|
359
|
-
|
|
360
|
-
data = response.json()
|
|
483
|
+
agent_card = await a2a_client.get_card()
|
|
361
484
|
|
|
362
485
|
# Extract skills from agent card
|
|
363
|
-
|
|
364
|
-
tool_names = [skill.get("name", "") for skill in skills if skill.get("name")]
|
|
486
|
+
tool_names = [skill.name for skill in agent_card.skills if skill.name]
|
|
365
487
|
|
|
366
488
|
logger.info(f"Found {len(tool_names)} tools from A2A agent {self.agent_config.id}")
|
|
367
489
|
return tool_names
|
|
@@ -402,7 +524,7 @@ class A2AAdapter(ProtocolAdapter):
|
|
|
402
524
|
"""
|
|
403
525
|
Get agent information including AdCP extension metadata from A2A agent card.
|
|
404
526
|
|
|
405
|
-
|
|
527
|
+
Uses A2A client's get_card() method to fetch the agent card and extracts:
|
|
406
528
|
- Basic agent info (name, description, version)
|
|
407
529
|
- AdCP extension (extensions.adcp.adcp_version, extensions.adcp.protocols_supported)
|
|
408
530
|
- Available skills/tools
|
|
@@ -410,47 +532,36 @@ class A2AAdapter(ProtocolAdapter):
|
|
|
410
532
|
Returns:
|
|
411
533
|
Dictionary with agent metadata
|
|
412
534
|
"""
|
|
413
|
-
client
|
|
414
|
-
|
|
415
|
-
headers = {"Content-Type": "application/json"}
|
|
416
|
-
|
|
417
|
-
if self.agent_config.auth_token:
|
|
418
|
-
if self.agent_config.auth_type == "bearer":
|
|
419
|
-
headers[self.agent_config.auth_header] = f"Bearer {self.agent_config.auth_token}"
|
|
420
|
-
else:
|
|
421
|
-
headers[self.agent_config.auth_header] = self.agent_config.auth_token
|
|
535
|
+
# Get the A2A client (which already fetched the agent card)
|
|
536
|
+
a2a_client = await self._get_a2a_client()
|
|
422
537
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
logger.debug(f"Fetching A2A agent info for {self.agent_config.id} from {url}")
|
|
538
|
+
logger.debug(f"Fetching A2A agent info for {self.agent_config.id}")
|
|
426
539
|
|
|
427
540
|
try:
|
|
428
|
-
|
|
429
|
-
response.raise_for_status()
|
|
430
|
-
|
|
431
|
-
agent_card = response.json()
|
|
541
|
+
agent_card = await a2a_client.get_card()
|
|
432
542
|
|
|
433
543
|
# Extract basic info
|
|
434
544
|
info: dict[str, Any] = {
|
|
435
|
-
"name": agent_card.
|
|
436
|
-
"description": agent_card.
|
|
437
|
-
"version": agent_card.
|
|
545
|
+
"name": agent_card.name,
|
|
546
|
+
"description": agent_card.description,
|
|
547
|
+
"version": agent_card.version,
|
|
438
548
|
"protocol": "a2a",
|
|
439
549
|
}
|
|
440
550
|
|
|
441
551
|
# Extract skills/tools
|
|
442
|
-
|
|
443
|
-
tool_names = [skill.get("name") for skill in skills if skill.get("name")]
|
|
552
|
+
tool_names = [skill.name for skill in agent_card.skills if skill.name]
|
|
444
553
|
if tool_names:
|
|
445
554
|
info["tools"] = tool_names
|
|
446
555
|
|
|
447
556
|
# Extract AdCP extension metadata
|
|
448
|
-
extensions
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if
|
|
452
|
-
|
|
453
|
-
|
|
557
|
+
# Note: AgentCard type doesn't include extensions in the SDK,
|
|
558
|
+
# but it may be present at runtime
|
|
559
|
+
extensions = getattr(agent_card, "extensions", None)
|
|
560
|
+
if extensions:
|
|
561
|
+
adcp_ext = extensions.get("adcp")
|
|
562
|
+
if adcp_ext:
|
|
563
|
+
info["adcp_version"] = adcp_ext.get("adcp_version")
|
|
564
|
+
info["protocols_supported"] = adcp_ext.get("protocols_supported")
|
|
454
565
|
|
|
455
566
|
logger.info(f"Retrieved agent info for {self.agent_config.id}")
|
|
456
567
|
return info
|