adcp 1.0.2__tar.gz → 1.0.4__tar.gz
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-1.0.2/src/adcp.egg-info → adcp-1.0.4}/PKG-INFO +1 -1
- {adcp-1.0.2 → adcp-1.0.4}/pyproject.toml +2 -1
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp/__init__.py +1 -1
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp/__main__.py +107 -8
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp/client.py +128 -73
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp/protocols/a2a.py +46 -4
- adcp-1.0.4/src/adcp/protocols/base.py +150 -0
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp/protocols/mcp.py +80 -3
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp/types/generated.py +19 -2
- adcp-1.0.4/src/adcp/types/tasks.py +511 -0
- adcp-1.0.4/src/adcp/utils/response_parser.py +122 -0
- {adcp-1.0.2 → adcp-1.0.4/src/adcp.egg-info}/PKG-INFO +1 -1
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp.egg-info/SOURCES.txt +4 -1
- {adcp-1.0.2 → adcp-1.0.4}/tests/test_cli.py +18 -14
- adcp-1.0.4/tests/test_client.py +382 -0
- {adcp-1.0.2 → adcp-1.0.4}/tests/test_code_generation.py +17 -38
- adcp-1.0.4/tests/test_format_id_validation.py +116 -0
- {adcp-1.0.2 → adcp-1.0.4}/tests/test_protocols.py +62 -5
- adcp-1.0.4/tests/test_response_parser.py +137 -0
- adcp-1.0.2/src/adcp/protocols/base.py +0 -49
- adcp-1.0.2/src/adcp/types/tasks.py +0 -281
- adcp-1.0.2/tests/test_client.py +0 -237
- {adcp-1.0.2 → adcp-1.0.4}/LICENSE +0 -0
- {adcp-1.0.2 → adcp-1.0.4}/README.md +0 -0
- {adcp-1.0.2 → adcp-1.0.4}/setup.cfg +0 -0
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp/config.py +0 -0
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp/exceptions.py +0 -0
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp/protocols/__init__.py +0 -0
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp/types/__init__.py +0 -0
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp/types/core.py +0 -0
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp/utils/__init__.py +0 -0
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp/utils/operation_id.py +0 -0
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp.egg-info/dependency_links.txt +0 -0
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp.egg-info/entry_points.txt +0 -0
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp.egg-info/requires.txt +0 -0
- {adcp-1.0.2 → adcp-1.0.4}/src/adcp.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "adcp"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.4"
|
|
8
8
|
description = "Official Python client for the Ad Context Protocol (AdCP)"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "AdCP Community", email = "maintainers@adcontextprotocol.org"}
|
|
@@ -58,6 +58,7 @@ where = ["src"]
|
|
|
58
58
|
[tool.black]
|
|
59
59
|
line-length = 100
|
|
60
60
|
target-version = ["py310", "py311", "py312"]
|
|
61
|
+
extend-exclude = "/(generated|tasks)\\.py$"
|
|
61
62
|
|
|
62
63
|
[tool.ruff]
|
|
63
64
|
line-length = 100
|
|
@@ -36,13 +36,15 @@ def print_result(result: Any, json_output: bool = False) -> None:
|
|
|
36
36
|
"data": result.data,
|
|
37
37
|
"error": result.error,
|
|
38
38
|
"metadata": result.metadata,
|
|
39
|
-
"debug_info":
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
"debug_info": (
|
|
40
|
+
{
|
|
41
|
+
"request": result.debug_info.request,
|
|
42
|
+
"response": result.debug_info.response,
|
|
43
|
+
"duration_ms": result.debug_info.duration_ms,
|
|
44
|
+
}
|
|
45
|
+
if result.debug_info
|
|
46
|
+
else None
|
|
47
|
+
),
|
|
46
48
|
}
|
|
47
49
|
)
|
|
48
50
|
else:
|
|
@@ -73,10 +75,107 @@ async def execute_tool(
|
|
|
73
75
|
config = AgentConfig(**agent_config)
|
|
74
76
|
|
|
75
77
|
async with ADCPClient(config) as client:
|
|
76
|
-
|
|
78
|
+
# Dispatch to specific method based on tool name
|
|
79
|
+
result = await _dispatch_tool(client, tool_name, payload)
|
|
77
80
|
print_result(result, json_output)
|
|
78
81
|
|
|
79
82
|
|
|
83
|
+
# Tool dispatch mapping - single source of truth for ADCP methods
|
|
84
|
+
# Types are filled at runtime to avoid circular imports
|
|
85
|
+
TOOL_DISPATCH: dict[str, tuple[str, type | None]] = {
|
|
86
|
+
"get_products": ("get_products", None),
|
|
87
|
+
"list_creative_formats": ("list_creative_formats", None),
|
|
88
|
+
"sync_creatives": ("sync_creatives", None),
|
|
89
|
+
"list_creatives": ("list_creatives", None),
|
|
90
|
+
"get_media_buy_delivery": ("get_media_buy_delivery", None),
|
|
91
|
+
"list_authorized_properties": ("list_authorized_properties", None),
|
|
92
|
+
"get_signals": ("get_signals", None),
|
|
93
|
+
"activate_signal": ("activate_signal", None),
|
|
94
|
+
"provide_performance_feedback": ("provide_performance_feedback", None),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def _dispatch_tool(client: ADCPClient, tool_name: str, payload: dict[str, Any]) -> Any:
|
|
99
|
+
"""Dispatch tool call to appropriate client method.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
client: ADCP client instance
|
|
103
|
+
tool_name: Name of the tool to invoke
|
|
104
|
+
payload: Request payload as dict
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
TaskResult with typed response or error
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
ValidationError: If payload doesn't match request schema (caught and returned as TaskResult)
|
|
111
|
+
"""
|
|
112
|
+
from pydantic import ValidationError
|
|
113
|
+
|
|
114
|
+
from adcp.types import generated as gen
|
|
115
|
+
from adcp.types.core import TaskResult, TaskStatus
|
|
116
|
+
|
|
117
|
+
# Lazy initialization of request types (avoid circular imports)
|
|
118
|
+
if TOOL_DISPATCH["get_products"][1] is None:
|
|
119
|
+
TOOL_DISPATCH["get_products"] = ("get_products", gen.GetProductsRequest)
|
|
120
|
+
TOOL_DISPATCH["list_creative_formats"] = (
|
|
121
|
+
"list_creative_formats",
|
|
122
|
+
gen.ListCreativeFormatsRequest,
|
|
123
|
+
)
|
|
124
|
+
TOOL_DISPATCH["sync_creatives"] = ("sync_creatives", gen.SyncCreativesRequest)
|
|
125
|
+
TOOL_DISPATCH["list_creatives"] = ("list_creatives", gen.ListCreativesRequest)
|
|
126
|
+
TOOL_DISPATCH["get_media_buy_delivery"] = (
|
|
127
|
+
"get_media_buy_delivery",
|
|
128
|
+
gen.GetMediaBuyDeliveryRequest,
|
|
129
|
+
)
|
|
130
|
+
TOOL_DISPATCH["list_authorized_properties"] = (
|
|
131
|
+
"list_authorized_properties",
|
|
132
|
+
gen.ListAuthorizedPropertiesRequest,
|
|
133
|
+
)
|
|
134
|
+
TOOL_DISPATCH["get_signals"] = ("get_signals", gen.GetSignalsRequest)
|
|
135
|
+
TOOL_DISPATCH["activate_signal"] = ("activate_signal", gen.ActivateSignalRequest)
|
|
136
|
+
TOOL_DISPATCH["provide_performance_feedback"] = (
|
|
137
|
+
"provide_performance_feedback",
|
|
138
|
+
gen.ProvidePerformanceFeedbackRequest,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Check if tool exists
|
|
142
|
+
if tool_name not in TOOL_DISPATCH:
|
|
143
|
+
available = ", ".join(sorted(TOOL_DISPATCH.keys()))
|
|
144
|
+
return TaskResult(
|
|
145
|
+
status=TaskStatus.FAILED,
|
|
146
|
+
error=f"Unknown tool: {tool_name}. Available tools: {available}",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Get method and request type
|
|
150
|
+
method_name, request_type = TOOL_DISPATCH[tool_name]
|
|
151
|
+
|
|
152
|
+
# Type guard - request_type should be initialized by this point
|
|
153
|
+
if request_type is None:
|
|
154
|
+
return TaskResult(
|
|
155
|
+
status=TaskStatus.FAILED,
|
|
156
|
+
error=f"Internal error: {tool_name} request type not initialized",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
method = getattr(client, method_name)
|
|
160
|
+
|
|
161
|
+
# Validate and invoke
|
|
162
|
+
try:
|
|
163
|
+
request = request_type(**payload)
|
|
164
|
+
return await method(request)
|
|
165
|
+
except ValidationError as e:
|
|
166
|
+
# User-friendly error for invalid payloads
|
|
167
|
+
error_details = []
|
|
168
|
+
for error in e.errors():
|
|
169
|
+
field = ".".join(str(loc) for loc in error["loc"])
|
|
170
|
+
msg = error["msg"]
|
|
171
|
+
error_details.append(f" - {field}: {msg}")
|
|
172
|
+
|
|
173
|
+
return TaskResult(
|
|
174
|
+
status=TaskStatus.FAILED,
|
|
175
|
+
error=f"Invalid request payload for {tool_name}:\n" + "\n".join(error_details),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
80
179
|
def load_payload(payload_arg: str | None) -> dict[str, Any]:
|
|
81
180
|
"""Load payload from argument (JSON, @file, or stdin)."""
|
|
82
181
|
if not payload_arg:
|
|
@@ -41,6 +41,7 @@ from adcp.types.generated import (
|
|
|
41
41
|
ProvidePerformanceFeedbackResponse,
|
|
42
42
|
SyncCreativesRequest,
|
|
43
43
|
SyncCreativesResponse,
|
|
44
|
+
WebhookPayload,
|
|
44
45
|
)
|
|
45
46
|
from adcp.utils.operation_id import create_operation_id
|
|
46
47
|
|
|
@@ -123,7 +124,7 @@ class ADCPClient:
|
|
|
123
124
|
)
|
|
124
125
|
)
|
|
125
126
|
|
|
126
|
-
|
|
127
|
+
raw_result = await self.adapter.get_products(params)
|
|
127
128
|
|
|
128
129
|
self._emit_activity(
|
|
129
130
|
Activity(
|
|
@@ -131,12 +132,12 @@ class ADCPClient:
|
|
|
131
132
|
operation_id=operation_id,
|
|
132
133
|
agent_id=self.agent_config.id,
|
|
133
134
|
task_type="get_products",
|
|
134
|
-
status=
|
|
135
|
+
status=raw_result.status,
|
|
135
136
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
136
137
|
)
|
|
137
138
|
)
|
|
138
139
|
|
|
139
|
-
return
|
|
140
|
+
return self.adapter._parse_response(raw_result, GetProductsResponse)
|
|
140
141
|
|
|
141
142
|
async def list_creative_formats(
|
|
142
143
|
self,
|
|
@@ -164,7 +165,7 @@ class ADCPClient:
|
|
|
164
165
|
)
|
|
165
166
|
)
|
|
166
167
|
|
|
167
|
-
|
|
168
|
+
raw_result = await self.adapter.list_creative_formats(params)
|
|
168
169
|
|
|
169
170
|
self._emit_activity(
|
|
170
171
|
Activity(
|
|
@@ -172,12 +173,13 @@ class ADCPClient:
|
|
|
172
173
|
operation_id=operation_id,
|
|
173
174
|
agent_id=self.agent_config.id,
|
|
174
175
|
task_type="list_creative_formats",
|
|
175
|
-
status=
|
|
176
|
+
status=raw_result.status,
|
|
176
177
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
177
178
|
)
|
|
178
179
|
)
|
|
179
180
|
|
|
180
|
-
|
|
181
|
+
# Parse response using adapter's helper
|
|
182
|
+
return self.adapter._parse_response(raw_result, ListCreativeFormatsResponse)
|
|
181
183
|
|
|
182
184
|
async def sync_creatives(
|
|
183
185
|
self,
|
|
@@ -205,7 +207,7 @@ class ADCPClient:
|
|
|
205
207
|
)
|
|
206
208
|
)
|
|
207
209
|
|
|
208
|
-
|
|
210
|
+
raw_result = await self.adapter.sync_creatives(params)
|
|
209
211
|
|
|
210
212
|
self._emit_activity(
|
|
211
213
|
Activity(
|
|
@@ -213,12 +215,12 @@ class ADCPClient:
|
|
|
213
215
|
operation_id=operation_id,
|
|
214
216
|
agent_id=self.agent_config.id,
|
|
215
217
|
task_type="sync_creatives",
|
|
216
|
-
status=
|
|
218
|
+
status=raw_result.status,
|
|
217
219
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
218
220
|
)
|
|
219
221
|
)
|
|
220
222
|
|
|
221
|
-
return
|
|
223
|
+
return self.adapter._parse_response(raw_result, SyncCreativesResponse)
|
|
222
224
|
|
|
223
225
|
async def list_creatives(
|
|
224
226
|
self,
|
|
@@ -246,7 +248,7 @@ class ADCPClient:
|
|
|
246
248
|
)
|
|
247
249
|
)
|
|
248
250
|
|
|
249
|
-
|
|
251
|
+
raw_result = await self.adapter.list_creatives(params)
|
|
250
252
|
|
|
251
253
|
self._emit_activity(
|
|
252
254
|
Activity(
|
|
@@ -254,12 +256,12 @@ class ADCPClient:
|
|
|
254
256
|
operation_id=operation_id,
|
|
255
257
|
agent_id=self.agent_config.id,
|
|
256
258
|
task_type="list_creatives",
|
|
257
|
-
status=
|
|
259
|
+
status=raw_result.status,
|
|
258
260
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
259
261
|
)
|
|
260
262
|
)
|
|
261
263
|
|
|
262
|
-
return
|
|
264
|
+
return self.adapter._parse_response(raw_result, ListCreativesResponse)
|
|
263
265
|
|
|
264
266
|
async def get_media_buy_delivery(
|
|
265
267
|
self,
|
|
@@ -287,7 +289,7 @@ class ADCPClient:
|
|
|
287
289
|
)
|
|
288
290
|
)
|
|
289
291
|
|
|
290
|
-
|
|
292
|
+
raw_result = await self.adapter.get_media_buy_delivery(params)
|
|
291
293
|
|
|
292
294
|
self._emit_activity(
|
|
293
295
|
Activity(
|
|
@@ -295,12 +297,12 @@ class ADCPClient:
|
|
|
295
297
|
operation_id=operation_id,
|
|
296
298
|
agent_id=self.agent_config.id,
|
|
297
299
|
task_type="get_media_buy_delivery",
|
|
298
|
-
status=
|
|
300
|
+
status=raw_result.status,
|
|
299
301
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
300
302
|
)
|
|
301
303
|
)
|
|
302
304
|
|
|
303
|
-
return
|
|
305
|
+
return self.adapter._parse_response(raw_result, GetMediaBuyDeliveryResponse)
|
|
304
306
|
|
|
305
307
|
async def list_authorized_properties(
|
|
306
308
|
self,
|
|
@@ -328,7 +330,7 @@ class ADCPClient:
|
|
|
328
330
|
)
|
|
329
331
|
)
|
|
330
332
|
|
|
331
|
-
|
|
333
|
+
raw_result = await self.adapter.list_authorized_properties(params)
|
|
332
334
|
|
|
333
335
|
self._emit_activity(
|
|
334
336
|
Activity(
|
|
@@ -336,12 +338,12 @@ class ADCPClient:
|
|
|
336
338
|
operation_id=operation_id,
|
|
337
339
|
agent_id=self.agent_config.id,
|
|
338
340
|
task_type="list_authorized_properties",
|
|
339
|
-
status=
|
|
341
|
+
status=raw_result.status,
|
|
340
342
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
341
343
|
)
|
|
342
344
|
)
|
|
343
345
|
|
|
344
|
-
return
|
|
346
|
+
return self.adapter._parse_response(raw_result, ListAuthorizedPropertiesResponse)
|
|
345
347
|
|
|
346
348
|
async def get_signals(
|
|
347
349
|
self,
|
|
@@ -369,7 +371,7 @@ class ADCPClient:
|
|
|
369
371
|
)
|
|
370
372
|
)
|
|
371
373
|
|
|
372
|
-
|
|
374
|
+
raw_result = await self.adapter.get_signals(params)
|
|
373
375
|
|
|
374
376
|
self._emit_activity(
|
|
375
377
|
Activity(
|
|
@@ -377,12 +379,12 @@ class ADCPClient:
|
|
|
377
379
|
operation_id=operation_id,
|
|
378
380
|
agent_id=self.agent_config.id,
|
|
379
381
|
task_type="get_signals",
|
|
380
|
-
status=
|
|
382
|
+
status=raw_result.status,
|
|
381
383
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
382
384
|
)
|
|
383
385
|
)
|
|
384
386
|
|
|
385
|
-
return
|
|
387
|
+
return self.adapter._parse_response(raw_result, GetSignalsResponse)
|
|
386
388
|
|
|
387
389
|
async def activate_signal(
|
|
388
390
|
self,
|
|
@@ -410,7 +412,7 @@ class ADCPClient:
|
|
|
410
412
|
)
|
|
411
413
|
)
|
|
412
414
|
|
|
413
|
-
|
|
415
|
+
raw_result = await self.adapter.activate_signal(params)
|
|
414
416
|
|
|
415
417
|
self._emit_activity(
|
|
416
418
|
Activity(
|
|
@@ -418,12 +420,12 @@ class ADCPClient:
|
|
|
418
420
|
operation_id=operation_id,
|
|
419
421
|
agent_id=self.agent_config.id,
|
|
420
422
|
task_type="activate_signal",
|
|
421
|
-
status=
|
|
423
|
+
status=raw_result.status,
|
|
422
424
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
423
425
|
)
|
|
424
426
|
)
|
|
425
427
|
|
|
426
|
-
return
|
|
428
|
+
return self.adapter._parse_response(raw_result, ActivateSignalResponse)
|
|
427
429
|
|
|
428
430
|
async def provide_performance_feedback(
|
|
429
431
|
self,
|
|
@@ -451,7 +453,7 @@ class ADCPClient:
|
|
|
451
453
|
)
|
|
452
454
|
)
|
|
453
455
|
|
|
454
|
-
|
|
456
|
+
raw_result = await self.adapter.provide_performance_feedback(params)
|
|
455
457
|
|
|
456
458
|
self._emit_activity(
|
|
457
459
|
Activity(
|
|
@@ -459,50 +461,12 @@ class ADCPClient:
|
|
|
459
461
|
operation_id=operation_id,
|
|
460
462
|
agent_id=self.agent_config.id,
|
|
461
463
|
task_type="provide_performance_feedback",
|
|
462
|
-
status=
|
|
464
|
+
status=raw_result.status,
|
|
463
465
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
464
466
|
)
|
|
465
467
|
)
|
|
466
468
|
|
|
467
|
-
return
|
|
468
|
-
|
|
469
|
-
async def call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
|
|
470
|
-
"""
|
|
471
|
-
Call any tool on the agent.
|
|
472
|
-
|
|
473
|
-
Args:
|
|
474
|
-
tool_name: Name of the tool to call
|
|
475
|
-
params: Tool parameters
|
|
476
|
-
|
|
477
|
-
Returns:
|
|
478
|
-
TaskResult with the response
|
|
479
|
-
"""
|
|
480
|
-
operation_id = create_operation_id()
|
|
481
|
-
|
|
482
|
-
self._emit_activity(
|
|
483
|
-
Activity(
|
|
484
|
-
type=ActivityType.PROTOCOL_REQUEST,
|
|
485
|
-
operation_id=operation_id,
|
|
486
|
-
agent_id=self.agent_config.id,
|
|
487
|
-
task_type=tool_name,
|
|
488
|
-
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
489
|
-
)
|
|
490
|
-
)
|
|
491
|
-
|
|
492
|
-
result = await self.adapter.call_tool(tool_name, params)
|
|
493
|
-
|
|
494
|
-
self._emit_activity(
|
|
495
|
-
Activity(
|
|
496
|
-
type=ActivityType.PROTOCOL_RESPONSE,
|
|
497
|
-
operation_id=operation_id,
|
|
498
|
-
agent_id=self.agent_config.id,
|
|
499
|
-
task_type=tool_name,
|
|
500
|
-
status=result.status,
|
|
501
|
-
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
502
|
-
)
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
return result
|
|
469
|
+
return self.adapter._parse_response(raw_result, ProvidePerformanceFeedbackResponse)
|
|
506
470
|
|
|
507
471
|
async def list_tools(self) -> list[str]:
|
|
508
472
|
"""
|
|
@@ -548,41 +512,132 @@ class ADCPClient:
|
|
|
548
512
|
|
|
549
513
|
return hmac.compare_digest(signature, expected_signature)
|
|
550
514
|
|
|
515
|
+
def _parse_webhook_result(self, webhook: WebhookPayload) -> TaskResult[Any]:
|
|
516
|
+
"""
|
|
517
|
+
Parse webhook payload into typed TaskResult based on task_type.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
webhook: Validated webhook payload
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
TaskResult with task-specific typed response data
|
|
524
|
+
"""
|
|
525
|
+
from adcp.types.core import TaskStatus
|
|
526
|
+
from adcp.utils.response_parser import parse_json_or_text
|
|
527
|
+
|
|
528
|
+
# Map task types to their response types (using string literals, not enum)
|
|
529
|
+
response_type_map: dict[str, type] = {
|
|
530
|
+
"get_products": GetProductsResponse,
|
|
531
|
+
"list_creative_formats": ListCreativeFormatsResponse,
|
|
532
|
+
"sync_creatives": SyncCreativesResponse,
|
|
533
|
+
"list_creatives": ListCreativesResponse,
|
|
534
|
+
"get_media_buy_delivery": GetMediaBuyDeliveryResponse,
|
|
535
|
+
"list_authorized_properties": ListAuthorizedPropertiesResponse,
|
|
536
|
+
"get_signals": GetSignalsResponse,
|
|
537
|
+
"activate_signal": ActivateSignalResponse,
|
|
538
|
+
"provide_performance_feedback": ProvidePerformanceFeedbackResponse,
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
# Handle completed tasks with result parsing
|
|
542
|
+
|
|
543
|
+
if webhook.status == "completed" and webhook.result is not None:
|
|
544
|
+
response_type = response_type_map.get(webhook.task_type)
|
|
545
|
+
if response_type:
|
|
546
|
+
try:
|
|
547
|
+
parsed_result: Any = parse_json_or_text(webhook.result, response_type)
|
|
548
|
+
return TaskResult[Any](
|
|
549
|
+
status=TaskStatus.COMPLETED,
|
|
550
|
+
data=parsed_result,
|
|
551
|
+
success=True,
|
|
552
|
+
metadata={
|
|
553
|
+
"task_id": webhook.task_id,
|
|
554
|
+
"operation_id": webhook.operation_id,
|
|
555
|
+
"timestamp": webhook.timestamp,
|
|
556
|
+
"message": webhook.message,
|
|
557
|
+
},
|
|
558
|
+
)
|
|
559
|
+
except ValueError as e:
|
|
560
|
+
logger.warning(f"Failed to parse webhook result: {e}")
|
|
561
|
+
# Fall through to untyped result
|
|
562
|
+
|
|
563
|
+
# Handle failed, input-required, or unparseable results
|
|
564
|
+
# Convert webhook status string to TaskStatus enum
|
|
565
|
+
try:
|
|
566
|
+
task_status = TaskStatus(webhook.status)
|
|
567
|
+
except ValueError:
|
|
568
|
+
# Fallback to FAILED for unknown statuses
|
|
569
|
+
task_status = TaskStatus.FAILED
|
|
570
|
+
|
|
571
|
+
return TaskResult[Any](
|
|
572
|
+
status=task_status,
|
|
573
|
+
data=webhook.result,
|
|
574
|
+
success=webhook.status == "completed",
|
|
575
|
+
error=webhook.error if isinstance(webhook.error, str) else None,
|
|
576
|
+
metadata={
|
|
577
|
+
"task_id": webhook.task_id,
|
|
578
|
+
"operation_id": webhook.operation_id,
|
|
579
|
+
"timestamp": webhook.timestamp,
|
|
580
|
+
"message": webhook.message,
|
|
581
|
+
"context_id": webhook.context_id,
|
|
582
|
+
"progress": webhook.progress,
|
|
583
|
+
},
|
|
584
|
+
)
|
|
585
|
+
|
|
551
586
|
async def handle_webhook(
|
|
552
587
|
self,
|
|
553
588
|
payload: dict[str, Any],
|
|
554
589
|
signature: str | None = None,
|
|
555
|
-
) ->
|
|
590
|
+
) -> TaskResult[Any]:
|
|
556
591
|
"""
|
|
557
|
-
Handle incoming webhook.
|
|
592
|
+
Handle incoming webhook and return typed result.
|
|
593
|
+
|
|
594
|
+
This method:
|
|
595
|
+
1. Verifies webhook signature (if provided)
|
|
596
|
+
2. Validates payload against WebhookPayload schema
|
|
597
|
+
3. Parses task-specific result data into typed response
|
|
598
|
+
4. Emits activity for monitoring
|
|
558
599
|
|
|
559
600
|
Args:
|
|
560
|
-
payload: Webhook payload
|
|
561
|
-
signature:
|
|
601
|
+
payload: Webhook payload dict
|
|
602
|
+
signature: Optional HMAC-SHA256 signature for verification
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
TaskResult with parsed task-specific response data
|
|
562
606
|
|
|
563
607
|
Raises:
|
|
564
608
|
ADCPWebhookSignatureError: If signature verification fails
|
|
609
|
+
ValidationError: If payload doesn't match WebhookPayload schema
|
|
610
|
+
|
|
611
|
+
Example:
|
|
612
|
+
>>> result = await client.handle_webhook(payload, signature)
|
|
613
|
+
>>> if result.success and isinstance(result.data, GetProductsResponse):
|
|
614
|
+
>>> print(f"Found {len(result.data.products)} products")
|
|
565
615
|
"""
|
|
616
|
+
# Verify signature before processing
|
|
566
617
|
if signature and not self._verify_webhook_signature(payload, signature):
|
|
567
618
|
logger.warning(
|
|
568
619
|
f"Webhook signature verification failed for agent {self.agent_config.id}"
|
|
569
620
|
)
|
|
570
621
|
raise ADCPWebhookSignatureError("Invalid webhook signature")
|
|
571
622
|
|
|
572
|
-
|
|
573
|
-
|
|
623
|
+
# Validate and parse webhook payload
|
|
624
|
+
webhook = WebhookPayload.model_validate(payload)
|
|
574
625
|
|
|
626
|
+
# Emit activity for monitoring
|
|
575
627
|
self._emit_activity(
|
|
576
628
|
Activity(
|
|
577
629
|
type=ActivityType.WEBHOOK_RECEIVED,
|
|
578
|
-
operation_id=operation_id,
|
|
630
|
+
operation_id=webhook.operation_id or "unknown",
|
|
579
631
|
agent_id=self.agent_config.id,
|
|
580
|
-
task_type=task_type,
|
|
632
|
+
task_type=webhook.task_type,
|
|
581
633
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
582
634
|
metadata={"payload": payload},
|
|
583
635
|
)
|
|
584
636
|
)
|
|
585
637
|
|
|
638
|
+
# Parse and return typed result
|
|
639
|
+
return self._parse_webhook_result(webhook)
|
|
640
|
+
|
|
586
641
|
|
|
587
642
|
class ADCPMultiAgentClient:
|
|
588
643
|
"""Client for managing multiple AdCP agents."""
|
|
@@ -54,7 +54,7 @@ class A2AAdapter(ProtocolAdapter):
|
|
|
54
54
|
await self._client.aclose()
|
|
55
55
|
self._client = None
|
|
56
56
|
|
|
57
|
-
async def
|
|
57
|
+
async def _call_a2a_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
|
|
58
58
|
"""
|
|
59
59
|
Call a tool using A2A protocol.
|
|
60
60
|
|
|
@@ -98,9 +98,11 @@ class A2AAdapter(ProtocolAdapter):
|
|
|
98
98
|
"url": url,
|
|
99
99
|
"method": "POST",
|
|
100
100
|
"headers": {
|
|
101
|
-
k:
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
k: (
|
|
102
|
+
v
|
|
103
|
+
if k.lower() not in ("authorization", self.agent_config.auth_header.lower())
|
|
104
|
+
else "***"
|
|
105
|
+
)
|
|
104
106
|
for k, v in headers.items()
|
|
105
107
|
},
|
|
106
108
|
"body": request_data,
|
|
@@ -202,6 +204,46 @@ class A2AAdapter(ProtocolAdapter):
|
|
|
202
204
|
|
|
203
205
|
return first_part
|
|
204
206
|
|
|
207
|
+
# ========================================================================
|
|
208
|
+
# ADCP Protocol Methods
|
|
209
|
+
# ========================================================================
|
|
210
|
+
|
|
211
|
+
async def get_products(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
212
|
+
"""Get advertising products."""
|
|
213
|
+
return await self._call_a2a_tool("get_products", params)
|
|
214
|
+
|
|
215
|
+
async def list_creative_formats(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
216
|
+
"""List supported creative formats."""
|
|
217
|
+
return await self._call_a2a_tool("list_creative_formats", params)
|
|
218
|
+
|
|
219
|
+
async def sync_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
220
|
+
"""Sync creatives."""
|
|
221
|
+
return await self._call_a2a_tool("sync_creatives", params)
|
|
222
|
+
|
|
223
|
+
async def list_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
224
|
+
"""List creatives."""
|
|
225
|
+
return await self._call_a2a_tool("list_creatives", params)
|
|
226
|
+
|
|
227
|
+
async def get_media_buy_delivery(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
228
|
+
"""Get media buy delivery."""
|
|
229
|
+
return await self._call_a2a_tool("get_media_buy_delivery", params)
|
|
230
|
+
|
|
231
|
+
async def list_authorized_properties(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
232
|
+
"""List authorized properties."""
|
|
233
|
+
return await self._call_a2a_tool("list_authorized_properties", params)
|
|
234
|
+
|
|
235
|
+
async def get_signals(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
236
|
+
"""Get signals."""
|
|
237
|
+
return await self._call_a2a_tool("get_signals", params)
|
|
238
|
+
|
|
239
|
+
async def activate_signal(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
240
|
+
"""Activate signal."""
|
|
241
|
+
return await self._call_a2a_tool("activate_signal", params)
|
|
242
|
+
|
|
243
|
+
async def provide_performance_feedback(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
244
|
+
"""Provide performance feedback."""
|
|
245
|
+
return await self._call_a2a_tool("provide_performance_feedback", params)
|
|
246
|
+
|
|
205
247
|
async def list_tools(self) -> list[str]:
|
|
206
248
|
"""
|
|
207
249
|
List available tools from A2A agent.
|