Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
# Pinned @adcp/sdk version. Bump deliberately; cache invalidates when this moves.
# Background: adcontextprotocol/adcp-client-python#779 (Track B), adcontextprotocol/adcp#4907.
env:
ADCP_SDK_VERSION: "7.10.2"
ADCP_SDK_VERSION: "8.1.0-beta.7"

concurrency:
group: ci-${{ github.ref }}
Expand Down Expand Up @@ -775,6 +775,11 @@ jobs:
storyboard-sales-proposal-mode:
name: AdCP storyboard runner — sales-proposal-mode (proposal_finalize)
runs-on: ubuntu-latest
env:
# beta.7 gates this storyboard on media_buy.supports_proposals=true,
# while this legacy proposal-mode example is intentionally validated
# by the older proposal_finalize storyboard contract.
ADCP_SDK_VERSION: "7.10.2"
# v1.5 ProposalManager finalize lifecycle proof. The mock seller
# declares ``finalize=True`` + wires an ``InMemoryProposalStore``;
# the framework's dispatch wiring intercepts ``refine[i].action='finalize'``
Expand Down
88 changes: 81 additions & 7 deletions examples/seller_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import os
import uuid
from datetime import datetime, timezone
from typing import Any

from adcp.server import (
Expand Down Expand Up @@ -79,6 +80,7 @@
accounts: dict[str, dict[str, Any]] = {}
media_buys: dict[str, dict[str, Any]] = {}
creatives: dict[str, dict[str, Any]] = {}
open_impairments: dict[tuple[str, str], dict[str, Any]] = {}
proposals: dict[str, dict[str, Any]] = {}
# Used when no account_id is present; single-tenant demo shortcut.
# Real sellers must scope directives and tasks by account_id.
Expand All @@ -89,6 +91,78 @@
# Seeded creative formats keyed by the string format ID the storyboard supplies.
# list_creative_formats merges these in so storyboard references resolve.
seeded_creative_formats: dict[str, dict[str, Any]] = {}


def _now_z() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def _package_creative_ids(pkg: dict[str, Any]) -> list[str]:
ids: list[str] = []
for assignment in pkg.get("creative_assignments") or []:
if isinstance(assignment, dict) and assignment.get("creative_id"):
ids.append(str(assignment["creative_id"]))
for creative in pkg.get("creatives") or []:
if isinstance(creative, dict) and creative.get("creative_id"):
ids.append(str(creative["creative_id"]))
return list(dict.fromkeys(ids))


def _health_fields_for_media_buy(media_buy_id: str | None, mb: dict[str, Any]) -> dict[str, Any]:
impaired_packages: dict[str, list[str]] = {}
for pkg in mb.get("packages", []):
package_id = pkg.get("package_id")
if not package_id:
continue
creative_ids = _package_creative_ids(pkg)
if not creative_ids:
continue
if any(
creatives.get(creative_id, {}).get("status") in {"approved", "active"}
for creative_id in creative_ids
):
continue
for creative_id in creative_ids:
creative_status = creatives.get(creative_id, {}).get("status")
if creative_status in {"rejected"}:
package_ids = impaired_packages.setdefault(creative_id, [])
if package_id not in package_ids:
package_ids.append(package_id)
media_buy_key = media_buy_id or "__anonymous__"
active_keys = {(media_buy_key, creative_id) for creative_id in impaired_packages}
for key in [
key for key in open_impairments if key[0] == media_buy_key and key not in active_keys
]:
del open_impairments[key]

impairments: list[dict[str, Any]] = []
for creative_id, package_ids in impaired_packages.items():
creative = creatives.get(creative_id, {})
key = (media_buy_key, creative_id)
if key not in open_impairments:
open_impairments[key] = {
"impairment_id": f"imp-{uuid.uuid4().hex[:8]}",
"observed_at": creative.get("status_changed_at") or _now_z(),
}
impairment = open_impairments[key]
impairments.append(
{
"impairment_id": impairment["impairment_id"],
"resource_type": "creative",
"resource_id": creative_id,
"package_ids": package_ids,
"transition": {"from": "approved", "to": "rejected"},
"reason_code": "content_rejected",
"reason": "Creative is no longer approved for delivery.",
"observed_at": impairment["observed_at"],
"remediation": "Assign an approved replacement creative.",
}
)
if impairments:
return {"health": "impaired", "impairments": impairments}
return {"health": "ok", "impairments": []}


# Single-shot directives registered by force_create_media_buy_arm; keyed by account_id.
pending_directives: dict[str, dict[str, Any]] = {}
# Tasks registered when create_media_buy consumes a 'submitted' directive; keyed by task_id.
Expand Down Expand Up @@ -310,9 +384,7 @@ async def sync_governance(self, params: dict[str, Any], context: Any = None) ->
{
"account": acct_ref,
"status": "synced",
"governance_agents": [
{"url": a.get("url"), "categories": a.get("categories", [])} for a in agents
],
"governance_agents": [{"url": a.get("url")} for a in agents],
}
)
return sync_governance_response(results)
Expand Down Expand Up @@ -345,7 +417,7 @@ async def get_products(self, params: dict[str, Any], context: Any = None) -> dic
}
]
return {
**products_response(PRODUCTS),
**products_response(PRODUCTS, cache_scope="public"),
"proposals": [
{
"proposal_id": proposal_id,
Expand All @@ -355,7 +427,7 @@ async def get_products(self, params: dict[str, Any], context: Any = None) -> dic
}
],
}
return products_response(PRODUCTS)
return products_response(PRODUCTS, cache_scope="public")

