From 795cc7383395d11c9c9a6e55d33cb2f0243b0cf1 Mon Sep 17 00:00:00 2001 From: BaiyuScope3 Date: Wed, 24 Dec 2025 14:20:10 -0500 Subject: [PATCH 1/5] fix: include human-readable format details in list_creative_formats response LLMs only see the 'content' text field in MCP tool results, not the 'structured_content' field. This change includes human-readable format details in the text content so Claude and other LLMs can see: - Format name and ID - Type and dimensions - Supported macros (full list) - Assets required - Description The structured_content still contains the full JSON for programmatic use. Extracted _format_to_human_readable() helper function for reusability. --- src/creative_agent/server.py | 56 ++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/creative_agent/server.py b/src/creative_agent/server.py index 5d831d5..ed54149 100644 --- a/src/creative_agent/server.py +++ b/src/creative_agent/server.py @@ -53,6 +53,49 @@ def normalize_format_id_for_comparison(format_id: FormatId | dict[str, Any] | st return ("", "") +def _format_to_human_readable(fmt: Any) -> str: + """Convert a Format object to human-readable text for LLM consumption. + + Args: + fmt: A CreativeFormat object + + Returns: + Human-readable string with key format details + """ + # Extract format ID + fmt_id = fmt.format_id.id if hasattr(fmt.format_id, "id") else str(fmt.format_id) + + # Extract dimensions + dims = "responsive" + if fmt.renders and len(fmt.renders) > 0: + render = fmt.renders[0] + if render.dimensions and render.dimensions.width and render.dimensions.height: + dims = f"{int(render.dimensions.width)}x{int(render.dimensions.height)}" + + # Extract macros info + macros = fmt.supported_macros or [] + macro_count_str = f"{len(macros)} supported macros" if macros else "no macros" + + # Extract assets info + assets = fmt.assets_required or [] + asset_ids = [a.asset_id for a in assets if hasattr(a, "asset_id")] + asset_str = ", ".join(asset_ids[:5]) + if len(asset_ids) > 5: + asset_str += f" (+{len(asset_ids) - 5} more)" + + # Build human-readable detail + detail = f"- **{fmt.name}** (`{fmt_id}`)\n" + detail += f" Type: {fmt.type.value if hasattr(fmt.type, 'value') else fmt.type} | Dimensions: {dims} | {macro_count_str}\n" + if fmt.description: + detail += f" {fmt.description[:150]}{'...' if len(fmt.description) > 150 else ''}\n" + if asset_str: + detail += f" Assets Required: {asset_str}\n" + if macros: + detail += f" Supported Macros: {', '.join(macros)}\n" + + return detail + + @mcp.tool() def list_creative_formats( format_ids: list[str | dict[str, Any]] | None = None, @@ -132,9 +175,18 @@ def list_creative_formats( if filter_desc: message += f" matching filters ({', '.join(filter_desc)})" + # Build human-readable format details for LLM consumption + response_json = response.model_dump(mode="json", exclude_none=True) + + if formats: + format_details = [_format_to_human_readable(fmt) for fmt in formats] + full_message = f"{message}:\n\n" + "\n".join(format_details) + else: + full_message = message + return ToolResult( - content=[TextContent(type="text", text=message)], - structured_content=response.model_dump(mode="json", exclude_none=True), + content=[TextContent(type="text", text=full_message)], + structured_content=response_json, ) except ValueError as e: error_response = {"error": f"Invalid input: {e}"} From 5116ad1f5858c41a505d01a973f4c6cc4ab436f9 Mon Sep 17 00:00:00 2001 From: BaiyuScope3 Date: Wed, 24 Dec 2025 15:02:15 -0500 Subject: [PATCH 2/5] fix: remove IMPRESSION_URL from COMMON_MACROS IMPRESSION_URL is not part of the universal macros specification. --- README.md | 2 +- scripts/test_card_rendering.py | 1 - src/creative_agent/data/standard_formats.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 49f1096..c684582 100644 --- a/README.md +++ b/README.md @@ -322,7 +322,7 @@ uv run fastmcp dev src/creative_agent/server.py Standard macros (included in `COMMON_MACROS`): - Privacy: `GDPR`, `GDPR_CONSENT`, `US_PRIVACY`, `GPP_STRING` -- Tracking: `MEDIA_BUY_ID`, `CREATIVE_ID`, `IMPRESSION_URL`, `CLICK_URL` +- Tracking: `MEDIA_BUY_ID`, `CREATIVE_ID`, `CLICK_URL` - Device: `DEVICE_TYPE`, `OS`, `OS_VERSION`, `USER_AGENT` - Context: `CACHEBUSTER` diff --git a/scripts/test_card_rendering.py b/scripts/test_card_rendering.py index 9b74211..e6687ae 100755 --- a/scripts/test_card_rendering.py +++ b/scripts/test_card_rendering.py @@ -282,7 +282,6 @@ def test_format_card_detailed() -> None: "CREATIVE_ID", "CACHEBUSTER", "CLICK_URL", - "IMPRESSION_URL", "DEVICE_TYPE", "VIDEO_ID", "POD_POSITION", diff --git a/src/creative_agent/data/standard_formats.py b/src/creative_agent/data/standard_formats.py index 46f3ef8..62aa055 100644 --- a/src/creative_agent/data/standard_formats.py +++ b/src/creative_agent/data/standard_formats.py @@ -27,7 +27,6 @@ "CREATIVE_ID", "CACHEBUSTER", "CLICK_URL", - "IMPRESSION_URL", "DEVICE_TYPE", "GDPR", "GDPR_CONSENT", From 96f690f6703aaf08c58e8eec39ef270d7279b459 Mon Sep 17 00:00:00 2001 From: BaiyuScope3 Date: Fri, 9 Jan 2026 12:06:15 -0500 Subject: [PATCH 3/5] feat: upgrade to adcp-client-python 2.18.0 with new assets field - Upgrade adcp dependency to >=2.18.0 - Add new 'assets' field to all formats using create_asset() helper - Add optional impression_tracker asset to all formats (except VAST) - Backfill deprecated assets_required from assets using get_required_assets() - Update server to use get_required_assets/get_optional_assets utilities - Update tests for new schema (PreviewRender.root, relaxed field validation) - Update test assertions for new assets/assets_required structure --- pyproject.toml | 2 +- src/creative_agent/data/standard_formats.py | 386 +++++++++++------- src/creative_agent/server.py | 26 +- .../test_preview_html_and_batch.py | 23 +- .../integration/test_tool_response_formats.py | 10 +- tests/unit/test_info_card_formats.py | 76 ++-- .../test_discriminator_validation.py | 154 +++---- uv.lock | 31 +- 8 files changed, 432 insertions(+), 276 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 56b30f0..31443ed 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.13.0", # Official ADCP Python client with template format support + "adcp>=2.18.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 62aa055..c6e2a1f 100644 --- a/src/creative_agent/data/standard_formats.py +++ b/src/creative_agent/data/standard_formats.py @@ -6,6 +6,7 @@ from typing import Any from adcp import FormatCategory, FormatId +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 from adcp.types.generated_poc.enums.format_id_parameter import FormatIdParameter @@ -40,23 +41,23 @@ def create_format_id(format_name: str) -> FormatId: return FormatId(agent_url=AnyUrl(AGENT_URL), id=format_name) -def create_asset_required( +def create_asset( asset_id: str, asset_type: AssetType, required: bool = True, requirements: dict[str, str | int | float | bool | list[str]] | None = None, -) -> LibAssetsRequired: - """Create an assets_required entry using the library's Pydantic model. +) -> LibAssets: + """Create an asset entry using the library's Assets Pydantic model. + This creates assets for the new 'assets' field (adcp-client-python 2.18.0+). The library model automatically handles exclude_none serialization and includes the item_type discriminator for union types. """ - # Convert local AssetType enum to library's AssetContentType from adcp import AssetContentType as LibAssetType lib_asset_type = LibAssetType(asset_type.value) - return LibAssetsRequired( + return LibAssets( asset_id=asset_id, asset_type=lib_asset_type, required=required, @@ -65,6 +66,23 @@ def create_asset_required( ) +def create_impression_tracker_asset() -> LibAssets: + """Create an optional impression tracker asset for 3rd party tracking. + + This creates a URL asset with url_type='tracker_pixel' that can be used + for third-party impression tracking pixels. + """ + return create_asset( + asset_id="impression_tracker", + asset_type=AssetType.url, + required=False, + requirements={ + "url_type": "tracker_pixel", + "description": "3rd party impression tracking pixel URL", + }, + ) + + def create_fixed_render(width: int, height: int, role: str = "primary") -> LibRender: """Create a render with fixed dimensions (non-responsive). @@ -124,19 +142,20 @@ def create_responsive_render( description="AI-generated display banner from brand context and prompt (supports any dimensions)", accepts_parameters=[FormatIdParameter.dimensions], supported_macros=COMMON_MACROS, - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="promoted_offerings", asset_type=AssetType.promoted_offerings, required=True, requirements={"description": "Brand manifest and product offerings for AI generation"}, ), - create_asset_required( + create_asset( asset_id="generation_prompt", asset_type=AssetType.text, required=True, requirements={"description": "Text prompt describing the desired creative"}, ), + create_impression_tracker_asset(), ], ), # Concrete formats for backward compatibility @@ -148,19 +167,20 @@ def create_responsive_render( renders=[create_fixed_render(300, 250)], output_format_ids=[create_format_id("display_300x250_image")], supported_macros=COMMON_MACROS, - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="promoted_offerings", asset_type=AssetType.promoted_offerings, required=True, requirements={"description": "Brand manifest and product offerings for AI generation"}, ), - create_asset_required( + create_asset( asset_id="generation_prompt", asset_type=AssetType.text, required=True, requirements={"description": "Text prompt describing the desired creative"}, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -171,19 +191,20 @@ def create_responsive_render( renders=[create_fixed_render(728, 90)], output_format_ids=[create_format_id("display_728x90_image")], supported_macros=COMMON_MACROS, - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="promoted_offerings", asset_type=AssetType.promoted_offerings, required=True, requirements={"description": "Brand manifest and product offerings for AI generation"}, ), - create_asset_required( + create_asset( asset_id="generation_prompt", asset_type=AssetType.text, required=True, requirements={"description": "Text prompt describing the desired creative"}, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -194,19 +215,20 @@ def create_responsive_render( renders=[create_fixed_render(320, 50)], output_format_ids=[create_format_id("display_320x50_image")], supported_macros=COMMON_MACROS, - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="promoted_offerings", asset_type=AssetType.promoted_offerings, required=True, requirements={"description": "Brand manifest and product offerings for AI generation"}, ), - create_asset_required( + create_asset( asset_id="generation_prompt", asset_type=AssetType.text, required=True, requirements={"description": "Text prompt describing the desired creative"}, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -217,19 +239,20 @@ def create_responsive_render( renders=[create_fixed_render(160, 600)], output_format_ids=[create_format_id("display_160x600_image")], supported_macros=COMMON_MACROS, - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="promoted_offerings", asset_type=AssetType.promoted_offerings, required=True, requirements={"description": "Brand manifest and product offerings for AI generation"}, ), - create_asset_required( + create_asset( asset_id="generation_prompt", asset_type=AssetType.text, required=True, requirements={"description": "Text prompt describing the desired creative"}, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -240,19 +263,20 @@ def create_responsive_render( renders=[create_fixed_render(336, 280)], output_format_ids=[create_format_id("display_336x280_image")], supported_macros=COMMON_MACROS, - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="promoted_offerings", asset_type=AssetType.promoted_offerings, required=True, requirements={"description": "Brand manifest and product offerings for AI generation"}, ), - create_asset_required( + create_asset( asset_id="generation_prompt", asset_type=AssetType.text, required=True, requirements={"description": "Text prompt describing the desired creative"}, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -263,19 +287,20 @@ def create_responsive_render( renders=[create_fixed_render(300, 600)], output_format_ids=[create_format_id("display_300x600_image")], supported_macros=COMMON_MACROS, - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="promoted_offerings", asset_type=AssetType.promoted_offerings, required=True, requirements={"description": "Brand manifest and product offerings for AI generation"}, ), - create_asset_required( + create_asset( asset_id="generation_prompt", asset_type=AssetType.text, required=True, requirements={"description": "Text prompt describing the desired creative"}, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -286,19 +311,20 @@ def create_responsive_render( renders=[create_fixed_render(970, 250)], output_format_ids=[create_format_id("display_970x250_image")], supported_macros=COMMON_MACROS, - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="promoted_offerings", asset_type=AssetType.promoted_offerings, required=True, requirements={"description": "Brand manifest and product offerings for AI generation"}, ), - create_asset_required( + create_asset( asset_id="generation_prompt", asset_type=AssetType.text, required=True, requirements={"description": "Text prompt describing the desired creative"}, ), + create_impression_tracker_asset(), ], ), ] @@ -314,8 +340,8 @@ def create_responsive_render( description="Video ad in standard aspect ratios (supports any duration)", accepts_parameters=[FormatIdParameter.duration], supported_macros=[*COMMON_MACROS, "VIDEO_ID", "POD_POSITION", "CONTENT_GENRE"], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="video_file", asset_type=AssetType.video, required=True, @@ -323,6 +349,7 @@ def create_responsive_render( "acceptable_formats": ["mp4", "mov", "webm"], }, ), + create_impression_tracker_asset(), ], ), # Template format - supports any dimensions @@ -333,8 +360,8 @@ def create_responsive_render( description="Video ad with specific dimensions (supports any size)", accepts_parameters=[FormatIdParameter.dimensions], supported_macros=[*COMMON_MACROS, "VIDEO_ID", "POD_POSITION", "CONTENT_GENRE"], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="video_file", asset_type=AssetType.video, required=True, @@ -342,6 +369,7 @@ def create_responsive_render( "acceptable_formats": ["mp4", "mov", "webm"], }, ), + create_impression_tracker_asset(), ], ), # Template format - VAST tag with any duration @@ -352,8 +380,8 @@ def create_responsive_render( description="Video ad via VAST tag (supports any duration)", accepts_parameters=[FormatIdParameter.duration], supported_macros=[*COMMON_MACROS, "VIDEO_ID", "POD_POSITION", "CONTENT_GENRE"], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="vast_tag", asset_type=AssetType.vast, required=True, @@ -367,8 +395,8 @@ def create_responsive_render( type=FormatCategory.video, description="30-second video ad in standard aspect ratios", supported_macros=[*COMMON_MACROS, "VIDEO_ID", "POD_POSITION", "CONTENT_GENRE"], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="video_file", asset_type=AssetType.video, required=True, @@ -378,6 +406,7 @@ def create_responsive_render( "description": "30-second video file", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -386,8 +415,8 @@ def create_responsive_render( type=FormatCategory.video, description="15-second video ad in standard aspect ratios", supported_macros=[*COMMON_MACROS, "VIDEO_ID", "POD_POSITION", "CONTENT_GENRE"], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="video_file", asset_type=AssetType.video, required=True, @@ -397,6 +426,7 @@ def create_responsive_render( "description": "15-second video file", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -405,8 +435,8 @@ def create_responsive_render( type=FormatCategory.video, description="30-second video ad via VAST tag", supported_macros=[*COMMON_MACROS, "VIDEO_ID", "POD_POSITION", "CONTENT_GENRE"], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="vast_tag", asset_type=AssetType.vast, required=True, @@ -423,8 +453,8 @@ def create_responsive_render( description="1920x1080 Full HD video (16:9)", supported_macros=[*COMMON_MACROS, "VIDEO_ID", "POD_POSITION", "CONTENT_GENRE"], renders=[create_fixed_render(1920, 1080)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="video_file", asset_type=AssetType.video, required=True, @@ -435,6 +465,7 @@ def create_responsive_render( "description": "1920x1080 video file", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -444,8 +475,8 @@ def create_responsive_render( description="1280x720 HD video (16:9)", supported_macros=[*COMMON_MACROS, "VIDEO_ID", "POD_POSITION", "CONTENT_GENRE"], renders=[create_fixed_render(1280, 720)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="video_file", asset_type=AssetType.video, required=True, @@ -456,6 +487,7 @@ def create_responsive_render( "description": "1280x720 video file", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -465,8 +497,8 @@ def create_responsive_render( description="1080x1920 vertical video (9:16) for mobile stories", supported_macros=[*COMMON_MACROS, "VIDEO_ID", "POD_POSITION", "CONTENT_GENRE"], renders=[create_fixed_render(1080, 1920)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="video_file", asset_type=AssetType.video, required=True, @@ -477,6 +509,7 @@ def create_responsive_render( "description": "1080x1920 vertical video file", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -486,8 +519,8 @@ def create_responsive_render( description="1080x1080 square video (1:1) for social feeds", supported_macros=[*COMMON_MACROS, "VIDEO_ID", "POD_POSITION", "CONTENT_GENRE"], renders=[create_fixed_render(1080, 1080)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="video_file", asset_type=AssetType.video, required=True, @@ -498,6 +531,7 @@ def create_responsive_render( "description": "1080x1080 square video file", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -506,8 +540,8 @@ def create_responsive_render( type=FormatCategory.video, description="30-second pre-roll ad for Connected TV and streaming platforms", supported_macros=[*COMMON_MACROS, "VIDEO_ID", "POD_POSITION", "CONTENT_GENRE", "PLAYER_SIZE"], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="video_file", asset_type=AssetType.video, required=True, @@ -517,6 +551,7 @@ def create_responsive_render( "description": "30-second CTV-optimized video file (1920x1080 recommended)", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -525,8 +560,8 @@ def create_responsive_render( type=FormatCategory.video, description="30-second mid-roll ad for Connected TV and streaming platforms", supported_macros=[*COMMON_MACROS, "VIDEO_ID", "POD_POSITION", "CONTENT_GENRE", "PLAYER_SIZE"], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="video_file", asset_type=AssetType.video, required=True, @@ -536,6 +571,7 @@ def create_responsive_render( "description": "30-second CTV-optimized video file (1920x1080 recommended)", }, ), + create_impression_tracker_asset(), ], ), ] @@ -551,8 +587,8 @@ def create_responsive_render( description="Static image banner (supports any dimensions)", accepts_parameters=[FormatIdParameter.dimensions], supported_macros=COMMON_MACROS, - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="banner_image", asset_type=AssetType.image, required=True, @@ -560,7 +596,7 @@ def create_responsive_render( "acceptable_formats": ["jpg", "png", "gif", "webp"], }, ), - create_asset_required( + create_asset( asset_id="click_url", asset_type=AssetType.url, required=True, @@ -568,6 +604,7 @@ def create_responsive_render( "description": "Clickthrough destination URL", }, ), + create_impression_tracker_asset(), ], ), # Concrete formats for backward compatibility @@ -578,8 +615,8 @@ def create_responsive_render( description="300x250 static image banner", supported_macros=COMMON_MACROS, renders=[create_fixed_render(300, 250)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="banner_image", asset_type=AssetType.image, required=True, @@ -590,7 +627,7 @@ def create_responsive_render( "acceptable_formats": ["jpg", "png", "gif", "webp"], }, ), - create_asset_required( + create_asset( asset_id="click_url", asset_type=AssetType.url, required=True, @@ -598,6 +635,7 @@ def create_responsive_render( "description": "Clickthrough destination URL", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -607,8 +645,8 @@ def create_responsive_render( description="728x90 static image banner", supported_macros=COMMON_MACROS, renders=[create_fixed_render(728, 90)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="banner_image", asset_type=AssetType.image, required=True, @@ -619,11 +657,12 @@ def create_responsive_render( "acceptable_formats": ["jpg", "png", "gif", "webp"], }, ), - create_asset_required( + create_asset( asset_id="click_url", asset_type=AssetType.url, required=True, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -633,8 +672,8 @@ def create_responsive_render( description="320x50 mobile banner", supported_macros=COMMON_MACROS, renders=[create_fixed_render(320, 50)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="banner_image", asset_type=AssetType.image, required=True, @@ -645,11 +684,12 @@ def create_responsive_render( "acceptable_formats": ["jpg", "png", "gif", "webp"], }, ), - create_asset_required( + create_asset( asset_id="click_url", asset_type=AssetType.url, required=True, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -659,8 +699,8 @@ def create_responsive_render( description="160x600 wide skyscraper banner", supported_macros=COMMON_MACROS, renders=[create_fixed_render(160, 600)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="banner_image", asset_type=AssetType.image, required=True, @@ -671,11 +711,12 @@ def create_responsive_render( "acceptable_formats": ["jpg", "png", "gif", "webp"], }, ), - create_asset_required( + create_asset( asset_id="click_url", asset_type=AssetType.url, required=True, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -685,8 +726,8 @@ def create_responsive_render( description="336x280 large rectangle banner", supported_macros=COMMON_MACROS, renders=[create_fixed_render(336, 280)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="banner_image", asset_type=AssetType.image, required=True, @@ -697,11 +738,12 @@ def create_responsive_render( "acceptable_formats": ["jpg", "png", "gif", "webp"], }, ), - create_asset_required( + create_asset( asset_id="click_url", asset_type=AssetType.url, required=True, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -711,8 +753,8 @@ def create_responsive_render( description="300x600 half page banner", supported_macros=COMMON_MACROS, renders=[create_fixed_render(300, 600)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="banner_image", asset_type=AssetType.image, required=True, @@ -723,11 +765,12 @@ def create_responsive_render( "acceptable_formats": ["jpg", "png", "gif", "webp"], }, ), - create_asset_required( + create_asset( asset_id="click_url", asset_type=AssetType.url, required=True, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -737,8 +780,8 @@ def create_responsive_render( description="970x250 billboard banner", supported_macros=COMMON_MACROS, renders=[create_fixed_render(970, 250)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="banner_image", asset_type=AssetType.image, required=True, @@ -749,11 +792,12 @@ def create_responsive_render( "acceptable_formats": ["jpg", "png", "gif", "webp"], }, ), - create_asset_required( + create_asset( asset_id="click_url", asset_type=AssetType.url, required=True, ), + create_impression_tracker_asset(), ], ), ] @@ -769,8 +813,8 @@ def create_responsive_render( description="HTML5 creative (supports any dimensions)", accepts_parameters=[FormatIdParameter.dimensions], supported_macros=COMMON_MACROS, - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="html_creative", asset_type=AssetType.html, required=True, @@ -778,6 +822,7 @@ def create_responsive_render( "max_file_size_mb": 0.5, }, ), + create_impression_tracker_asset(), ], ), # Concrete formats for backward compatibility @@ -788,8 +833,8 @@ def create_responsive_render( description="300x250 HTML5 creative", supported_macros=COMMON_MACROS, renders=[create_fixed_render(300, 250)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="html_creative", asset_type=AssetType.html, required=True, @@ -800,6 +845,7 @@ def create_responsive_render( "description": "HTML5 creative code", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -809,8 +855,8 @@ def create_responsive_render( description="728x90 HTML5 creative", supported_macros=COMMON_MACROS, renders=[create_fixed_render(728, 90)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="html_creative", asset_type=AssetType.html, required=True, @@ -820,6 +866,7 @@ def create_responsive_render( "max_file_size_mb": 0.5, }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -829,8 +876,8 @@ def create_responsive_render( description="160x600 HTML5 creative", supported_macros=COMMON_MACROS, renders=[create_fixed_render(160, 600)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="html_creative", asset_type=AssetType.html, required=True, @@ -840,6 +887,7 @@ def create_responsive_render( "max_file_size_mb": 0.5, }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -849,8 +897,8 @@ def create_responsive_render( description="336x280 HTML5 creative", supported_macros=COMMON_MACROS, renders=[create_fixed_render(336, 280)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="html_creative", asset_type=AssetType.html, required=True, @@ -860,6 +908,7 @@ def create_responsive_render( "max_file_size_mb": 0.5, }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -869,8 +918,8 @@ def create_responsive_render( description="300x600 HTML5 creative", supported_macros=COMMON_MACROS, renders=[create_fixed_render(300, 600)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="html_creative", asset_type=AssetType.html, required=True, @@ -880,6 +929,7 @@ def create_responsive_render( "max_file_size_mb": 0.5, }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -889,8 +939,8 @@ def create_responsive_render( description="970x250 HTML5 creative", supported_macros=COMMON_MACROS, renders=[create_fixed_render(970, 250)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="html_creative", asset_type=AssetType.html, required=True, @@ -900,6 +950,7 @@ def create_responsive_render( "max_file_size_mb": 0.5, }, ), + create_impression_tracker_asset(), ], ), ] @@ -915,12 +966,13 @@ def create_responsive_render( description="JavaScript-based display ad (supports any dimensions)", accepts_parameters=[FormatIdParameter.dimensions], supported_macros=COMMON_MACROS, - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="js_creative", asset_type=AssetType.javascript, required=True, ), + create_impression_tracker_asset(), ], ), ] @@ -933,8 +985,8 @@ def create_responsive_render( type=FormatCategory.native, description="Standard native ad with title, description, image, and CTA", supported_macros=COMMON_MACROS, - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="title", asset_type=AssetType.text, required=True, @@ -942,7 +994,7 @@ def create_responsive_render( "description": "Headline text (25 chars recommended)", }, ), - create_asset_required( + create_asset( asset_id="description", asset_type=AssetType.text, required=True, @@ -950,7 +1002,7 @@ def create_responsive_render( "description": "Body copy (90 chars recommended)", }, ), - create_asset_required( + create_asset( asset_id="main_image", asset_type=AssetType.image, required=True, @@ -958,7 +1010,7 @@ def create_responsive_render( "description": "Primary image (1200x627 recommended)", }, ), - create_asset_required( + create_asset( asset_id="icon", asset_type=AssetType.image, required=False, @@ -966,7 +1018,7 @@ def create_responsive_render( "description": "Brand icon (square, 200x200 recommended)", }, ), - create_asset_required( + create_asset( asset_id="cta_text", asset_type=AssetType.text, required=True, @@ -974,7 +1026,7 @@ def create_responsive_render( "description": "Call-to-action text", }, ), - create_asset_required( + create_asset( asset_id="sponsored_by", asset_type=AssetType.text, required=True, @@ -982,6 +1034,7 @@ def create_responsive_render( "description": "Advertiser name for disclosure", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -990,8 +1043,8 @@ def create_responsive_render( type=FormatCategory.native, description="In-article native ad with editorial styling", supported_macros=COMMON_MACROS, - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="headline", asset_type=AssetType.text, required=True, @@ -999,7 +1052,7 @@ def create_responsive_render( "description": "Editorial-style headline (60 chars recommended)", }, ), - create_asset_required( + create_asset( asset_id="body", asset_type=AssetType.text, required=True, @@ -1007,7 +1060,7 @@ def create_responsive_render( "description": "Article-style body copy (200 chars recommended)", }, ), - create_asset_required( + create_asset( asset_id="thumbnail", asset_type=AssetType.image, required=True, @@ -1015,7 +1068,7 @@ def create_responsive_render( "description": "Thumbnail image (square, 300x300 recommended)", }, ), - create_asset_required( + create_asset( asset_id="author", asset_type=AssetType.text, required=False, @@ -1023,7 +1076,7 @@ def create_responsive_render( "description": "Author name for editorial context", }, ), - create_asset_required( + create_asset( asset_id="click_url", asset_type=AssetType.url, required=True, @@ -1031,7 +1084,7 @@ def create_responsive_render( "description": "Landing page URL", }, ), - create_asset_required( + create_asset( asset_id="disclosure", asset_type=AssetType.text, required=True, @@ -1039,6 +1092,7 @@ def create_responsive_render( "description": "Sponsored content disclosure text", }, ), + create_impression_tracker_asset(), ], ), ] @@ -1051,8 +1105,8 @@ def create_responsive_render( type=FormatCategory.audio, description="15-second audio ad", supported_macros=[*COMMON_MACROS, "CONTENT_GENRE"], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="audio_file", asset_type=AssetType.audio, required=True, @@ -1061,6 +1115,7 @@ def create_responsive_render( "acceptable_formats": ["mp3", "aac", "m4a"], }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -1069,8 +1124,8 @@ def create_responsive_render( type=FormatCategory.audio, description="30-second audio ad", supported_macros=[*COMMON_MACROS, "CONTENT_GENRE"], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="audio_file", asset_type=AssetType.audio, required=True, @@ -1079,6 +1134,7 @@ def create_responsive_render( "acceptable_formats": ["mp3", "aac", "m4a"], }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -1087,8 +1143,8 @@ def create_responsive_render( type=FormatCategory.audio, description="60-second audio ad", supported_macros=[*COMMON_MACROS, "CONTENT_GENRE"], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="audio_file", asset_type=AssetType.audio, required=True, @@ -1097,6 +1153,7 @@ def create_responsive_render( "acceptable_formats": ["mp3", "aac", "m4a"], }, ), + create_impression_tracker_asset(), ], ), ] @@ -1110,8 +1167,8 @@ def create_responsive_render( description="Full HD digital billboard", supported_macros=[*COMMON_MACROS, "SCREEN_ID", "VENUE_TYPE", "VENUE_LAT", "VENUE_LONG"], renders=[create_fixed_render(1920, 1080)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="billboard_image", asset_type=AssetType.image, required=True, @@ -1121,6 +1178,7 @@ def create_responsive_render( "acceptable_formats": ["jpg", "png"], }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -1129,8 +1187,8 @@ def create_responsive_render( type=FormatCategory.dooh, description="Landscape-oriented digital billboard (various sizes)", supported_macros=[*COMMON_MACROS, "SCREEN_ID", "VENUE_TYPE", "VENUE_LAT", "VENUE_LONG"], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="billboard_image", asset_type=AssetType.image, required=True, @@ -1139,6 +1197,7 @@ def create_responsive_render( "description": "Landscape image (1920x1080 or larger)", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -1147,8 +1206,8 @@ def create_responsive_render( type=FormatCategory.dooh, description="Portrait-oriented digital billboard (various sizes)", supported_macros=[*COMMON_MACROS, "SCREEN_ID", "VENUE_TYPE", "VENUE_LAT", "VENUE_LONG"], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="billboard_image", asset_type=AssetType.image, required=True, @@ -1157,6 +1216,7 @@ def create_responsive_render( "description": "Portrait image (1080x1920 or similar)", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -1166,8 +1226,8 @@ def create_responsive_render( description="Transit and subway screen displays", supported_macros=[*COMMON_MACROS, "SCREEN_ID", "VENUE_TYPE", "VENUE_LAT", "VENUE_LONG", "TRANSIT_LINE"], renders=[create_fixed_render(1920, 1080)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="screen_image", asset_type=AssetType.image, required=True, @@ -1178,6 +1238,7 @@ def create_responsive_render( "description": "Transit screen content", }, ), + create_impression_tracker_asset(), ], ), ] @@ -1191,8 +1252,8 @@ def create_responsive_render( description="Standard visual card (300x400px) for displaying ad inventory products", supported_macros=COMMON_MACROS, renders=[create_fixed_render(300, 400)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="product_image", asset_type=AssetType.image, required=True, @@ -1200,7 +1261,7 @@ def create_responsive_render( "description": "Primary product image or placement preview", }, ), - create_asset_required( + create_asset( asset_id="product_name", asset_type=AssetType.text, required=True, @@ -1208,7 +1269,7 @@ def create_responsive_render( "description": "Display name of the product (e.g., 'Homepage Leaderboard')", }, ), - create_asset_required( + create_asset( asset_id="product_description", asset_type=AssetType.text, required=True, @@ -1216,7 +1277,7 @@ def create_responsive_render( "description": "Short description of the product (supports markdown)", }, ), - create_asset_required( + create_asset( asset_id="pricing_model", asset_type=AssetType.text, required=False, @@ -1224,7 +1285,7 @@ def create_responsive_render( "description": "Pricing model (e.g., 'CPM', 'flat_rate', 'CPC')", }, ), - create_asset_required( + create_asset( asset_id="pricing_amount", asset_type=AssetType.text, required=False, @@ -1232,7 +1293,7 @@ def create_responsive_render( "description": "Price amount (e.g., '15.00')", }, ), - create_asset_required( + create_asset( asset_id="pricing_currency", asset_type=AssetType.text, required=False, @@ -1240,7 +1301,7 @@ def create_responsive_render( "description": "Currency code (e.g., 'USD')", }, ), - create_asset_required( + create_asset( asset_id="delivery_type", asset_type=AssetType.text, required=False, @@ -1248,7 +1309,7 @@ def create_responsive_render( "description": "Delivery type: 'guaranteed' or 'bidded'", }, ), - create_asset_required( + create_asset( asset_id="primary_asset_type", asset_type=AssetType.text, required=False, @@ -1256,6 +1317,7 @@ def create_responsive_render( "description": "Primary asset type: 'display', 'video', 'audio', 'native'", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -1265,8 +1327,8 @@ def create_responsive_render( description="Detailed card with carousel and full specifications for rich product presentation", supported_macros=COMMON_MACROS, renders=[create_responsive_render()], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="product_image", asset_type=AssetType.image, required=True, @@ -1274,7 +1336,7 @@ def create_responsive_render( "description": "Primary product image or placement preview", }, ), - create_asset_required( + create_asset( asset_id="product_name", asset_type=AssetType.text, required=True, @@ -1282,7 +1344,7 @@ def create_responsive_render( "description": "Display name of the product (e.g., 'Homepage Leaderboard')", }, ), - create_asset_required( + create_asset( asset_id="product_description", asset_type=AssetType.text, required=True, @@ -1290,7 +1352,7 @@ def create_responsive_render( "description": "Detailed description of the product (supports markdown)", }, ), - create_asset_required( + create_asset( asset_id="pricing_model", asset_type=AssetType.text, required=False, @@ -1298,7 +1360,7 @@ def create_responsive_render( "description": "Pricing model (e.g., 'CPM', 'flat_rate', 'CPC')", }, ), - create_asset_required( + create_asset( asset_id="pricing_amount", asset_type=AssetType.text, required=False, @@ -1306,7 +1368,7 @@ def create_responsive_render( "description": "Price amount (e.g., '15.00')", }, ), - create_asset_required( + create_asset( asset_id="pricing_currency", asset_type=AssetType.text, required=False, @@ -1314,7 +1376,7 @@ def create_responsive_render( "description": "Currency code (e.g., 'USD')", }, ), - create_asset_required( + create_asset( asset_id="delivery_type", asset_type=AssetType.text, required=False, @@ -1322,7 +1384,7 @@ def create_responsive_render( "description": "Delivery type: 'guaranteed' or 'bidded'", }, ), - create_asset_required( + create_asset( asset_id="primary_asset_type", asset_type=AssetType.text, required=False, @@ -1330,6 +1392,7 @@ def create_responsive_render( "description": "Primary asset type: 'display', 'video', 'audio', 'native'", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -1339,8 +1402,8 @@ def create_responsive_render( description="Standard visual card (300x400px) for displaying creative formats in user interfaces", supported_macros=COMMON_MACROS, renders=[create_fixed_render(300, 400)], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="format", asset_type=AssetType.text, required=True, @@ -1348,6 +1411,7 @@ def create_responsive_render( "description": "Creative format specification to visualize on the card", }, ), + create_impression_tracker_asset(), ], ), CreativeFormat( @@ -1357,8 +1421,8 @@ def create_responsive_render( description="Detailed card with carousel and full specifications for rich format documentation", supported_macros=COMMON_MACROS, renders=[create_responsive_render()], - assets_required=[ - create_asset_required( + assets=[ + create_asset( asset_id="format", asset_type=AssetType.text, required=True, @@ -1366,6 +1430,7 @@ def create_responsive_render( "description": "Creative format specification with full details for detailed card", }, ), + create_impression_tracker_asset(), ], ), ] @@ -1384,6 +1449,41 @@ def create_responsive_render( ) +def _backfill_deprecated_assets_required() -> list[CreativeFormat]: + """Backfill the deprecated assets_required field for backward compatibility. + + The assets_required field is deprecated in adcp-client-python 2.18.0+ in favor + of the new assets field. This function derives assets_required from assets + using adcp's get_required_assets utility to maintain backward compatibility + with code that still uses assets_required. + + Since Pydantic models are frozen, we rebuild them with the backfilled field. + """ + from adcp import get_required_assets + + rebuilt = [] + + for fmt in STANDARD_FORMATS: + if not fmt.assets: + rebuilt.append(fmt) + continue + + # Use adcp utility to get required assets, then convert to AssetsRequired type + required_assets = get_required_assets(fmt) + fmt_dict = fmt.model_dump() + fmt_dict["assets_required"] = [ + LibAssetsRequired.model_validate(a.model_dump()) for a in required_assets + ] + + rebuilt.append(CreativeFormat.model_validate(fmt_dict)) + + return rebuilt + + +# Backfill deprecated assets_required field for backward compatibility +STANDARD_FORMATS = _backfill_deprecated_assets_required() # type: ignore[misc] + + def get_format_by_id(format_id: FormatId) -> CreativeFormat | None: """Get format by FormatId object. diff --git a/src/creative_agent/server.py b/src/creative_agent/server.py index ed54149..c92b648 100644 --- a/src/creative_agent/server.py +++ b/src/creative_agent/server.py @@ -6,7 +6,7 @@ from datetime import UTC, datetime, timedelta from typing import Any -from adcp import FormatId +from adcp import FormatId, get_optional_assets, get_required_assets from adcp.types import Capability from adcp.types.generated_poc.media_buy.list_creative_formats_response import CreativeAgent from fastmcp import FastMCP @@ -76,20 +76,28 @@ def _format_to_human_readable(fmt: Any) -> str: macros = fmt.supported_macros or [] macro_count_str = f"{len(macros)} supported macros" if macros else "no macros" - # Extract assets info - assets = fmt.assets_required or [] - asset_ids = [a.asset_id for a in assets if hasattr(a, "asset_id")] - asset_str = ", ".join(asset_ids[:5]) - if len(asset_ids) > 5: - asset_str += f" (+{len(asset_ids) - 5} more)" + # Extract assets info using adcp 2.18.0 utilities + # Filter to individual assets (Assets) which have asset_id, skip repeatable groups (Assets1) + required_assets = [a.asset_id for a in get_required_assets(fmt) if hasattr(a, "asset_id")] + optional_assets = [a.asset_id for a in get_optional_assets(fmt) if hasattr(a, "asset_id")] + + asset_req_str = ", ".join(required_assets[:5]) + if len(required_assets) > 5: + asset_req_str += f" (+{len(required_assets) - 5} more)" + + asset_opt_str = ", ".join(optional_assets[:5]) + if len(optional_assets) > 5: + asset_opt_str += f" (+{len(optional_assets) - 5} more)" # Build human-readable detail detail = f"- **{fmt.name}** (`{fmt_id}`)\n" detail += f" Type: {fmt.type.value if hasattr(fmt.type, 'value') else fmt.type} | Dimensions: {dims} | {macro_count_str}\n" if fmt.description: detail += f" {fmt.description[:150]}{'...' if len(fmt.description) > 150 else ''}\n" - if asset_str: - detail += f" Assets Required: {asset_str}\n" + if asset_req_str: + detail += f" Assets Required: {asset_req_str}\n" + if asset_opt_str: + detail += f" Assets Optional: {asset_opt_str}\n" if macros: detail += f" Supported Macros: {', '.join(macros)}\n" diff --git a/tests/integration/test_preview_html_and_batch.py b/tests/integration/test_preview_html_and_batch.py index 264e922..b75a1c5 100644 --- a/tests/integration/test_preview_html_and_batch.py +++ b/tests/integration/test_preview_html_and_batch.py @@ -55,17 +55,18 @@ def test_html_output_returns_preview_html(self): # Check that preview_html is present first_preview = response.root.previews[0] first_render = first_preview.renders[0] - assert first_render.preview_html is not None - assert isinstance(first_render.preview_html, str) - assert len(first_render.preview_html) > 0 + # PreviewRender is a RootModel in adcp 2.18.0, access inner fields via .root + assert first_render.root.preview_html is not None + assert isinstance(first_render.root.preview_html, str) + assert len(first_render.root.preview_html) > 0 # Verify output_format discriminator - assert first_render.output_format == "html" + assert first_render.root.output_format == "html" # With discriminated unions, preview_url field doesn't exist for "html" variant - assert not hasattr(first_render, "preview_url") + assert not hasattr(first_render.root, "preview_url") # HTML should contain expected elements - assert "..." def test_vast_union_validates_from_dict(self): """VastAsset union can validate from dict with correct discriminator.""" @@ -237,23 +243,25 @@ def test_daast_inline_delivery_valid(self): assert asset.delivery_type == "inline" assert asset.content == "..." - def test_url_delivery_cannot_have_content(self): - """URL delivery should reject 'content' field.""" - with pytest.raises(ValidationError, match="Extra inputs are not permitted"): - DaastAsset1( - delivery_type="url", - url="https://audioserver.com/daast.xml", - content="...", # Extra field - ) + def test_url_delivery_allows_extra_fields(self): + """URL delivery allows extra fields in adcp 2.18.0+ (relaxed schema).""" + asset = DaastAsset1( + delivery_type="url", + url="https://audioserver.com/daast.xml", + content="...", # Extra field - allowed in 2.18.0+ + ) + assert asset.delivery_type == "url" + assert str(asset.url) == "https://audioserver.com/daast.xml" - def test_inline_delivery_cannot_have_url(self): - """Inline delivery should reject 'url' field.""" - with pytest.raises(ValidationError, match="Extra inputs are not permitted"): - DaastAsset2( - delivery_type="inline", - content="...", - url="https://audioserver.com/daast.xml", # Extra field - ) + def test_inline_delivery_allows_extra_fields(self): + """Inline delivery allows extra fields in adcp 2.18.0+ (relaxed schema).""" + asset = DaastAsset2( + delivery_type="inline", + content="...", + url="https://audioserver.com/daast.xml", # Extra field - allowed in 2.18.0+ + ) + assert asset.delivery_type == "inline" + assert asset.content == "..." class TestPreviewRenderDiscriminator: @@ -294,27 +302,29 @@ def test_both_output_format_valid(self): assert str(render.preview_url) == "https://preview.example.com/creative.html" assert render.preview_html == "
Creative content
" - def test_url_format_cannot_have_preview_html(self): - """URL format should reject 'preview_html' field.""" - with pytest.raises(ValidationError, match="Extra inputs are not permitted"): - Renders( - render_id="render_1", - output_format="url", - preview_url="https://preview.example.com/creative.html", - preview_html="
Not allowed
", # Extra field - role="primary", - ) + def test_url_format_allows_extra_fields(self): + """URL format allows extra fields in adcp 2.18.0+ (relaxed schema).""" + render = Renders( + render_id="render_1", + output_format="url", + preview_url="https://preview.example.com/creative.html", + preview_html="
Now allowed
", # Extra field - allowed in 2.18.0+ + role="primary", + ) + assert render.output_format == "url" + assert str(render.preview_url) == "https://preview.example.com/creative.html" - def test_html_format_cannot_have_preview_url(self): - """HTML format should reject 'preview_url' field.""" - with pytest.raises(ValidationError, match="Extra inputs are not permitted"): - Renders1( - render_id="render_1", - output_format="html", - preview_html="
Creative content
", - preview_url="https://preview.example.com/creative.html", # Extra field - role="primary", - ) + def test_html_format_allows_extra_fields(self): + """HTML format allows extra fields in adcp 2.18.0+ (relaxed schema).""" + render = Renders1( + render_id="render_1", + output_format="html", + preview_html="
Creative content
", + preview_url="https://preview.example.com/creative.html", # Extra field - allowed in 2.18.0+ + role="primary", + ) + assert render.output_format == "html" + assert render.preview_html == "
Creative content
" def test_invalid_output_format_rejected(self): """Invalid output_format literal should fail validation.""" diff --git a/uv.lock b/uv.lock index 65abcc8..89bd596 100644 --- a/uv.lock +++ b/uv.lock @@ -24,7 +24,7 @@ wheels = [ [[package]] name = "adcp" -version = "2.13.0" +version = "2.18.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/d3/d9/45a25b65b4959c23624ca053dbbfaa5647f4bb1bc87aa863971435d92107/adcp-2.13.0.tar.gz", hash = "sha256:33e147d0b747d66303018add681f659c79e60d693806255b7f811136e4bc21bf", size = 160210, upload-time = "2025-12-07T12:44:48.35Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/a6/589d7a9f37dbc1c2e10aa7993159e467347de7065984ad4059779a1be375/adcp-2.13.0-py3-none-any.whl", hash = "sha256:617a394e189a09f54cfb6ae5b431937f2a7c90a44fa2e8b63012cda334c0bc85", size = 193254, upload-time = "2025-12-07T12:44:46.886Z" }, + { 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" }, ] [[package]] @@ -73,7 +73,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "adcp", specifier = ">=2.13.0" }, + { name = "adcp", specifier = ">=2.18.0" }, { name = "bleach", specifier = ">=6.3.0" }, { name = "boto3", specifier = ">=1.35.0" }, { name = "fastapi", specifier = ">=0.100.0" }, @@ -888,7 +888,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.16.0" +version = "1.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -897,15 +897,18 @@ dependencies = [ { name = "jsonschema" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/a1/b1f328da3b153683d2ec34f849b4b6eac2790fb240e3aef06ff2fab3df9d/mcp-1.16.0.tar.gz", hash = "sha256:39b8ca25460c578ee2cdad33feeea122694cfdf73eef58bee76c42f6ef0589df", size = 472918, upload-time = "2025-10-02T16:58:20.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/0e/7cebc88e17daf94ebe28c95633af595ccb2864dc2ee7abd75542d98495cc/mcp-1.16.0-py3-none-any.whl", hash = "sha256:ec917be9a5d31b09ba331e1768aa576e0af45470d657a0319996a20a57d7d633", size = 167266, upload-time = "2025-10-02T16:58:19.039Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" }, ] [[package]] @@ -1311,6 +1314,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pyperclip" version = "1.11.0" From 05a200e0752df5fa1d4deaab7a4b08bb484e4faa Mon Sep 17 00:00:00 2001 From: BaiyuScope3 Date: Fri, 9 Jan 2026 12:08:46 -0500 Subject: [PATCH 4/5] style: apply ruff format --- src/creative_agent/data/standard_formats.py | 4 +--- tests/unit/test_info_card_formats.py | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/creative_agent/data/standard_formats.py b/src/creative_agent/data/standard_formats.py index c6e2a1f..476a638 100644 --- a/src/creative_agent/data/standard_formats.py +++ b/src/creative_agent/data/standard_formats.py @@ -1471,9 +1471,7 @@ def _backfill_deprecated_assets_required() -> list[CreativeFormat]: # Use adcp utility to get required assets, then convert to AssetsRequired type required_assets = get_required_assets(fmt) fmt_dict = fmt.model_dump() - fmt_dict["assets_required"] = [ - LibAssetsRequired.model_validate(a.model_dump()) for a in required_assets - ] + fmt_dict["assets_required"] = [LibAssetsRequired.model_validate(a.model_dump()) for a in required_assets] rebuilt.append(CreativeFormat.model_validate(fmt_dict)) diff --git a/tests/unit/test_info_card_formats.py b/tests/unit/test_info_card_formats.py index 3482acf..d5ef53c 100644 --- a/tests/unit/test_info_card_formats.py +++ b/tests/unit/test_info_card_formats.py @@ -116,9 +116,7 @@ def test_requires_product_assets(self): # Check optional assets are in assets but not in assets_required optional_asset_ids = { - get_asset_attr(asset, "asset_id") - for asset in fmt.assets - if not get_asset_attr(asset, "required") + get_asset_attr(asset, "asset_id") for asset in fmt.assets if not get_asset_attr(asset, "required") } assert "pricing_model" in optional_asset_ids assert "impression_tracker" in optional_asset_ids @@ -172,9 +170,7 @@ def test_requires_product_assets(self): # Check optional assets are in assets but not in assets_required optional_asset_ids = { - get_asset_attr(asset, "asset_id") - for asset in fmt.assets - if not get_asset_attr(asset, "required") + get_asset_attr(asset, "asset_id") for asset in fmt.assets if not get_asset_attr(asset, "required") } assert "pricing_model" in optional_asset_ids assert "impression_tracker" in optional_asset_ids From 7843b333bb4367d7776237c31099fd645001dde5 Mon Sep 17 00:00:00 2001 From: BaiyuScope3 Date: Fri, 9 Jan 2026 13:54:01 -0500 Subject: [PATCH 5/5] refactor: use adcp utilities for asset validation - Replace manual assets_required iteration with get_format_assets() and get_required_assets() - Utilities handle both new 'assets' and deprecated 'assets_required' fields automatically --- src/creative_agent/validation.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/creative_agent/validation.py b/src/creative_agent/validation.py index cf62cbf..a12405b 100644 --- a/src/creative_agent/validation.py +++ b/src/creative_agent/validation.py @@ -374,18 +374,15 @@ def validate_manifest_assets( return ["Manifest assets must be a dictionary"] # Build a map of asset_id -> asset_type from format if provided + # Uses adcp utilities which handle both new `assets` and deprecated `assets_required` fields asset_type_map = {} - if format_obj and hasattr(format_obj, "assets_required") and format_obj.assets_required: - for required_asset in format_obj.assets_required: - # Handle both dict and object formats for required_asset - if isinstance(required_asset, dict): - asset_id = required_asset.get("asset_id") - asset_type = required_asset.get("asset_type") - is_required = required_asset.get("required", True) - else: - asset_id = getattr(required_asset, "asset_id", None) - asset_type = getattr(required_asset, "asset_type", None) - is_required = getattr(required_asset, "required", True) + if format_obj: + from adcp import get_format_assets, get_required_assets + + # Build asset type map from all format assets + for asset in get_format_assets(format_obj): + 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 @@ -394,8 +391,10 @@ def validate_manifest_assets( else: asset_type_map[asset_id] = str(asset_type) - # Check if this is a required (non-optional) asset - if is_required and asset_id and asset_id not in assets: + # Check required assets are present in manifest + for required_asset in get_required_assets(format_obj): + asset_id = getattr(required_asset, "asset_id", None) + if asset_id and asset_id not in assets: errors.append(f"Required asset missing: {asset_id}") # Validate each asset