adcp 2.12.0__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 +364 -0
- adcp/__main__.py +440 -0
- adcp/adagents.py +642 -0
- adcp/client.py +1057 -0
- adcp/config.py +82 -0
- adcp/exceptions.py +185 -0
- adcp/protocols/__init__.py +9 -0
- adcp/protocols/a2a.py +484 -0
- adcp/protocols/base.py +190 -0
- adcp/protocols/mcp.py +440 -0
- adcp/py.typed +0 -0
- adcp/simple.py +451 -0
- adcp/testing/__init__.py +53 -0
- adcp/testing/test_helpers.py +311 -0
- adcp/types/__init__.py +561 -0
- adcp/types/_generated.py +237 -0
- adcp/types/aliases.py +748 -0
- adcp/types/base.py +26 -0
- adcp/types/core.py +174 -0
- adcp/types/generated_poc/__init__.py +3 -0
- adcp/types/generated_poc/adagents.py +411 -0
- adcp/types/generated_poc/core/__init__.py +3 -0
- adcp/types/generated_poc/core/activation_key.py +30 -0
- adcp/types/generated_poc/core/assets/__init__.py +3 -0
- adcp/types/generated_poc/core/assets/audio_asset.py +26 -0
- adcp/types/generated_poc/core/assets/css_asset.py +20 -0
- adcp/types/generated_poc/core/assets/daast_asset.py +61 -0
- adcp/types/generated_poc/core/assets/html_asset.py +18 -0
- adcp/types/generated_poc/core/assets/image_asset.py +19 -0
- adcp/types/generated_poc/core/assets/javascript_asset.py +23 -0
- adcp/types/generated_poc/core/assets/text_asset.py +20 -0
- adcp/types/generated_poc/core/assets/url_asset.py +28 -0
- adcp/types/generated_poc/core/assets/vast_asset.py +63 -0
- adcp/types/generated_poc/core/assets/video_asset.py +24 -0
- adcp/types/generated_poc/core/assets/webhook_asset.py +53 -0
- adcp/types/generated_poc/core/brand_manifest.py +201 -0
- adcp/types/generated_poc/core/context.py +15 -0
- adcp/types/generated_poc/core/creative_asset.py +102 -0
- adcp/types/generated_poc/core/creative_assignment.py +27 -0
- adcp/types/generated_poc/core/creative_filters.py +86 -0
- adcp/types/generated_poc/core/creative_manifest.py +68 -0
- adcp/types/generated_poc/core/creative_policy.py +28 -0
- adcp/types/generated_poc/core/delivery_metrics.py +111 -0
- adcp/types/generated_poc/core/deployment.py +78 -0
- adcp/types/generated_poc/core/destination.py +43 -0
- adcp/types/generated_poc/core/dimensions.py +18 -0
- adcp/types/generated_poc/core/error.py +29 -0
- adcp/types/generated_poc/core/ext.py +15 -0
- adcp/types/generated_poc/core/format.py +260 -0
- adcp/types/generated_poc/core/format_id.py +50 -0
- adcp/types/generated_poc/core/frequency_cap.py +19 -0
- adcp/types/generated_poc/core/measurement.py +40 -0
- adcp/types/generated_poc/core/media_buy.py +40 -0
- adcp/types/generated_poc/core/package.py +68 -0
- adcp/types/generated_poc/core/performance_feedback.py +78 -0
- adcp/types/generated_poc/core/placement.py +37 -0
- adcp/types/generated_poc/core/product.py +164 -0
- adcp/types/generated_poc/core/product_filters.py +97 -0
- adcp/types/generated_poc/core/promoted_offerings.py +102 -0
- adcp/types/generated_poc/core/promoted_products.py +38 -0
- adcp/types/generated_poc/core/property.py +64 -0
- adcp/types/generated_poc/core/property_id.py +21 -0
- adcp/types/generated_poc/core/property_tag.py +21 -0
- adcp/types/generated_poc/core/protocol_envelope.py +61 -0
- adcp/types/generated_poc/core/publisher_property_selector.py +75 -0
- adcp/types/generated_poc/core/push_notification_config.py +51 -0
- adcp/types/generated_poc/core/reporting_capabilities.py +51 -0
- adcp/types/generated_poc/core/response.py +24 -0
- adcp/types/generated_poc/core/signal_filters.py +29 -0
- adcp/types/generated_poc/core/sub_asset.py +55 -0
- adcp/types/generated_poc/core/targeting.py +53 -0
- adcp/types/generated_poc/core/webhook_payload.py +96 -0
- adcp/types/generated_poc/creative/__init__.py +3 -0
- adcp/types/generated_poc/creative/list_creative_formats_request.py +88 -0
- adcp/types/generated_poc/creative/list_creative_formats_response.py +55 -0
- adcp/types/generated_poc/creative/preview_creative_request.py +153 -0
- adcp/types/generated_poc/creative/preview_creative_response.py +169 -0
- adcp/types/generated_poc/creative/preview_render.py +152 -0
- adcp/types/generated_poc/enums/__init__.py +3 -0
- adcp/types/generated_poc/enums/adcp_domain.py +12 -0
- adcp/types/generated_poc/enums/asset_content_type.py +23 -0
- adcp/types/generated_poc/enums/auth_scheme.py +12 -0
- adcp/types/generated_poc/enums/available_metric.py +19 -0
- adcp/types/generated_poc/enums/channels.py +19 -0
- adcp/types/generated_poc/enums/co_branding_requirement.py +13 -0
- adcp/types/generated_poc/enums/creative_action.py +15 -0
- adcp/types/generated_poc/enums/creative_agent_capability.py +14 -0
- adcp/types/generated_poc/enums/creative_sort_field.py +16 -0
- adcp/types/generated_poc/enums/creative_status.py +14 -0
- adcp/types/generated_poc/enums/daast_tracking_event.py +21 -0
- adcp/types/generated_poc/enums/daast_version.py +12 -0
- adcp/types/generated_poc/enums/delivery_type.py +12 -0
- adcp/types/generated_poc/enums/dimension_unit.py +14 -0
- adcp/types/generated_poc/enums/feed_format.py +13 -0
- adcp/types/generated_poc/enums/feedback_source.py +14 -0
- adcp/types/generated_poc/enums/format_category.py +17 -0
- adcp/types/generated_poc/enums/format_id_parameter.py +12 -0
- adcp/types/generated_poc/enums/frequency_cap_scope.py +16 -0
- adcp/types/generated_poc/enums/history_entry_type.py +12 -0
- adcp/types/generated_poc/enums/http_method.py +12 -0
- adcp/types/generated_poc/enums/identifier_types.py +29 -0
- adcp/types/generated_poc/enums/javascript_module_type.py +13 -0
- adcp/types/generated_poc/enums/landing_page_requirement.py +13 -0
- adcp/types/generated_poc/enums/markdown_flavor.py +12 -0
- adcp/types/generated_poc/enums/media_buy_status.py +14 -0
- adcp/types/generated_poc/enums/metric_type.py +18 -0
- adcp/types/generated_poc/enums/notification_type.py +14 -0
- adcp/types/generated_poc/enums/pacing.py +13 -0
- adcp/types/generated_poc/enums/preview_output_format.py +12 -0
- adcp/types/generated_poc/enums/pricing_model.py +17 -0
- adcp/types/generated_poc/enums/property_type.py +17 -0
- adcp/types/generated_poc/enums/publisher_identifier_types.py +15 -0
- adcp/types/generated_poc/enums/reporting_frequency.py +13 -0
- adcp/types/generated_poc/enums/signal_catalog_type.py +13 -0
- adcp/types/generated_poc/enums/sort_direction.py +12 -0
- adcp/types/generated_poc/enums/standard_format_ids.py +45 -0
- adcp/types/generated_poc/enums/task_status.py +19 -0
- adcp/types/generated_poc/enums/task_type.py +15 -0
- adcp/types/generated_poc/enums/update_frequency.py +14 -0
- adcp/types/generated_poc/enums/url_asset_type.py +13 -0
- adcp/types/generated_poc/enums/validation_mode.py +12 -0
- adcp/types/generated_poc/enums/vast_tracking_event.py +26 -0
- adcp/types/generated_poc/enums/vast_version.py +15 -0
- adcp/types/generated_poc/enums/webhook_response_type.py +14 -0
- adcp/types/generated_poc/enums/webhook_security_method.py +13 -0
- adcp/types/generated_poc/media_buy/__init__.py +3 -0
- adcp/types/generated_poc/media_buy/build_creative_request.py +41 -0
- adcp/types/generated_poc/media_buy/build_creative_response.py +51 -0
- adcp/types/generated_poc/media_buy/create_media_buy_request.py +94 -0
- adcp/types/generated_poc/media_buy/create_media_buy_response.py +56 -0
- adcp/types/generated_poc/media_buy/get_media_buy_delivery_request.py +47 -0
- adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py +235 -0
- adcp/types/generated_poc/media_buy/get_products_request.py +48 -0
- adcp/types/generated_poc/media_buy/get_products_response.py +28 -0
- adcp/types/generated_poc/media_buy/list_authorized_properties_request.py +38 -0
- adcp/types/generated_poc/media_buy/list_authorized_properties_response.py +84 -0
- adcp/types/generated_poc/media_buy/list_creative_formats_request.py +74 -0
- adcp/types/generated_poc/media_buy/list_creative_formats_response.py +56 -0
- adcp/types/generated_poc/media_buy/list_creatives_request.py +76 -0
- adcp/types/generated_poc/media_buy/list_creatives_response.py +214 -0
- adcp/types/generated_poc/media_buy/package_request.py +63 -0
- adcp/types/generated_poc/media_buy/provide_performance_feedback_request.py +125 -0
- adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py +53 -0
- adcp/types/generated_poc/media_buy/sync_creatives_request.py +63 -0
- adcp/types/generated_poc/media_buy/sync_creatives_response.py +105 -0
- adcp/types/generated_poc/media_buy/update_media_buy_request.py +195 -0
- adcp/types/generated_poc/media_buy/update_media_buy_response.py +55 -0
- adcp/types/generated_poc/pricing_options/__init__.py +3 -0
- adcp/types/generated_poc/pricing_options/cpc_option.py +43 -0
- adcp/types/generated_poc/pricing_options/cpcv_option.py +45 -0
- adcp/types/generated_poc/pricing_options/cpm_auction_option.py +58 -0
- adcp/types/generated_poc/pricing_options/cpm_fixed_option.py +43 -0
- adcp/types/generated_poc/pricing_options/cpp_option.py +64 -0
- adcp/types/generated_poc/pricing_options/cpv_option.py +77 -0
- adcp/types/generated_poc/pricing_options/flat_rate_option.py +93 -0
- adcp/types/generated_poc/pricing_options/vcpm_auction_option.py +61 -0
- adcp/types/generated_poc/pricing_options/vcpm_fixed_option.py +47 -0
- adcp/types/generated_poc/protocols/__init__.py +3 -0
- adcp/types/generated_poc/protocols/adcp_extension.py +37 -0
- adcp/types/generated_poc/signals/__init__.py +3 -0
- adcp/types/generated_poc/signals/activate_signal_request.py +32 -0
- adcp/types/generated_poc/signals/activate_signal_response.py +51 -0
- adcp/types/generated_poc/signals/get_signals_request.py +53 -0
- adcp/types/generated_poc/signals/get_signals_response.py +59 -0
- adcp/utils/__init__.py +7 -0
- adcp/utils/operation_id.py +15 -0
- adcp/utils/preview_cache.py +491 -0
- adcp/utils/response_parser.py +171 -0
- adcp/validation.py +172 -0
- adcp-2.12.0.data/data/ADCP_VERSION +1 -0
- adcp-2.12.0.dist-info/METADATA +992 -0
- adcp-2.12.0.dist-info/RECORD +176 -0
- adcp-2.12.0.dist-info/WHEEL +5 -0
- adcp-2.12.0.dist-info/entry_points.txt +2 -0
- adcp-2.12.0.dist-info/licenses/LICENSE +17 -0
- adcp-2.12.0.dist-info/top_level.txt +1 -0
adcp/client.py
ADDED
|
@@ -0,0 +1,1057 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Main client classes for AdCP."""
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from adcp.exceptions import ADCPWebhookSignatureError
|
|
17
|
+
from adcp.protocols.a2a import A2AAdapter
|
|
18
|
+
from adcp.protocols.base import ProtocolAdapter
|
|
19
|
+
from adcp.protocols.mcp import MCPAdapter
|
|
20
|
+
from adcp.types import (
|
|
21
|
+
ActivateSignalRequest,
|
|
22
|
+
ActivateSignalResponse,
|
|
23
|
+
BuildCreativeRequest,
|
|
24
|
+
BuildCreativeResponse,
|
|
25
|
+
CreateMediaBuyRequest,
|
|
26
|
+
CreateMediaBuyResponse,
|
|
27
|
+
GeneratedTaskStatus,
|
|
28
|
+
GetMediaBuyDeliveryRequest,
|
|
29
|
+
GetMediaBuyDeliveryResponse,
|
|
30
|
+
GetProductsRequest,
|
|
31
|
+
GetProductsResponse,
|
|
32
|
+
GetSignalsRequest,
|
|
33
|
+
GetSignalsResponse,
|
|
34
|
+
ListAuthorizedPropertiesRequest,
|
|
35
|
+
ListAuthorizedPropertiesResponse,
|
|
36
|
+
ListCreativeFormatsRequest,
|
|
37
|
+
ListCreativeFormatsResponse,
|
|
38
|
+
ListCreativesRequest,
|
|
39
|
+
ListCreativesResponse,
|
|
40
|
+
PreviewCreativeRequest,
|
|
41
|
+
PreviewCreativeResponse,
|
|
42
|
+
ProvidePerformanceFeedbackRequest,
|
|
43
|
+
ProvidePerformanceFeedbackResponse,
|
|
44
|
+
SyncCreativesRequest,
|
|
45
|
+
SyncCreativesResponse,
|
|
46
|
+
UpdateMediaBuyRequest,
|
|
47
|
+
UpdateMediaBuyResponse,
|
|
48
|
+
WebhookPayload,
|
|
49
|
+
)
|
|
50
|
+
from adcp.types.core import (
|
|
51
|
+
Activity,
|
|
52
|
+
ActivityType,
|
|
53
|
+
AgentConfig,
|
|
54
|
+
Protocol,
|
|
55
|
+
TaskResult,
|
|
56
|
+
TaskStatus,
|
|
57
|
+
)
|
|
58
|
+
from adcp.utils.operation_id import create_operation_id
|
|
59
|
+
|
|
60
|
+
logger = logging.getLogger(__name__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ADCPClient:
|
|
64
|
+
"""Client for interacting with a single AdCP agent."""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
agent_config: AgentConfig,
|
|
69
|
+
webhook_url_template: str | None = None,
|
|
70
|
+
webhook_secret: str | None = None,
|
|
71
|
+
on_activity: Callable[[Activity], None] | None = None,
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Initialize ADCP client for a single agent.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
agent_config: Agent configuration
|
|
78
|
+
webhook_url_template: Template for webhook URLs with {agent_id},
|
|
79
|
+
{task_type}, {operation_id}
|
|
80
|
+
webhook_secret: Secret for webhook signature verification
|
|
81
|
+
on_activity: Callback for activity events
|
|
82
|
+
"""
|
|
83
|
+
self.agent_config = agent_config
|
|
84
|
+
self.webhook_url_template = webhook_url_template
|
|
85
|
+
self.webhook_secret = webhook_secret
|
|
86
|
+
self.on_activity = on_activity
|
|
87
|
+
|
|
88
|
+
# Initialize protocol adapter
|
|
89
|
+
self.adapter: ProtocolAdapter
|
|
90
|
+
if agent_config.protocol == Protocol.A2A:
|
|
91
|
+
self.adapter = A2AAdapter(agent_config)
|
|
92
|
+
elif agent_config.protocol == Protocol.MCP:
|
|
93
|
+
self.adapter = MCPAdapter(agent_config)
|
|
94
|
+
else:
|
|
95
|
+
raise ValueError(f"Unsupported protocol: {agent_config.protocol}")
|
|
96
|
+
|
|
97
|
+
# Initialize simple API accessor (lazy import to avoid circular dependency)
|
|
98
|
+
from adcp.simple import SimpleAPI
|
|
99
|
+
|
|
100
|
+
self.simple = SimpleAPI(self)
|
|
101
|
+
|
|
102
|
+
def get_webhook_url(self, task_type: str, operation_id: str) -> str:
|
|
103
|
+
"""Generate webhook URL for a task."""
|
|
104
|
+
if not self.webhook_url_template:
|
|
105
|
+
raise ValueError("webhook_url_template not configured")
|
|
106
|
+
|
|
107
|
+
return self.webhook_url_template.format(
|
|
108
|
+
agent_id=self.agent_config.id,
|
|
109
|
+
task_type=task_type,
|
|
110
|
+
operation_id=operation_id,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _emit_activity(self, activity: Activity) -> None:
|
|
114
|
+
"""Emit activity event."""
|
|
115
|
+
if self.on_activity:
|
|
116
|
+
self.on_activity(activity)
|
|
117
|
+
|
|
118
|
+
async def get_products(
|
|
119
|
+
self,
|
|
120
|
+
request: GetProductsRequest,
|
|
121
|
+
fetch_previews: bool = False,
|
|
122
|
+
preview_output_format: str = "url",
|
|
123
|
+
creative_agent_client: ADCPClient | None = None,
|
|
124
|
+
) -> TaskResult[GetProductsResponse]:
|
|
125
|
+
"""
|
|
126
|
+
Get advertising products.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
request: Request parameters
|
|
130
|
+
fetch_previews: If True, generate preview URLs for each product's formats
|
|
131
|
+
(uses batch API for 5-10x performance improvement)
|
|
132
|
+
preview_output_format: "url" for iframe URLs (default), "html" for direct
|
|
133
|
+
embedding (2-3x faster, no iframe overhead)
|
|
134
|
+
creative_agent_client: Client for creative agent (required if
|
|
135
|
+
fetch_previews=True)
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
TaskResult containing GetProductsResponse with optional preview URLs in metadata
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: If fetch_previews=True but creative_agent_client is not provided
|
|
142
|
+
"""
|
|
143
|
+
if fetch_previews and not creative_agent_client:
|
|
144
|
+
raise ValueError("creative_agent_client is required when fetch_previews=True")
|
|
145
|
+
|
|
146
|
+
operation_id = create_operation_id()
|
|
147
|
+
params = request.model_dump(exclude_none=True)
|
|
148
|
+
|
|
149
|
+
self._emit_activity(
|
|
150
|
+
Activity(
|
|
151
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
152
|
+
operation_id=operation_id,
|
|
153
|
+
agent_id=self.agent_config.id,
|
|
154
|
+
task_type="get_products",
|
|
155
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
raw_result = await self.adapter.get_products(params)
|
|
160
|
+
|
|
161
|
+
self._emit_activity(
|
|
162
|
+
Activity(
|
|
163
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
164
|
+
operation_id=operation_id,
|
|
165
|
+
agent_id=self.agent_config.id,
|
|
166
|
+
task_type="get_products",
|
|
167
|
+
status=raw_result.status,
|
|
168
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
result: TaskResult[GetProductsResponse] = self.adapter._parse_response(
|
|
173
|
+
raw_result, GetProductsResponse
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if fetch_previews and result.success and result.data and creative_agent_client:
|
|
177
|
+
from adcp.utils.preview_cache import add_preview_urls_to_products
|
|
178
|
+
|
|
179
|
+
products_with_previews = await add_preview_urls_to_products(
|
|
180
|
+
result.data.products,
|
|
181
|
+
creative_agent_client,
|
|
182
|
+
use_batch=True,
|
|
183
|
+
output_format=preview_output_format,
|
|
184
|
+
)
|
|
185
|
+
result.metadata = result.metadata or {}
|
|
186
|
+
result.metadata["products_with_previews"] = products_with_previews
|
|
187
|
+
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
async def list_creative_formats(
|
|
191
|
+
self,
|
|
192
|
+
request: ListCreativeFormatsRequest,
|
|
193
|
+
fetch_previews: bool = False,
|
|
194
|
+
preview_output_format: str = "url",
|
|
195
|
+
) -> TaskResult[ListCreativeFormatsResponse]:
|
|
196
|
+
"""
|
|
197
|
+
List supported creative formats.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
request: Request parameters
|
|
201
|
+
fetch_previews: If True, generate preview URLs for each format using
|
|
202
|
+
sample manifests (uses batch API for 5-10x performance improvement)
|
|
203
|
+
preview_output_format: "url" for iframe URLs (default), "html" for direct
|
|
204
|
+
embedding (2-3x faster, no iframe overhead)
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
TaskResult containing ListCreativeFormatsResponse with optional preview URLs in metadata
|
|
208
|
+
"""
|
|
209
|
+
operation_id = create_operation_id()
|
|
210
|
+
params = request.model_dump(exclude_none=True)
|
|
211
|
+
|
|
212
|
+
self._emit_activity(
|
|
213
|
+
Activity(
|
|
214
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
215
|
+
operation_id=operation_id,
|
|
216
|
+
agent_id=self.agent_config.id,
|
|
217
|
+
task_type="list_creative_formats",
|
|
218
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
raw_result = await self.adapter.list_creative_formats(params)
|
|
223
|
+
|
|
224
|
+
self._emit_activity(
|
|
225
|
+
Activity(
|
|
226
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
227
|
+
operation_id=operation_id,
|
|
228
|
+
agent_id=self.agent_config.id,
|
|
229
|
+
task_type="list_creative_formats",
|
|
230
|
+
status=raw_result.status,
|
|
231
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
result: TaskResult[ListCreativeFormatsResponse] = self.adapter._parse_response(
|
|
236
|
+
raw_result, ListCreativeFormatsResponse
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if fetch_previews and result.success and result.data:
|
|
240
|
+
from adcp.utils.preview_cache import add_preview_urls_to_formats
|
|
241
|
+
|
|
242
|
+
formats_with_previews = await add_preview_urls_to_formats(
|
|
243
|
+
result.data.formats,
|
|
244
|
+
self,
|
|
245
|
+
use_batch=True,
|
|
246
|
+
output_format=preview_output_format,
|
|
247
|
+
)
|
|
248
|
+
result.metadata = result.metadata or {}
|
|
249
|
+
result.metadata["formats_with_previews"] = formats_with_previews
|
|
250
|
+
|
|
251
|
+
return result
|
|
252
|
+
|
|
253
|
+
async def preview_creative(
|
|
254
|
+
self,
|
|
255
|
+
request: PreviewCreativeRequest,
|
|
256
|
+
) -> TaskResult[PreviewCreativeResponse]:
|
|
257
|
+
"""
|
|
258
|
+
Generate preview of a creative manifest.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
request: Request parameters
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
TaskResult containing PreviewCreativeResponse with preview URLs
|
|
265
|
+
"""
|
|
266
|
+
operation_id = create_operation_id()
|
|
267
|
+
params = request.model_dump(exclude_none=True)
|
|
268
|
+
|
|
269
|
+
self._emit_activity(
|
|
270
|
+
Activity(
|
|
271
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
272
|
+
operation_id=operation_id,
|
|
273
|
+
agent_id=self.agent_config.id,
|
|
274
|
+
task_type="preview_creative",
|
|
275
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
raw_result = await self.adapter.preview_creative(params) # type: ignore[attr-defined]
|
|
280
|
+
|
|
281
|
+
self._emit_activity(
|
|
282
|
+
Activity(
|
|
283
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
284
|
+
operation_id=operation_id,
|
|
285
|
+
agent_id=self.agent_config.id,
|
|
286
|
+
task_type="preview_creative",
|
|
287
|
+
status=raw_result.status,
|
|
288
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return self.adapter._parse_response(raw_result, PreviewCreativeResponse)
|
|
293
|
+
|
|
294
|
+
async def sync_creatives(
|
|
295
|
+
self,
|
|
296
|
+
request: SyncCreativesRequest,
|
|
297
|
+
) -> TaskResult[SyncCreativesResponse]:
|
|
298
|
+
"""
|
|
299
|
+
Sync Creatives.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
request: Request parameters
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
TaskResult containing SyncCreativesResponse
|
|
306
|
+
"""
|
|
307
|
+
operation_id = create_operation_id()
|
|
308
|
+
params = request.model_dump(exclude_none=True)
|
|
309
|
+
|
|
310
|
+
self._emit_activity(
|
|
311
|
+
Activity(
|
|
312
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
313
|
+
operation_id=operation_id,
|
|
314
|
+
agent_id=self.agent_config.id,
|
|
315
|
+
task_type="sync_creatives",
|
|
316
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
raw_result = await self.adapter.sync_creatives(params)
|
|
321
|
+
|
|
322
|
+
self._emit_activity(
|
|
323
|
+
Activity(
|
|
324
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
325
|
+
operation_id=operation_id,
|
|
326
|
+
agent_id=self.agent_config.id,
|
|
327
|
+
task_type="sync_creatives",
|
|
328
|
+
status=raw_result.status,
|
|
329
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return self.adapter._parse_response(raw_result, SyncCreativesResponse)
|
|
334
|
+
|
|
335
|
+
async def list_creatives(
|
|
336
|
+
self,
|
|
337
|
+
request: ListCreativesRequest,
|
|
338
|
+
) -> TaskResult[ListCreativesResponse]:
|
|
339
|
+
"""
|
|
340
|
+
List Creatives.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
request: Request parameters
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
TaskResult containing ListCreativesResponse
|
|
347
|
+
"""
|
|
348
|
+
operation_id = create_operation_id()
|
|
349
|
+
params = request.model_dump(exclude_none=True)
|
|
350
|
+
|
|
351
|
+
self._emit_activity(
|
|
352
|
+
Activity(
|
|
353
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
354
|
+
operation_id=operation_id,
|
|
355
|
+
agent_id=self.agent_config.id,
|
|
356
|
+
task_type="list_creatives",
|
|
357
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
raw_result = await self.adapter.list_creatives(params)
|
|
362
|
+
|
|
363
|
+
self._emit_activity(
|
|
364
|
+
Activity(
|
|
365
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
366
|
+
operation_id=operation_id,
|
|
367
|
+
agent_id=self.agent_config.id,
|
|
368
|
+
task_type="list_creatives",
|
|
369
|
+
status=raw_result.status,
|
|
370
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
return self.adapter._parse_response(raw_result, ListCreativesResponse)
|
|
375
|
+
|
|
376
|
+
async def get_media_buy_delivery(
|
|
377
|
+
self,
|
|
378
|
+
request: GetMediaBuyDeliveryRequest,
|
|
379
|
+
) -> TaskResult[GetMediaBuyDeliveryResponse]:
|
|
380
|
+
"""
|
|
381
|
+
Get Media Buy Delivery.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
request: Request parameters
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
TaskResult containing GetMediaBuyDeliveryResponse
|
|
388
|
+
"""
|
|
389
|
+
operation_id = create_operation_id()
|
|
390
|
+
params = request.model_dump(exclude_none=True)
|
|
391
|
+
|
|
392
|
+
self._emit_activity(
|
|
393
|
+
Activity(
|
|
394
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
395
|
+
operation_id=operation_id,
|
|
396
|
+
agent_id=self.agent_config.id,
|
|
397
|
+
task_type="get_media_buy_delivery",
|
|
398
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
raw_result = await self.adapter.get_media_buy_delivery(params)
|
|
403
|
+
|
|
404
|
+
self._emit_activity(
|
|
405
|
+
Activity(
|
|
406
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
407
|
+
operation_id=operation_id,
|
|
408
|
+
agent_id=self.agent_config.id,
|
|
409
|
+
task_type="get_media_buy_delivery",
|
|
410
|
+
status=raw_result.status,
|
|
411
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
return self.adapter._parse_response(raw_result, GetMediaBuyDeliveryResponse)
|
|
416
|
+
|
|
417
|
+
async def list_authorized_properties(
|
|
418
|
+
self,
|
|
419
|
+
request: ListAuthorizedPropertiesRequest,
|
|
420
|
+
) -> TaskResult[ListAuthorizedPropertiesResponse]:
|
|
421
|
+
"""
|
|
422
|
+
List Authorized Properties.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
request: Request parameters
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
TaskResult containing ListAuthorizedPropertiesResponse
|
|
429
|
+
"""
|
|
430
|
+
operation_id = create_operation_id()
|
|
431
|
+
params = request.model_dump(exclude_none=True)
|
|
432
|
+
|
|
433
|
+
self._emit_activity(
|
|
434
|
+
Activity(
|
|
435
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
436
|
+
operation_id=operation_id,
|
|
437
|
+
agent_id=self.agent_config.id,
|
|
438
|
+
task_type="list_authorized_properties",
|
|
439
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
440
|
+
)
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
raw_result = await self.adapter.list_authorized_properties(params)
|
|
444
|
+
|
|
445
|
+
self._emit_activity(
|
|
446
|
+
Activity(
|
|
447
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
448
|
+
operation_id=operation_id,
|
|
449
|
+
agent_id=self.agent_config.id,
|
|
450
|
+
task_type="list_authorized_properties",
|
|
451
|
+
status=raw_result.status,
|
|
452
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
return self.adapter._parse_response(raw_result, ListAuthorizedPropertiesResponse)
|
|
457
|
+
|
|
458
|
+
async def get_signals(
|
|
459
|
+
self,
|
|
460
|
+
request: GetSignalsRequest,
|
|
461
|
+
) -> TaskResult[GetSignalsResponse]:
|
|
462
|
+
"""
|
|
463
|
+
Get Signals.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
request: Request parameters
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
TaskResult containing GetSignalsResponse
|
|
470
|
+
"""
|
|
471
|
+
operation_id = create_operation_id()
|
|
472
|
+
params = request.model_dump(exclude_none=True)
|
|
473
|
+
|
|
474
|
+
self._emit_activity(
|
|
475
|
+
Activity(
|
|
476
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
477
|
+
operation_id=operation_id,
|
|
478
|
+
agent_id=self.agent_config.id,
|
|
479
|
+
task_type="get_signals",
|
|
480
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
raw_result = await self.adapter.get_signals(params)
|
|
485
|
+
|
|
486
|
+
self._emit_activity(
|
|
487
|
+
Activity(
|
|
488
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
489
|
+
operation_id=operation_id,
|
|
490
|
+
agent_id=self.agent_config.id,
|
|
491
|
+
task_type="get_signals",
|
|
492
|
+
status=raw_result.status,
|
|
493
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
494
|
+
)
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
return self.adapter._parse_response(raw_result, GetSignalsResponse)
|
|
498
|
+
|
|
499
|
+
async def activate_signal(
|
|
500
|
+
self,
|
|
501
|
+
request: ActivateSignalRequest,
|
|
502
|
+
) -> TaskResult[ActivateSignalResponse]:
|
|
503
|
+
"""
|
|
504
|
+
Activate Signal.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
request: Request parameters
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
TaskResult containing ActivateSignalResponse
|
|
511
|
+
"""
|
|
512
|
+
operation_id = create_operation_id()
|
|
513
|
+
params = request.model_dump(exclude_none=True)
|
|
514
|
+
|
|
515
|
+
self._emit_activity(
|
|
516
|
+
Activity(
|
|
517
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
518
|
+
operation_id=operation_id,
|
|
519
|
+
agent_id=self.agent_config.id,
|
|
520
|
+
task_type="activate_signal",
|
|
521
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
raw_result = await self.adapter.activate_signal(params)
|
|
526
|
+
|
|
527
|
+
self._emit_activity(
|
|
528
|
+
Activity(
|
|
529
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
530
|
+
operation_id=operation_id,
|
|
531
|
+
agent_id=self.agent_config.id,
|
|
532
|
+
task_type="activate_signal",
|
|
533
|
+
status=raw_result.status,
|
|
534
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
535
|
+
)
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
return self.adapter._parse_response(raw_result, ActivateSignalResponse)
|
|
539
|
+
|
|
540
|
+
async def provide_performance_feedback(
|
|
541
|
+
self,
|
|
542
|
+
request: ProvidePerformanceFeedbackRequest,
|
|
543
|
+
) -> TaskResult[ProvidePerformanceFeedbackResponse]:
|
|
544
|
+
"""
|
|
545
|
+
Provide Performance Feedback.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
request: Request parameters
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
TaskResult containing ProvidePerformanceFeedbackResponse
|
|
552
|
+
"""
|
|
553
|
+
operation_id = create_operation_id()
|
|
554
|
+
params = request.model_dump(exclude_none=True)
|
|
555
|
+
|
|
556
|
+
self._emit_activity(
|
|
557
|
+
Activity(
|
|
558
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
559
|
+
operation_id=operation_id,
|
|
560
|
+
agent_id=self.agent_config.id,
|
|
561
|
+
task_type="provide_performance_feedback",
|
|
562
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
raw_result = await self.adapter.provide_performance_feedback(params)
|
|
567
|
+
|
|
568
|
+
self._emit_activity(
|
|
569
|
+
Activity(
|
|
570
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
571
|
+
operation_id=operation_id,
|
|
572
|
+
agent_id=self.agent_config.id,
|
|
573
|
+
task_type="provide_performance_feedback",
|
|
574
|
+
status=raw_result.status,
|
|
575
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
576
|
+
)
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
return self.adapter._parse_response(raw_result, ProvidePerformanceFeedbackResponse)
|
|
580
|
+
|
|
581
|
+
async def create_media_buy(
|
|
582
|
+
self,
|
|
583
|
+
request: CreateMediaBuyRequest,
|
|
584
|
+
) -> TaskResult[CreateMediaBuyResponse]:
|
|
585
|
+
"""
|
|
586
|
+
Create a new media buy reservation.
|
|
587
|
+
|
|
588
|
+
Requests the agent to reserve inventory for a campaign. The agent returns a
|
|
589
|
+
media_buy_id that tracks this reservation and can be used for updates.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
request: Media buy creation parameters including:
|
|
593
|
+
- brand_manifest: Advertiser brand information and creative assets
|
|
594
|
+
- packages: List of package requests specifying desired inventory
|
|
595
|
+
- publisher_properties: Target properties for ad placement
|
|
596
|
+
- budget: Optional budget constraints
|
|
597
|
+
- start_date/end_date: Campaign flight dates
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
TaskResult containing CreateMediaBuyResponse with:
|
|
601
|
+
- media_buy_id: Unique identifier for this reservation
|
|
602
|
+
- status: Current state of the media buy
|
|
603
|
+
- packages: Confirmed package details
|
|
604
|
+
- Additional platform-specific metadata
|
|
605
|
+
|
|
606
|
+
Example:
|
|
607
|
+
>>> from adcp import ADCPClient, CreateMediaBuyRequest
|
|
608
|
+
>>> client = ADCPClient(agent_config)
|
|
609
|
+
>>> request = CreateMediaBuyRequest(
|
|
610
|
+
... brand_manifest=brand,
|
|
611
|
+
... packages=[package_request],
|
|
612
|
+
... publisher_properties=properties
|
|
613
|
+
... )
|
|
614
|
+
>>> result = await client.create_media_buy(request)
|
|
615
|
+
>>> if result.success:
|
|
616
|
+
... media_buy_id = result.data.media_buy_id
|
|
617
|
+
"""
|
|
618
|
+
operation_id = create_operation_id()
|
|
619
|
+
params = request.model_dump(exclude_none=True)
|
|
620
|
+
|
|
621
|
+
self._emit_activity(
|
|
622
|
+
Activity(
|
|
623
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
624
|
+
operation_id=operation_id,
|
|
625
|
+
agent_id=self.agent_config.id,
|
|
626
|
+
task_type="create_media_buy",
|
|
627
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
628
|
+
)
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
raw_result = await self.adapter.create_media_buy(params)
|
|
632
|
+
|
|
633
|
+
self._emit_activity(
|
|
634
|
+
Activity(
|
|
635
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
636
|
+
operation_id=operation_id,
|
|
637
|
+
agent_id=self.agent_config.id,
|
|
638
|
+
task_type="create_media_buy",
|
|
639
|
+
status=raw_result.status,
|
|
640
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
641
|
+
)
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
return self.adapter._parse_response(raw_result, CreateMediaBuyResponse)
|
|
645
|
+
|
|
646
|
+
async def update_media_buy(
|
|
647
|
+
self,
|
|
648
|
+
request: UpdateMediaBuyRequest,
|
|
649
|
+
) -> TaskResult[UpdateMediaBuyResponse]:
|
|
650
|
+
"""
|
|
651
|
+
Update an existing media buy reservation.
|
|
652
|
+
|
|
653
|
+
Modifies a previously created media buy by updating packages or publisher
|
|
654
|
+
properties. The update operation uses discriminated unions to specify what
|
|
655
|
+
to change - either package details or targeting properties.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
request: Media buy update parameters including:
|
|
659
|
+
- media_buy_id: Identifier from create_media_buy response
|
|
660
|
+
- updates: Discriminated union specifying update type:
|
|
661
|
+
* UpdateMediaBuyPackagesRequest: Modify package selections
|
|
662
|
+
* UpdateMediaBuyPropertiesRequest: Change targeting properties
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
TaskResult containing UpdateMediaBuyResponse with:
|
|
666
|
+
- media_buy_id: The updated media buy identifier
|
|
667
|
+
- status: Updated state of the media buy
|
|
668
|
+
- packages: Updated package configurations
|
|
669
|
+
- Additional platform-specific metadata
|
|
670
|
+
|
|
671
|
+
Example:
|
|
672
|
+
>>> from adcp import ADCPClient, UpdateMediaBuyPackagesRequest
|
|
673
|
+
>>> client = ADCPClient(agent_config)
|
|
674
|
+
>>> request = UpdateMediaBuyPackagesRequest(
|
|
675
|
+
... media_buy_id="mb_123",
|
|
676
|
+
... packages=[updated_package]
|
|
677
|
+
... )
|
|
678
|
+
>>> result = await client.update_media_buy(request)
|
|
679
|
+
>>> if result.success:
|
|
680
|
+
... updated_packages = result.data.packages
|
|
681
|
+
"""
|
|
682
|
+
operation_id = create_operation_id()
|
|
683
|
+
params = request.model_dump(exclude_none=True)
|
|
684
|
+
|
|
685
|
+
self._emit_activity(
|
|
686
|
+
Activity(
|
|
687
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
688
|
+
operation_id=operation_id,
|
|
689
|
+
agent_id=self.agent_config.id,
|
|
690
|
+
task_type="update_media_buy",
|
|
691
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
692
|
+
)
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
raw_result = await self.adapter.update_media_buy(params)
|
|
696
|
+
|
|
697
|
+
self._emit_activity(
|
|
698
|
+
Activity(
|
|
699
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
700
|
+
operation_id=operation_id,
|
|
701
|
+
agent_id=self.agent_config.id,
|
|
702
|
+
task_type="update_media_buy",
|
|
703
|
+
status=raw_result.status,
|
|
704
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
705
|
+
)
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
return self.adapter._parse_response(raw_result, UpdateMediaBuyResponse)
|
|
709
|
+
|
|
710
|
+
async def build_creative(
|
|
711
|
+
self,
|
|
712
|
+
request: BuildCreativeRequest,
|
|
713
|
+
) -> TaskResult[BuildCreativeResponse]:
|
|
714
|
+
"""
|
|
715
|
+
Generate production-ready creative assets.
|
|
716
|
+
|
|
717
|
+
Requests the creative agent to build final deliverable assets in the target
|
|
718
|
+
format (e.g., VAST, DAAST, HTML5). This is typically called after previewing
|
|
719
|
+
and approving a creative manifest.
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
request: Creative build parameters including:
|
|
723
|
+
- manifest: Creative manifest with brand info and content
|
|
724
|
+
- target_format_id: Desired output format identifier
|
|
725
|
+
- inputs: Optional user-provided inputs for template variables
|
|
726
|
+
- deployment: Platform or agent deployment configuration
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
TaskResult containing BuildCreativeResponse with:
|
|
730
|
+
- assets: Production-ready creative files (URLs or inline content)
|
|
731
|
+
- format_id: The generated format identifier
|
|
732
|
+
- manifest: The creative manifest used for generation
|
|
733
|
+
- metadata: Additional platform-specific details
|
|
734
|
+
|
|
735
|
+
Example:
|
|
736
|
+
>>> from adcp import ADCPClient, BuildCreativeRequest
|
|
737
|
+
>>> client = ADCPClient(agent_config)
|
|
738
|
+
>>> request = BuildCreativeRequest(
|
|
739
|
+
... manifest=creative_manifest,
|
|
740
|
+
... target_format_id="vast_2.0",
|
|
741
|
+
... inputs={"duration": 30}
|
|
742
|
+
... )
|
|
743
|
+
>>> result = await client.build_creative(request)
|
|
744
|
+
>>> if result.success:
|
|
745
|
+
... vast_url = result.data.assets[0].url
|
|
746
|
+
"""
|
|
747
|
+
operation_id = create_operation_id()
|
|
748
|
+
params = request.model_dump(exclude_none=True)
|
|
749
|
+
|
|
750
|
+
self._emit_activity(
|
|
751
|
+
Activity(
|
|
752
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
753
|
+
operation_id=operation_id,
|
|
754
|
+
agent_id=self.agent_config.id,
|
|
755
|
+
task_type="build_creative",
|
|
756
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
raw_result = await self.adapter.build_creative(params)
|
|
761
|
+
|
|
762
|
+
self._emit_activity(
|
|
763
|
+
Activity(
|
|
764
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
765
|
+
operation_id=operation_id,
|
|
766
|
+
agent_id=self.agent_config.id,
|
|
767
|
+
task_type="build_creative",
|
|
768
|
+
status=raw_result.status,
|
|
769
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
770
|
+
)
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
return self.adapter._parse_response(raw_result, BuildCreativeResponse)
|
|
774
|
+
|
|
775
|
+
async def list_tools(self) -> list[str]:
|
|
776
|
+
"""
|
|
777
|
+
List available tools from the agent.
|
|
778
|
+
|
|
779
|
+
Returns:
|
|
780
|
+
List of tool names
|
|
781
|
+
"""
|
|
782
|
+
return await self.adapter.list_tools()
|
|
783
|
+
|
|
784
|
+
async def get_info(self) -> dict[str, Any]:
|
|
785
|
+
"""
|
|
786
|
+
Get agent information including AdCP extension metadata.
|
|
787
|
+
|
|
788
|
+
Returns agent card information including:
|
|
789
|
+
- Agent name, description, version
|
|
790
|
+
- Protocol type (mcp or a2a)
|
|
791
|
+
- AdCP version (from extensions.adcp.adcp_version)
|
|
792
|
+
- Supported protocols (from extensions.adcp.protocols_supported)
|
|
793
|
+
- Available tools/skills
|
|
794
|
+
|
|
795
|
+
Returns:
|
|
796
|
+
Dictionary with agent metadata
|
|
797
|
+
"""
|
|
798
|
+
return await self.adapter.get_agent_info()
|
|
799
|
+
|
|
800
|
+
async def close(self) -> None:
|
|
801
|
+
"""Close the adapter and clean up resources."""
|
|
802
|
+
if hasattr(self.adapter, "close"):
|
|
803
|
+
logger.debug(f"Closing adapter for agent {self.agent_config.id}")
|
|
804
|
+
await self.adapter.close()
|
|
805
|
+
|
|
806
|
+
async def __aenter__(self) -> ADCPClient:
|
|
807
|
+
"""Async context manager entry."""
|
|
808
|
+
return self
|
|
809
|
+
|
|
810
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
811
|
+
"""Async context manager exit."""
|
|
812
|
+
await self.close()
|
|
813
|
+
|
|
814
|
+
def _verify_webhook_signature(self, payload: dict[str, Any], signature: str) -> bool:
|
|
815
|
+
"""
|
|
816
|
+
Verify HMAC-SHA256 signature of webhook payload.
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
payload: Webhook payload dict
|
|
820
|
+
signature: Signature to verify
|
|
821
|
+
|
|
822
|
+
Returns:
|
|
823
|
+
True if signature is valid, False otherwise
|
|
824
|
+
"""
|
|
825
|
+
if not self.webhook_secret:
|
|
826
|
+
return True
|
|
827
|
+
|
|
828
|
+
payload_bytes = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
|
829
|
+
expected_signature = hmac.new(
|
|
830
|
+
self.webhook_secret.encode("utf-8"), payload_bytes, hashlib.sha256
|
|
831
|
+
).hexdigest()
|
|
832
|
+
|
|
833
|
+
return hmac.compare_digest(signature, expected_signature)
|
|
834
|
+
|
|
835
|
+
def _parse_webhook_result(self, webhook: WebhookPayload) -> TaskResult[Any]:
|
|
836
|
+
"""
|
|
837
|
+
Parse webhook payload into typed TaskResult based on task_type.
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
webhook: Validated webhook payload
|
|
841
|
+
|
|
842
|
+
Returns:
|
|
843
|
+
TaskResult with task-specific typed response data
|
|
844
|
+
"""
|
|
845
|
+
from adcp.utils.response_parser import parse_json_or_text
|
|
846
|
+
|
|
847
|
+
# Map task types to their response types (using string literals, not enum)
|
|
848
|
+
# Note: Some response types are Union types (e.g., ActivateSignalResponse = Success | Error)
|
|
849
|
+
response_type_map: dict[str, type[BaseModel] | Any] = {
|
|
850
|
+
"get_products": GetProductsResponse,
|
|
851
|
+
"list_creative_formats": ListCreativeFormatsResponse,
|
|
852
|
+
"sync_creatives": SyncCreativesResponse, # Union type
|
|
853
|
+
"list_creatives": ListCreativesResponse,
|
|
854
|
+
"get_media_buy_delivery": GetMediaBuyDeliveryResponse,
|
|
855
|
+
"list_authorized_properties": ListAuthorizedPropertiesResponse,
|
|
856
|
+
"get_signals": GetSignalsResponse,
|
|
857
|
+
"activate_signal": ActivateSignalResponse, # Union type
|
|
858
|
+
"provide_performance_feedback": ProvidePerformanceFeedbackResponse,
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
# Handle completed tasks with result parsing
|
|
862
|
+
|
|
863
|
+
if webhook.status == GeneratedTaskStatus.completed and webhook.result is not None:
|
|
864
|
+
response_type = response_type_map.get(webhook.task_type.value)
|
|
865
|
+
if response_type:
|
|
866
|
+
try:
|
|
867
|
+
parsed_result: Any = parse_json_or_text(webhook.result, response_type)
|
|
868
|
+
return TaskResult[Any](
|
|
869
|
+
status=TaskStatus.COMPLETED,
|
|
870
|
+
data=parsed_result,
|
|
871
|
+
success=True,
|
|
872
|
+
metadata={
|
|
873
|
+
"task_id": webhook.task_id,
|
|
874
|
+
"operation_id": webhook.operation_id,
|
|
875
|
+
"timestamp": webhook.timestamp,
|
|
876
|
+
"message": webhook.message,
|
|
877
|
+
},
|
|
878
|
+
)
|
|
879
|
+
except ValueError as e:
|
|
880
|
+
logger.warning(f"Failed to parse webhook result: {e}")
|
|
881
|
+
# Fall through to untyped result
|
|
882
|
+
|
|
883
|
+
# Handle failed, input-required, or unparseable results
|
|
884
|
+
# Convert webhook status to core TaskStatus enum
|
|
885
|
+
# Map generated enum values to core enum values
|
|
886
|
+
status_map = {
|
|
887
|
+
GeneratedTaskStatus.completed: TaskStatus.COMPLETED,
|
|
888
|
+
GeneratedTaskStatus.submitted: TaskStatus.SUBMITTED,
|
|
889
|
+
GeneratedTaskStatus.working: TaskStatus.WORKING,
|
|
890
|
+
GeneratedTaskStatus.failed: TaskStatus.FAILED,
|
|
891
|
+
GeneratedTaskStatus.input_required: TaskStatus.NEEDS_INPUT,
|
|
892
|
+
}
|
|
893
|
+
task_status = status_map.get(webhook.status, TaskStatus.FAILED)
|
|
894
|
+
|
|
895
|
+
return TaskResult[Any](
|
|
896
|
+
status=task_status,
|
|
897
|
+
data=webhook.result,
|
|
898
|
+
success=webhook.status == GeneratedTaskStatus.completed,
|
|
899
|
+
error=webhook.error if isinstance(webhook.error, str) else None,
|
|
900
|
+
metadata={
|
|
901
|
+
"task_id": webhook.task_id,
|
|
902
|
+
"operation_id": webhook.operation_id,
|
|
903
|
+
"timestamp": webhook.timestamp,
|
|
904
|
+
"message": webhook.message,
|
|
905
|
+
"context_id": webhook.context_id,
|
|
906
|
+
"progress": webhook.progress,
|
|
907
|
+
},
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
async def handle_webhook(
|
|
911
|
+
self,
|
|
912
|
+
payload: dict[str, Any],
|
|
913
|
+
signature: str | None = None,
|
|
914
|
+
) -> TaskResult[Any]:
|
|
915
|
+
"""
|
|
916
|
+
Handle incoming webhook and return typed result.
|
|
917
|
+
|
|
918
|
+
This method:
|
|
919
|
+
1. Verifies webhook signature (if provided)
|
|
920
|
+
2. Validates payload against WebhookPayload schema
|
|
921
|
+
3. Parses task-specific result data into typed response
|
|
922
|
+
4. Emits activity for monitoring
|
|
923
|
+
|
|
924
|
+
Args:
|
|
925
|
+
payload: Webhook payload dict
|
|
926
|
+
signature: Optional HMAC-SHA256 signature for verification
|
|
927
|
+
|
|
928
|
+
Returns:
|
|
929
|
+
TaskResult with parsed task-specific response data
|
|
930
|
+
|
|
931
|
+
Raises:
|
|
932
|
+
ADCPWebhookSignatureError: If signature verification fails
|
|
933
|
+
ValidationError: If payload doesn't match WebhookPayload schema
|
|
934
|
+
|
|
935
|
+
Example:
|
|
936
|
+
>>> result = await client.handle_webhook(payload, signature)
|
|
937
|
+
>>> if result.success and isinstance(result.data, GetProductsResponse):
|
|
938
|
+
>>> print(f"Found {len(result.data.products)} products")
|
|
939
|
+
"""
|
|
940
|
+
# Verify signature before processing
|
|
941
|
+
if signature and not self._verify_webhook_signature(payload, signature):
|
|
942
|
+
logger.warning(
|
|
943
|
+
f"Webhook signature verification failed for agent {self.agent_config.id}"
|
|
944
|
+
)
|
|
945
|
+
raise ADCPWebhookSignatureError("Invalid webhook signature")
|
|
946
|
+
|
|
947
|
+
# Validate and parse webhook payload
|
|
948
|
+
webhook = WebhookPayload.model_validate(payload)
|
|
949
|
+
|
|
950
|
+
# Emit activity for monitoring
|
|
951
|
+
self._emit_activity(
|
|
952
|
+
Activity(
|
|
953
|
+
type=ActivityType.WEBHOOK_RECEIVED,
|
|
954
|
+
operation_id=webhook.operation_id or "unknown",
|
|
955
|
+
agent_id=self.agent_config.id,
|
|
956
|
+
task_type=webhook.task_type.value,
|
|
957
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
958
|
+
metadata={"payload": payload},
|
|
959
|
+
)
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
# Parse and return typed result
|
|
963
|
+
return self._parse_webhook_result(webhook)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
class ADCPMultiAgentClient:
|
|
967
|
+
"""Client for managing multiple AdCP agents."""
|
|
968
|
+
|
|
969
|
+
def __init__(
|
|
970
|
+
self,
|
|
971
|
+
agents: list[AgentConfig],
|
|
972
|
+
webhook_url_template: str | None = None,
|
|
973
|
+
webhook_secret: str | None = None,
|
|
974
|
+
on_activity: Callable[[Activity], None] | None = None,
|
|
975
|
+
handlers: dict[str, Callable[..., Any]] | None = None,
|
|
976
|
+
):
|
|
977
|
+
"""
|
|
978
|
+
Initialize multi-agent client.
|
|
979
|
+
|
|
980
|
+
Args:
|
|
981
|
+
agents: List of agent configurations
|
|
982
|
+
webhook_url_template: Template for webhook URLs
|
|
983
|
+
webhook_secret: Secret for webhook verification
|
|
984
|
+
on_activity: Callback for activity events
|
|
985
|
+
handlers: Task completion handlers
|
|
986
|
+
"""
|
|
987
|
+
self.agents = {
|
|
988
|
+
agent.id: ADCPClient(
|
|
989
|
+
agent,
|
|
990
|
+
webhook_url_template=webhook_url_template,
|
|
991
|
+
webhook_secret=webhook_secret,
|
|
992
|
+
on_activity=on_activity,
|
|
993
|
+
)
|
|
994
|
+
for agent in agents
|
|
995
|
+
}
|
|
996
|
+
self.handlers = handlers or {}
|
|
997
|
+
|
|
998
|
+
def agent(self, agent_id: str) -> ADCPClient:
|
|
999
|
+
"""Get client for specific agent."""
|
|
1000
|
+
if agent_id not in self.agents:
|
|
1001
|
+
raise ValueError(f"Agent not found: {agent_id}")
|
|
1002
|
+
return self.agents[agent_id]
|
|
1003
|
+
|
|
1004
|
+
@property
|
|
1005
|
+
def agent_ids(self) -> list[str]:
|
|
1006
|
+
"""Get list of agent IDs."""
|
|
1007
|
+
return list(self.agents.keys())
|
|
1008
|
+
|
|
1009
|
+
async def close(self) -> None:
|
|
1010
|
+
"""Close all agent clients and clean up resources."""
|
|
1011
|
+
import asyncio
|
|
1012
|
+
|
|
1013
|
+
logger.debug("Closing all agent clients in multi-agent client")
|
|
1014
|
+
close_tasks = [client.close() for client in self.agents.values()]
|
|
1015
|
+
await asyncio.gather(*close_tasks, return_exceptions=True)
|
|
1016
|
+
|
|
1017
|
+
async def __aenter__(self) -> ADCPMultiAgentClient:
|
|
1018
|
+
"""Async context manager entry."""
|
|
1019
|
+
return self
|
|
1020
|
+
|
|
1021
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
1022
|
+
"""Async context manager exit."""
|
|
1023
|
+
await self.close()
|
|
1024
|
+
|
|
1025
|
+
async def get_products(
|
|
1026
|
+
self,
|
|
1027
|
+
request: GetProductsRequest,
|
|
1028
|
+
) -> list[TaskResult[GetProductsResponse]]:
|
|
1029
|
+
"""
|
|
1030
|
+
Execute get_products across all agents in parallel.
|
|
1031
|
+
|
|
1032
|
+
Args:
|
|
1033
|
+
request: Request parameters
|
|
1034
|
+
|
|
1035
|
+
Returns:
|
|
1036
|
+
List of TaskResults containing GetProductsResponse for each agent
|
|
1037
|
+
"""
|
|
1038
|
+
import asyncio
|
|
1039
|
+
|
|
1040
|
+
tasks = [agent.get_products(request) for agent in self.agents.values()]
|
|
1041
|
+
return await asyncio.gather(*tasks)
|
|
1042
|
+
|
|
1043
|
+
@classmethod
|
|
1044
|
+
def from_env(cls) -> ADCPMultiAgentClient:
|
|
1045
|
+
"""Create client from environment variables."""
|
|
1046
|
+
agents_json = os.getenv("ADCP_AGENTS")
|
|
1047
|
+
if not agents_json:
|
|
1048
|
+
raise ValueError("ADCP_AGENTS environment variable not set")
|
|
1049
|
+
|
|
1050
|
+
agents_data = json.loads(agents_json)
|
|
1051
|
+
agents = [AgentConfig(**agent) for agent in agents_data]
|
|
1052
|
+
|
|
1053
|
+
return cls(
|
|
1054
|
+
agents=agents,
|
|
1055
|
+
webhook_url_template=os.getenv("WEBHOOK_URL_TEMPLATE"),
|
|
1056
|
+
webhook_secret=os.getenv("WEBHOOK_SECRET"),
|
|
1057
|
+
)
|