Skip to content

Commit 04966d7

Browse files
bokelleyclaude
andauthored
fix(examples): seller_agent.py AdCP 3.0.1 storyboard compliance (items 1-6 of #304) (#310)
* fix(examples): seller_agent.py passes AdCP 3.0.1 storyboard compliance (items 1–6 of #304) Six gaps identified by the media_buy_seller storyboard runner after the #296 transport fix exposed content-side failures in the reference example: 1. declare `adcp.idempotency` in capabilities so the runner does not downgrade to v2 mode (`idempotency={"supported": False}`) 2. include `total_budget` (schema-required number) in `get_media_buys` entries, computed as the sum of per-package budgets 3. return `status=pending_creatives` from `create_media_buy` when no `creative_assignments`/`creatives` are in the request packages, and transition to `active` in `update_media_buy` when creatives are attached 4. fix `list_creative_formats` render shape: wrap width/height in a `dimensions` object and add the required `role` field 5. honour the `format_ids` filter in `list_creative_formats`, matching on the full `(agent_url, id)` pair 6. return `PACKAGE_NOT_FOUND` in `update_media_buy` when a package ID in the update request does not exist in the stored media buy Item 7 (seed_product / controller_detected) remains blocked on #282. https://claude.ai/code/session_01HAP5upax2a7FrcrmgVwTX2 * fix(examples): align DemoStore.simulate_delivery reported_spend type with base class The base TestControllerStore declares reported_spend as dict[str, Any] | None (matching the ReportedSpend schema {amount, currency}). DemoStore had it as float | None, causing type mismatch and incorrect stored structure when the storyboard sends a structured object. https://claude.ai/code/session_01HAP5upax2a7FrcrmgVwTX2 * fix(examples): explicitly pass valid_actions for pending_creatives status MEDIA_BUY_STATE_MACHINE on main lacks the pending_creatives key (it lands with PR #296). Without explicit valid_actions, media_buy_response() and update_media_buy_response() return valid_actions=[] for pending_creatives buys, blocking the storyboard from discovering that sync_creatives is available. Pass the expected actions list explicitly until #296 merges. https://claude.ai/code/session_01HAP5upax2a7FrcrmgVwTX2 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6be0232 commit 04966d7

1 file changed

Lines changed: 93 additions & 45 deletions

File tree

examples/seller_agent.py

Lines changed: 93 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ async def get_adcp_capabilities(
107107
) -> dict[str, Any]:
108108
return capabilities_response(
109109
["media_buy"],
110+
idempotency={"supported": False},
110111
compliance_testing={
111112
"scenarios": [
112113
"force_account_status",
@@ -223,27 +224,40 @@ async def create_media_buy(self, params: dict[str, Any], context: Any = None) ->
223224
}
224225
)
225226

227+
has_creatives = any(
228+
pkg.get("creative_assignments") or pkg.get("creatives")
229+
for pkg in params["packages"]
230+
)
231+
status = "active" if has_creatives else "pending_creatives"
232+
226233
mb_id = f"mb-{uuid.uuid4().hex[:8]}"
227234
media_buys[mb_id] = {
228-
"status": "active",
235+
"status": status,
229236
"currency": "USD",
230237
"packages": packages,
231238
"revision": 1,
232239
}
233-
return media_buy_response(mb_id, packages, status="active")
240+
pending_actions = ["sync_creatives", "cancel", "update_budget", "update_dates",
241+
"update_packages", "add_packages"]
242+
return media_buy_response(
243+
mb_id, packages, status=status,
244+
valid_actions=pending_actions if status == "pending_creatives" else None,
245+
)
234246

235247
async def get_media_buys(self, params: dict[str, Any], context: Any = None) -> dict[str, Any]:
236248
requested_ids = params.get("media_buy_ids")
237249
results = []
238250
for mb_id, mb in media_buys.items():
239251
if requested_ids and mb_id not in requested_ids:
240252
continue
253+
total_budget = sum((pkg.get("budget") or 0) for pkg in mb.get("packages", []))
241254
results.append(
242255
{
243256
"media_buy_id": mb_id,
244257
"status": mb["status"],
245258
"currency": mb.get("currency", "USD"),
246259
"packages": mb.get("packages", []),
260+
"total_budget": total_budget,
247261
}
248262
)
249263
return media_buys_response(results)
@@ -257,7 +271,25 @@ async def update_media_buy(self, params: dict[str, Any], context: Any = None) ->
257271
if params.get("revision") and params["revision"] != mb.get("revision", 1):
258272
return adcp_error("CONFLICT", "Revision mismatch - refetch and retry")
259273

274+
if params.get("packages"):
275+
existing_pkg_ids = {p["package_id"] for p in mb.get("packages", [])}
276+
for pkg_update in params["packages"]:
277+
pkg_id = pkg_update.get("package_id")
278+
if pkg_id and pkg_id not in existing_pkg_ids:
279+
return adcp_error(
280+
"PACKAGE_NOT_FOUND",
281+
f"Package '{pkg_id}' not found in media buy {mb_id}",
282+
field="package_id",
283+
)
284+
260285
status = mb["status"]
286+
if status == "pending_creatives" and params.get("packages"):
287+
if any(
288+
pkg.get("creative_assignments") or pkg.get("creatives")
289+
for pkg in params["packages"]
290+
):
291+
mb["status"] = "active"
292+
status = "active"
261293
if params.get("paused") is True and status == "active":
262294
mb["status"] = "paused"
263295
elif params.get("paused") is False and status == "paused":
@@ -269,55 +301,71 @@ async def update_media_buy(self, params: dict[str, Any], context: Any = None) ->
269301
return cancel_media_buy_response(mb_id, "buyer")
270302

271303
mb["revision"] = mb.get("revision", 1) + 1
272-
return update_media_buy_response(mb_id, status=mb["status"], revision=mb["revision"])
304+
pending_actions = ["sync_creatives", "cancel", "update_budget", "update_dates",
305+
"update_packages", "add_packages"]
306+
return update_media_buy_response(
307+
mb_id,
308+
status=mb["status"],
309+
revision=mb["revision"],
310+
valid_actions=pending_actions if mb["status"] == "pending_creatives" else None,
311+
)
273312

274313
async def list_creative_formats(
275314
self, params: dict[str, Any], context: Any = None
276315
) -> dict[str, Any]:
277-
return creative_formats_response(
278-
[
279-
{
280-
"format_id": {
281-
"agent_url": AGENT_URL,
282-
"id": "display_300x250",
283-
},
284-
"name": "Display 300x250",
285-
"renders": [{"width": 300, "height": 250}],
286-
"assets": [
287-
{
288-
"item_type": "individual",
289-
"asset_id": "image",
290-
"asset_type": "image",
291-
"required": True,
292-
"accepted_media_types": [
293-
"image/png",
294-
"image/jpeg",
295-
],
296-
}
297-
],
316+
all_formats: list[dict[str, Any]] = [
317+
{
318+
"format_id": {
319+
"agent_url": AGENT_URL,
320+
"id": "display_300x250",
298321
},
299-
{
300-
"format_id": {
301-
"agent_url": AGENT_URL,
302-
"id": "display_970x250",
303-
},
304-
"name": "Display 970x250",
305-
"renders": [{"width": 970, "height": 250}],
306-
"assets": [
307-
{
308-
"item_type": "individual",
309-
"asset_id": "image",
310-
"asset_type": "image",
311-
"required": True,
312-
"accepted_media_types": [
313-
"image/png",
314-
"image/jpeg",
315-
],
316-
}
317-
],
322+
"name": "Display 300x250",
323+
"renders": [{"role": "primary", "dimensions": {"width": 300, "height": 250}}],
324+
"assets": [
325+
{
326+
"item_type": "individual",
327+
"asset_id": "image",
328+
"asset_type": "image",
329+
"required": True,
330+
"accepted_media_types": [
331+
"image/png",
332+
"image/jpeg",
333+
],
334+
}
335+
],
336+
},
337+
{
338+
"format_id": {
339+
"agent_url": AGENT_URL,
340+
"id": "display_970x250",
318341
},
342+
"name": "Display 970x250",
343+
"renders": [{"role": "primary", "dimensions": {"width": 970, "height": 250}}],
344+
"assets": [
345+
{
346+
"item_type": "individual",
347+
"asset_id": "image",
348+
"asset_type": "image",
349+
"required": True,
350+
"accepted_media_types": [
351+
"image/png",
352+
"image/jpeg",
353+
],
354+
}
355+
],
356+
},
357+
]
358+
filter_ids = params.get("format_ids")
359+
if filter_ids:
360+
wanted = {(fid.get("agent_url"), fid["id"]) for fid in filter_ids if "id" in fid}
361+
formats = [
362+
f
363+
for f in all_formats
364+
if (f["format_id"].get("agent_url"), f["format_id"]["id"]) in wanted
319365
]
320-
)
366+
else:
367+
formats = all_formats
368+
return creative_formats_response(formats)
321369

322370
async def sync_creatives(self, params: dict[str, Any], context: Any = None) -> dict[str, Any]:
323371
results = []
@@ -414,7 +462,7 @@ async def simulate_delivery(
414462
impressions: int | None = None,
415463
clicks: int | None = None,
416464
conversions: int | None = None,
417-
reported_spend: float | None = None,
465+
reported_spend: dict[str, Any] | None = None,
418466
) -> dict[str, Any]:
419467
if media_buy_id not in media_buys:
420468
raise TestControllerError("NOT_FOUND", f"Media buy {media_buy_id} not found")

0 commit comments

Comments
 (0)