Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 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
5 changes: 4 additions & 1 deletion examples/sales_proposal_mode_seller/src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@ def build_router() -> PlatformRouter:
idempotency=IdempotencyUnsupported(supported=False),
),
account=CapabilitiesAccount(supported_billing=["operator"]),
media_buy=MediaBuy(supported_pricing_models=["cpm"]),
media_buy=MediaBuy(
supported_pricing_models=["cpm"],
supports_proposals=True,
),
supported_protocols=[SupportedProtocol.media_buy],
),
)
Expand Down
3 changes: 2 additions & 1 deletion examples/sales_proposal_mode_seller/src/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class ProposalModeDecisioningPlatform(DecisioningPlatform, SalesPlatform):
account=CapabilitiesAccount(supported_billing=["operator"]),
media_buy=MediaBuy(
supported_pricing_models=["cpm"],
supports_proposals=True,
),
supported_protocols=[SupportedProtocol.media_buy],
)
Expand All @@ -83,7 +84,7 @@ def get_products(self, req: Any, ctx: RequestContext[Any]) -> dict[str, Any]:
# the manager's get_products instead — this is only for tenants
# without proposal mode wiring (none in this example).
del req, ctx
return {"products": []}
return {"products": [], "cache_scope": "public"}

def create_media_buy(
self,
Expand Down
9 changes: 9 additions & 0 deletions examples/sales_proposal_mode_seller/src/proposal_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ async def get_products(
return {
"products": _PRODUCTS,
"proposals": [_draft_proposal_payload()],
"cache_scope": "public",
}

async def refine_products(
Expand Down Expand Up @@ -240,6 +241,7 @@ async def refine_products(
"products": _PRODUCTS,
"proposals": [proposal],
"refinement_applied": applied,
"cache_scope": "public",
}

async def finalize_proposal(
Expand Down Expand Up @@ -270,6 +272,13 @@ async def finalize_proposal(
entry["firm_cpm"] = firm_cpm[pid]
expires_at = datetime.now(timezone.utc) + timedelta(hours=24)
committed_payload["expires_at"] = expires_at.isoformat().replace("+00:00", "Z")
committed_payload["insertion_order"] = {
"io_id": f"io_{req.proposal_id}",
"requires_signature": False,
"terms": {
"publisher": "proposal-mode-demo",
},
}
return FinalizeProposalSuccess(
proposal=committed_payload,
expires_at=expires_at,
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