async def create_media_buy(self, params: dict[str, Any], context: Any = None) -> dict[str, Any]:
account_id = (params.get("account") or {}).get("account_id") or _DEFAULT_ACCOUNT_ID
Expand Down Expand Up @@ -478,6 +550,7 @@ async def get_media_buys(self, params: dict[str, Any], context: Any = None) -> d
"currency": mb.get("currency", "USD"),
"packages": mb.get("packages", []),
"total_budget": total_budget,
**_health_fields_for_media_buy(mb_id, mb),
}
)
return media_buys_response(results)
Expand Down Expand Up @@ -605,12 +678,11 @@ async def sync_creatives(self, params: dict[str, Any], context: Any = None) -> d
results = []
for c in params.get("creatives", []):
creative_id = c.get("creative_id") or f"c-{uuid.uuid4().hex[:8]}"
creatives[creative_id] = {**c, "status": "approved"}
creatives[creative_id] = {**c, "status": "approved", "status_changed_at": _now_z()}
results.append(
{
"creative_id": creative_id,
"action": "created",
"status": "approved",
}
)
# Transition any media buys waiting on creatives to pending_start
Expand Down Expand Up @@ -696,6 +768,7 @@ async def force_creative_status(
current_state=prev,
)
c["status"] = status
c["status_changed_at"] = _now_z()
return {"previous_state": prev, "current_state": status}

async def simulate_delivery(
Expand Down Expand Up @@ -900,6 +973,7 @@ async def seed_creative(
data = dict(fixture or {})
cid = creative_id or data.get("creative_id") or f"c-seeded-{uuid.uuid4().hex[:8]}"
data["creative_id"] = cid
data.setdefault("status_changed_at", _now_z())
creatives[cid] = data
return {"creative_id": cid}

Expand Down
12 changes: 9 additions & 3 deletions examples/v3_reference_seller/tests/test_smoke_broadening.py
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,9 @@ async def test_create_media_buy_echoes_packages_with_seller_minted_ids(
assert pkg.targeting_overlay.collection_list.list_id == "coll_evening_news"
# Buyer supplied a creative_assignment — status reflects upstream-derived
# status ("approved" → "pending_start"), not pending_creatives.
assert result.status.value == "pending_start"
assert result.status == "completed"
assert result.media_buy_status is not None
assert result.media_buy_status.value == "pending_start"


@pytest.mark.asyncio
Expand Down Expand Up @@ -786,7 +788,9 @@ async def test_create_media_buy_no_creatives_returns_pending_creatives_status(
)
result = await platform.create_media_buy(req, ctx)
assert isinstance(result, CreateMediaBuySuccessResponse)
assert result.status.value == "pending_creatives"
assert result.status == "completed"
assert result.media_buy_status is not None
assert result.media_buy_status.value == "pending_creatives"
assert result.packages is not None
assert result.packages[0].package_id is not None
assert result.packages[0].package_id.startswith("li_test_")
Expand Down Expand Up @@ -824,7 +828,9 @@ async def test_update_media_buy_cancel_marks_local_state(respx_mock: Any) -> Non
)
result = await platform.update_media_buy("ord_test", patch, ctx)
assert isinstance(result, UpdateMediaBuySuccessResponse)
assert result.status.value == "canceled"
assert result.status == "completed"
assert result.media_buy_status is not None
assert result.media_buy_status.value == "canceled"
assert result.revision == 1

# Re-cancel — irreversible.
Expand Down
1 change: 1 addition & 0 deletions scripts/consolidate_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ def _stem_matches_export(module_stem: str, export_name: str) -> bool:
"UpdateContentStandardsResponse2": "UpdateContentStandardsResponse",
"UpdateMediaBuyResponse1": "UpdateMediaBuyResponse",
"UpdateMediaBuyResponse2": "UpdateMediaBuyResponse",
"UpdateMediaBuyResponse3": "UpdateMediaBuyResponse",
"ValidateContentDeliveryResponse1": "ValidateContentDeliveryResponse",
"ValidateContentDeliveryResponse2": "ValidateContentDeliveryResponse",
}
Expand Down
Loading
Loading