diff --git a/pyproject.toml b/pyproject.toml index 31443ed..55d5762 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "boto3>=1.35.0", "markdown>=3.6", "bleach>=6.3.0", - "adcp>=2.18.0", # Official ADCP Python client with template format support + "adcp>=3.1.0", # Official ADCP Python client with template format support ] [project.scripts] diff --git a/src/creative_agent/data/standard_formats.py b/src/creative_agent/data/standard_formats.py index aa74610..b6a405f 100644 --- a/src/creative_agent/data/standard_formats.py +++ b/src/creative_agent/data/standard_formats.py @@ -5,7 +5,7 @@ from typing import Any -from adcp import FormatCategory, FormatId +from adcp import FormatCategory, FormatId, get_required_assets from adcp.types.generated_poc.core.format import Assets as LibAssets from adcp.types.generated_poc.core.format import AssetsRequired as LibAssetsRequired from adcp.types.generated_poc.core.format import Renders as LibRender @@ -1716,8 +1716,10 @@ def has_asset_type(req: Any, target_type: AssetType | str) -> bool: results = [ fmt for fmt in results - if fmt.assets_required - and all(any(has_asset_type(req, asset_type) for req in fmt.assets_required) for asset_type in asset_types) + if get_required_assets(fmt) + and all( + any(has_asset_type(req, asset_type) for req in get_required_assets(fmt)) for asset_type in asset_types + ) ] return results diff --git a/src/creative_agent/renderers/base.py b/src/creative_agent/renderers/base.py index 01801fb..d5b0165 100644 --- a/src/creative_agent/renderers/base.py +++ b/src/creative_agent/renderers/base.py @@ -85,15 +85,15 @@ def build_asset_type_map(self, format_obj: Any) -> dict[str, str]: Dictionary mapping asset_id to asset_type string """ asset_type_map = {} - if hasattr(format_obj, "assets_required") and format_obj.assets_required: - for required_asset in format_obj.assets_required: + if hasattr(format_obj, "assets") and format_obj.assets: + for asset in format_obj.assets: # Handle both dict and object access - if isinstance(required_asset, dict): - asset_id = required_asset.get("asset_id") - asset_type = required_asset.get("asset_type") + if isinstance(asset, dict): + asset_id = asset.get("asset_id") + asset_type = asset.get("asset_type") else: - asset_id = getattr(required_asset, "asset_id", None) - asset_type = getattr(required_asset, "asset_type", None) + asset_id = getattr(asset, "asset_id", None) + asset_type = getattr(asset, "asset_type", None) if asset_id and asset_type: # Handle enum or string asset_type diff --git a/src/creative_agent/server.py b/src/creative_agent/server.py index c92b648..6130644 100644 --- a/src/creative_agent/server.py +++ b/src/creative_agent/server.py @@ -714,16 +714,12 @@ def build_creative( format_spec += f"Dimensions: {int(render.dimensions.width)}x{int(render.dimensions.height)}\n" format_spec += "\nRequired Assets:\n" - if output_fmt.assets_required: - for asset_req in output_fmt.assets_required: - # assets_required are always Pydantic models (adcp 2.2.0+) - if hasattr(asset_req, "asset_group_id"): - # Repeatable group (AssetsRequired1) - format_spec += f"- {asset_req.asset_group_id} (repeatable group)\n" - elif hasattr(asset_req, "asset_id"): - # Individual asset (AssetsRequired) - asset_type = getattr(asset_req, "asset_type", "unknown") - format_spec += f"- {asset_req.asset_id} ({asset_type})\n" + required_assets = get_required_assets(output_fmt) + for asset_req in required_assets: + asset_id = getattr(asset_req, "asset_id", None) + asset_type = getattr(asset_req, "asset_type", "unknown") + if asset_id: + format_spec += f"- {asset_id} ({asset_type})\n" # Add brand context if provided brand_context = "" diff --git a/tests/integration/test_template_formats.py b/tests/integration/test_template_formats.py index fb07d26..bfc6a35 100644 --- a/tests/integration/test_template_formats.py +++ b/tests/integration/test_template_formats.py @@ -6,7 +6,7 @@ import json -from adcp import FormatId, ListCreativeFormatsResponse +from adcp import FormatId, ListCreativeFormatsResponse, get_required_assets from adcp.types.generated_poc.enums.format_id_parameter import FormatIdParameter from pydantic import AnyUrl @@ -215,19 +215,20 @@ def test_get_video_template_by_base_id(self): class TestTemplateAssetRequirements: """Test asset requirements for template vs concrete formats.""" - def test_template_formats_have_assets_required(self): - """Template formats should have assets_required defined.""" + def test_template_formats_have_required_assets(self): + """Template formats should have required assets defined.""" # Get display_image template format_id = FormatId(agent_url=AnyUrl(str(AGENT_URL)), id="display_image") fmt = get_format_by_id(format_id) assert fmt is not None - assert fmt.assets_required is not None - assert len(fmt.assets_required) > 0 + required_assets = get_required_assets(fmt) + assert required_assets is not None + assert len(required_assets) > 0 # Find the image asset image_asset = None - for asset in fmt.assets_required: + for asset in required_assets: if asset.asset_id == "banner_image": image_asset = asset break @@ -241,12 +242,13 @@ def test_concrete_formats_have_explicit_requirements(self): fmt = get_format_by_id(format_id) assert fmt is not None - assert fmt.assets_required is not None - assert len(fmt.assets_required) > 0 + required_assets = get_required_assets(fmt) + assert required_assets is not None + assert len(required_assets) > 0 # Find the image asset image_asset = None - for asset in fmt.assets_required: + for asset in required_assets: if asset.asset_id == "banner_image": image_asset = asset break diff --git a/tests/integration/test_tool_response_formats.py b/tests/integration/test_tool_response_formats.py index af7f054..02ed252 100644 --- a/tests/integration/test_tool_response_formats.py +++ b/tests/integration/test_tool_response_formats.py @@ -15,6 +15,7 @@ FormatId, ListCreativeFormatsResponse, PreviewCreativeResponse, + get_required_assets, ) from creative_agent import server @@ -118,16 +119,16 @@ def test_no_extra_wrapper_fields(self): f"Response must have required keys {expected_keys}, got {actual_keys}" ) - def test_assets_required_have_asset_id(self): - """Per ADCP PR #135, all AssetsRequired must have asset_id field.""" + def test_assets_have_asset_id(self): + """Per ADCP PR #135, all assets must have asset_id field.""" result = list_creative_formats() response = ListCreativeFormatsResponse.model_validate(result.structured_content) - formats_with_assets = [fmt for fmt in response.formats if fmt.assets_required] - assert len(formats_with_assets) > 0, "Should have formats with assets_required" + formats_with_assets = [fmt for fmt in response.formats if get_required_assets(fmt)] + assert len(formats_with_assets) > 0, "Should have formats with required assets" for fmt in formats_with_assets: - for asset in fmt.assets_required: + for asset in get_required_assets(fmt): # Access asset_id - will raise AttributeError if missing asset_dict = asset.model_dump() if hasattr(asset, "model_dump") else dict(asset) assert "asset_id" in asset_dict, f"Format {fmt.format_id.id} has asset without asset_id: {asset_dict}" diff --git a/tests/unit/test_info_card_formats.py b/tests/unit/test_info_card_formats.py index d5ef53c..4f7922a 100644 --- a/tests/unit/test_info_card_formats.py +++ b/tests/unit/test_info_card_formats.py @@ -1,6 +1,6 @@ """Tests for info card format definitions.""" -from adcp import FormatId +from adcp import FormatId, get_required_assets from creative_agent.data.format_types import AssetType, Type from creative_agent.data.standard_formats import AGENT_URL, INFO_CARD_FORMATS, filter_formats @@ -95,12 +95,13 @@ def test_requires_product_assets(self): assert fmt.assets assert len(fmt.assets) == 9 # 8 original + impression_tracker - # Check required assets are in assets_required (backward compatibility) - assert fmt.assets_required - assert len(fmt.assets_required) == 3 # Only required=True assets + # Check required assets + required_assets = get_required_assets(fmt) + assert required_assets + assert len(required_assets) == 3 # Only required=True assets - # Check required asset ids in assets_required - required_asset_ids = {get_asset_attr(asset, "asset_id") for asset in fmt.assets_required} + # Check required asset ids + required_asset_ids = {get_asset_attr(asset, "asset_id") for asset in required_assets} assert "product_image" in required_asset_ids assert "product_name" in required_asset_ids assert "product_description" in required_asset_ids @@ -114,7 +115,7 @@ def test_requires_product_assets(self): assert asset_type_map["product_description"] == "text" assert asset_type_map["impression_tracker"] == "url" - # Check optional assets are in assets but not in assets_required + # Check optional assets are in assets but not required optional_asset_ids = { get_asset_attr(asset, "asset_id") for asset in fmt.assets if not get_asset_attr(asset, "required") } @@ -149,12 +150,13 @@ def test_requires_product_assets(self): assert fmt.assets assert len(fmt.assets) == 9 # 8 original + impression_tracker - # Check required assets are in assets_required (backward compatibility) - assert fmt.assets_required - assert len(fmt.assets_required) == 3 # Only required=True assets + # Check required assets + required_assets = get_required_assets(fmt) + assert required_assets + assert len(required_assets) == 3 # Only required=True assets - # Check required asset ids in assets_required - required_asset_ids = {get_asset_attr(asset, "asset_id") for asset in fmt.assets_required} + # Check required asset ids + required_asset_ids = {get_asset_attr(asset, "asset_id") for asset in required_assets} assert "product_image" in required_asset_ids assert "product_name" in required_asset_ids assert "product_description" in required_asset_ids @@ -168,7 +170,7 @@ def test_requires_product_assets(self): assert asset_type_map["product_description"] == "text" assert asset_type_map["impression_tracker"] == "url" - # Check optional assets are in assets but not in assets_required + # Check optional assets are in assets but not required optional_asset_ids = { get_asset_attr(asset, "asset_id") for asset in fmt.assets if not get_asset_attr(asset, "required") } @@ -198,9 +200,10 @@ def test_requires_format_asset(self): format_id = FormatId(agent_url=AGENT_URL, id="format_card_standard") results = filter_formats(format_ids=[format_id]) fmt = results[0] - assert fmt.assets_required - assert len(fmt.assets_required) == 1 - asset = fmt.assets_required[0] + required_assets = get_required_assets(fmt) + assert required_assets + assert len(required_assets) == 1 + asset = required_assets[0] assert get_asset_attr(asset, "asset_id") == "format" assert get_asset_attr(asset, "asset_type") == "text" assert get_asset_attr(asset, "required") is True @@ -228,9 +231,10 @@ def test_requires_format_asset(self): format_id = FormatId(agent_url=AGENT_URL, id="format_card_detailed") results = filter_formats(format_ids=[format_id]) fmt = results[0] - assert fmt.assets_required - assert len(fmt.assets_required) == 1 - asset = fmt.assets_required[0] + required_assets = get_required_assets(fmt) + assert required_assets + assert len(required_assets) == 1 + asset = required_assets[0] assert get_asset_attr(asset, "asset_id") == "format" assert get_asset_attr(asset, "asset_type") == "text" assert get_asset_attr(asset, "required") is True diff --git a/tests/validation/test_template_parameter_validation.py b/tests/validation/test_template_parameter_validation.py index 48270ae..4c55626 100644 --- a/tests/validation/test_template_parameter_validation.py +++ b/tests/validation/test_template_parameter_validation.py @@ -8,7 +8,7 @@ """ import pytest -from adcp import FormatId +from adcp import FormatId, get_required_assets from adcp.types.generated_poc.enums.format_id_parameter import FormatIdParameter from pydantic import AnyUrl, ValidationError @@ -112,7 +112,7 @@ def test_extract_dimensions_from_format_id(self): fmt = get_format_by_id(template_format_id) assert fmt is not None - assert fmt.assets_required is not None + assert get_required_assets(fmt) is not None # Template format accepts dimensions parameter assert FormatIdParameter.dimensions in getattr(fmt, "accepts_parameters", []) @@ -131,7 +131,7 @@ def test_extract_duration_from_format_id(self): fmt = get_format_by_id(template_format_id) assert fmt is not None - assert fmt.assets_required is not None + assert get_required_assets(fmt) is not None # Template format accepts duration parameter assert FormatIdParameter.duration in getattr(fmt, "accepts_parameters", []) @@ -161,7 +161,7 @@ def test_concrete_format_has_explicit_dimensions(self): format_id = FormatId(agent_url=AnyUrl(str(AGENT_URL)), id="display_300x250_image") fmt = get_format_by_id(format_id) - image_asset = next(a for a in fmt.assets_required if a.asset_id == "banner_image") + image_asset = next(a for a in get_required_assets(fmt) if a.asset_id == "banner_image") requirements = getattr(image_asset, "requirements", {}) diff --git a/uv.lock b/uv.lock index 89bd596..546565e 100644 --- a/uv.lock +++ b/uv.lock @@ -24,7 +24,7 @@ wheels = [ [[package]] name = "adcp" -version = "2.18.0" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "a2a-sdk" }, @@ -34,9 +34,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/bd/09b09642a50e1fc0831c42810dd1acbb4f30d14d3d15d265a31e1521a651/adcp-2.18.0.tar.gz", hash = "sha256:9df0262f7a2c0ca2ccfcc1b026837a4d7927b605b311220912fa21ed755f11da", size = 192176, upload-time = "2026-01-09T15:55:38.336Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/3f/b58e5f1bbdf7fbade34291bac2899e196e53ff5d8b2aee80ef62fba73971/adcp-3.1.0.tar.gz", hash = "sha256:9133d8d9d210822bd9f055838be7f0688bd6a3f4bdf75b316bce2f27effffcfe", size = 243588, upload-time = "2026-01-26T20:32:54.741Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/f2/bad9c2a0fd14063ecff4d6b33aed2107fea513a065a6db7921099c1fb533/adcp-2.18.0-py3-none-any.whl", hash = "sha256:b508b879b8fddc5873e73afed4b0749aab31ef68409b5a696ee3975ea7c315b0", size = 221978, upload-time = "2026-01-09T15:55:36.634Z" }, + { url = "https://files.pythonhosted.org/packages/51/9e/f12fa04b8fb3cc53ce43ba59b3d8346600d5531fab7c5a628f26cac16c7c/adcp-3.1.0-py3-none-any.whl", hash = "sha256:8dee1b2dba109bbaecea6ca444276f89f774c862b19eb64602436ae11026b270", size = 305748, upload-time = "2026-01-26T20:32:53.659Z" }, ] [[package]] @@ -73,7 +73,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "adcp", specifier = ">=2.18.0" }, + { name = "adcp", specifier = ">=3.1.0" }, { name = "bleach", specifier = ">=6.3.0" }, { name = "boto3", specifier = ">=1.35.0" }, { name = "fastapi", specifier = ">=0.100.0" },