From be23b243ec45e86a0e52e9194f3ded7f85db79f5 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Mon, 20 Oct 2025 07:00:27 -0400 Subject: [PATCH 1/3] Update to latest AdCP spec: consistent build_creative and preview_creative MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated schemas from adcontextprotocol.org - Removed deprecated schemas (promoted-offerings-asset, creative-library-item, snippet-type, asset-types index) - Added build-creative schemas from /media-buy/ endpoint - Regenerated Pydantic models Key API changes: - preview_creative: Removed promoted_offerings, brand_card, asset_filters parameters - These should now be included inside creative_manifest.assets - build_creative: Complete rewrite to match new spec - Now takes target_format_id and creative_manifest (with input assets) - Returns creative_manifest in structured_content - Simpler, more consistent API aligned with preview_creative The new spec makes both tools consistent: they both accept creative_manifest as input, making the API more intuitive and easier to use. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/creative_agent/schemas/manifest.py | 5 +- ..._schemas_v1_core_creative_manifest_json.py | 40 +- ..._creative_preview_creative_request_json.py | 46 +- ...1_media_buy_build_creative_request_json.py | 865 +++++++++++++++++ ..._media_buy_build_creative_response_json.py | 883 ++++++++++++++++++ src/creative_agent/server.py | 566 +++-------- ..._assets_promoted-offerings-asset_json.json | 48 - ...as_v1_core_creative-library-item_json.json | 166 ---- ...reative_preview-creative-request_json.json | 8 +- .../_schemas_v1_enums_snippet-type_json.json | 96 -- ...media-buy_build-creative-request_json.json | 25 + ...edia-buy_build-creative-response_json.json | 24 + ...andard-formats_asset-types_index_json.json | 72 -- 13 files changed, 1975 insertions(+), 869 deletions(-) create mode 100644 src/creative_agent/schemas_generated/_schemas_v1_media_buy_build_creative_request_json.py create mode 100644 src/creative_agent/schemas_generated/_schemas_v1_media_buy_build_creative_response_json.py delete mode 100644 tests/schemas/v1/_schemas_v1_core_assets_promoted-offerings-asset_json.json delete mode 100644 tests/schemas/v1/_schemas_v1_core_creative-library-item_json.json delete mode 100644 tests/schemas/v1/_schemas_v1_enums_snippet-type_json.json create mode 100644 tests/schemas/v1/_schemas_v1_media-buy_build-creative-request_json.json create mode 100644 tests/schemas/v1/_schemas_v1_media-buy_build-creative-response_json.json delete mode 100644 tests/schemas/v1/_schemas_v1_standard-formats_asset-types_index_json.json diff --git a/src/creative_agent/schemas/manifest.py b/src/creative_agent/schemas/manifest.py index 704c4ad..b4287db 100644 --- a/src/creative_agent/schemas/manifest.py +++ b/src/creative_agent/schemas/manifest.py @@ -22,12 +22,9 @@ class PreviewCreativeRequest(BaseModel): """Request for preview_creative task.""" format_id: FormatId - creative_manifest: dict[str, Any] # AdCP CreativeAsset structure + creative_manifest: dict[str, Any] # AdCP CreativeAsset structure (including promoted_offerings if required) inputs: list[PreviewInput] | None = None template_id: str | None = None - brand_card: dict[str, Any] | None = None - promoted_products: dict[str, Any] | None = None - asset_filters: dict[str, Any] | None = None class PreviewHints(BaseModel): diff --git a/src/creative_agent/schemas_generated/_schemas_v1_core_creative_manifest_json.py b/src/creative_agent/schemas_generated/_schemas_v1_core_creative_manifest_json.py index 6def50a..d3414f7 100644 --- a/src/creative_agent/schemas_generated/_schemas_v1_core_creative_manifest_json.py +++ b/src/creative_agent/schemas_generated/_schemas_v1_core_creative_manifest_json.py @@ -48,7 +48,7 @@ class Assets(BaseModel): ] = None -class Assets16(BaseModel): +class Assets14(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -70,7 +70,7 @@ class Assets16(BaseModel): ] = None -class Assets17(BaseModel): +class Assets15(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -113,7 +113,7 @@ class TrackingEvent(Enum): player_collapse = "playerCollapse" -class Assets18(BaseModel): +class Assets16(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -140,7 +140,7 @@ class Assets18(BaseModel): ] = None -class Assets19(BaseModel): +class Assets17(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -167,7 +167,7 @@ class Assets19(BaseModel): ] = None -class Assets20(BaseModel): +class Assets18(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -177,7 +177,7 @@ class Assets20(BaseModel): ] = None -class Assets21(BaseModel): +class Assets19(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -187,7 +187,7 @@ class Assets21(BaseModel): ] = None -class Assets22(BaseModel): +class Assets20(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -203,7 +203,7 @@ class ModuleType(Enum): script = "script" -class Assets23(BaseModel): +class Assets21(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -242,7 +242,7 @@ class Security(BaseModel): ] = None -class Assets24(BaseModel): +class Assets22(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -276,7 +276,7 @@ class Assets24(BaseModel): ] -class Assets25(BaseModel): +class Assets23(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -306,7 +306,7 @@ class TrackingEvent10(Enum): unmute = "unmute" -class Assets26(BaseModel): +class Assets24(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -330,7 +330,7 @@ class Assets26(BaseModel): ] = None -class Assets27(BaseModel): +class Assets25(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -735,7 +735,7 @@ class AssetSelectors(BaseModel): ] = None -class Assets28(BaseModel): +class Assets26(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -824,17 +824,17 @@ class CreativeManifest(BaseModel): str, Union[ Assets, - Assets16, - Assets17, - Union[Assets18, Assets19], + Assets14, + Assets15, + Union[Assets16, Assets17], + Assets18, + Assets19, Assets20, Assets21, Assets22, Assets23, - Assets24, - Assets25, - Union[Assets26, Assets27], - Assets28, + Union[Assets24, Assets25], + Assets26, ], ], Field( diff --git a/src/creative_agent/schemas_generated/_schemas_v1_creative_preview_creative_request_json.py b/src/creative_agent/schemas_generated/_schemas_v1_creative_preview_creative_request_json.py index 15ad5b3..e4e187d 100644 --- a/src/creative_agent/schemas_generated/_schemas_v1_creative_preview_creative_request_json.py +++ b/src/creative_agent/schemas_generated/_schemas_v1_creative_preview_creative_request_json.py @@ -48,7 +48,7 @@ class Assets(BaseModel): ] = None -class Assets30(BaseModel): +class Assets28(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -70,7 +70,7 @@ class Assets30(BaseModel): ] = None -class Assets31(BaseModel): +class Assets29(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -113,7 +113,7 @@ class TrackingEvent(Enum): player_collapse = "playerCollapse" -class Assets32(BaseModel): +class Assets30(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -140,7 +140,7 @@ class Assets32(BaseModel): ] = None -class Assets33(BaseModel): +class Assets31(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -167,7 +167,7 @@ class Assets33(BaseModel): ] = None -class Assets34(BaseModel): +class Assets32(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -177,7 +177,7 @@ class Assets34(BaseModel): ] = None -class Assets35(BaseModel): +class Assets33(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -187,7 +187,7 @@ class Assets35(BaseModel): ] = None -class Assets36(BaseModel): +class Assets34(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -203,7 +203,7 @@ class ModuleType(Enum): script = "script" -class Assets37(BaseModel): +class Assets35(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -242,7 +242,7 @@ class Security(BaseModel): ] = None -class Assets38(BaseModel): +class Assets36(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -276,7 +276,7 @@ class Assets38(BaseModel): ] -class Assets39(BaseModel): +class Assets37(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -306,7 +306,7 @@ class TrackingEvent14(Enum): unmute = "unmute" -class Assets40(BaseModel): +class Assets38(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -330,7 +330,7 @@ class Assets40(BaseModel): ] = None -class Assets41(BaseModel): +class Assets39(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -735,7 +735,7 @@ class AssetSelectors(BaseModel): ] = None -class Assets42(BaseModel): +class Assets40(BaseModel): model_config = ConfigDict( extra="forbid", ) @@ -820,17 +820,17 @@ class CreativeManifest(BaseModel): str, Union[ Assets, - Assets30, - Assets31, - Union[Assets32, Assets33], + Assets28, + Assets29, + Union[Assets30, Assets31], + Assets32, + Assets33, Assets34, Assets35, Assets36, Assets37, - Assets38, - Assets39, - Union[Assets40, Assets41], - Assets42, + Union[Assets38, Assets39], + Assets40, ], ], Field( @@ -891,9 +891,3 @@ class PreviewCreativeRequest(BaseModel): Optional[str], Field(description="Specific template ID for custom format rendering"), ] = None - promoted_offerings: Annotated[ - Optional[Any], - Field( - description="Circular reference to /schemas/v1/core/promoted-offerings.json" - ), - ] = None diff --git a/src/creative_agent/schemas_generated/_schemas_v1_media_buy_build_creative_request_json.py b/src/creative_agent/schemas_generated/_schemas_v1_media_buy_build_creative_request_json.py new file mode 100644 index 0000000..57e2e08 --- /dev/null +++ b/src/creative_agent/schemas_generated/_schemas_v1_media_buy_build_creative_request_json.py @@ -0,0 +1,865 @@ +# generated by datamodel-codegen: +# filename: _schemas_v1_media-buy_build-creative-request_json.json + +from __future__ import annotations + +from enum import Enum +from typing import Annotated, Any, Optional, Union + +from pydantic import AnyUrl, AwareDatetime, BaseModel, ConfigDict, EmailStr, Field + + +class FormatId(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + agent_url: Annotated[ + AnyUrl, + Field( + description="URL of the agent that defines this format (e.g., 'https://creatives.adcontextprotocol.org' for standard formats, or 'https://publisher.com/.well-known/adcp/sales' for custom formats)" + ), + ] + id: Annotated[ + str, + Field( + description="Format identifier within the agent's namespace (e.g., 'display_300x250', 'video_standard_30s')", + pattern="^[a-zA-Z0-9_-]+$", + ), + ] + + +class Assets(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="URL to the image asset")] + width: Annotated[ + Optional[int], Field(description="Image width in pixels", ge=1) + ] = None + height: Annotated[ + Optional[int], Field(description="Image height in pixels", ge=1) + ] = None + format: Annotated[ + Optional[str], + Field(description="Image file format (jpg, png, gif, webp, etc.)"), + ] = None + alt_text: Annotated[ + Optional[str], Field(description="Alternative text for accessibility") + ] = None + + +class Assets42(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="URL to the video asset")] + width: Annotated[ + Optional[int], Field(description="Video width in pixels", ge=1) + ] = None + height: Annotated[ + Optional[int], Field(description="Video height in pixels", ge=1) + ] = None + duration_ms: Annotated[ + Optional[int], Field(description="Video duration in milliseconds", ge=0) + ] = None + format: Annotated[ + Optional[str], Field(description="Video file format (mp4, webm, mov, etc.)") + ] = None + bitrate_kbps: Annotated[ + Optional[int], Field(description="Video bitrate in kilobits per second", ge=1) + ] = None + + +class Assets43(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="URL to the audio asset")] + duration_ms: Annotated[ + Optional[int], Field(description="Audio duration in milliseconds", ge=0) + ] = None + format: Annotated[ + Optional[str], Field(description="Audio file format (mp3, wav, aac, etc.)") + ] = None + bitrate_kbps: Annotated[ + Optional[int], Field(description="Audio bitrate in kilobits per second", ge=1) + ] = None + + +class VastVersion(Enum): + field_2_0 = "2.0" + field_3_0 = "3.0" + field_4_0 = "4.0" + field_4_1 = "4.1" + field_4_2 = "4.2" + + +class TrackingEvent(Enum): + start = "start" + first_quartile = "firstQuartile" + midpoint = "midpoint" + third_quartile = "thirdQuartile" + complete = "complete" + impression = "impression" + click = "click" + pause = "pause" + resume = "resume" + skip = "skip" + mute = "mute" + unmute = "unmute" + fullscreen = "fullscreen" + exit_fullscreen = "exitFullscreen" + player_expand = "playerExpand" + player_collapse = "playerCollapse" + + +class Assets44(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="URL endpoint that returns VAST XML")] + content: Annotated[Optional[str], Field(description="Inline VAST XML content")] = ( + None + ) + vast_version: Annotated[ + Optional[VastVersion], Field(description="VAST specification version") + ] = None + vpaid_enabled: Annotated[ + Optional[bool], + Field( + description="Whether VPAID (Video Player-Ad Interface Definition) is supported" + ), + ] = None + duration_ms: Annotated[ + Optional[int], + Field(description="Expected video duration in milliseconds (if known)", ge=0), + ] = None + tracking_events: Annotated[ + Optional[list[TrackingEvent]], + Field(description="Tracking events supported by this VAST tag"), + ] = None + + +class Assets45(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[ + Optional[AnyUrl], Field(description="URL endpoint that returns VAST XML") + ] = None + content: Annotated[str, Field(description="Inline VAST XML content")] + vast_version: Annotated[ + Optional[VastVersion], Field(description="VAST specification version") + ] = None + vpaid_enabled: Annotated[ + Optional[bool], + Field( + description="Whether VPAID (Video Player-Ad Interface Definition) is supported" + ), + ] = None + duration_ms: Annotated[ + Optional[int], + Field(description="Expected video duration in milliseconds (if known)", ge=0), + ] = None + tracking_events: Annotated[ + Optional[list[TrackingEvent]], + Field(description="Tracking events supported by this VAST tag"), + ] = None + + +class Assets46(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + content: Annotated[str, Field(description="Text content")] + language: Annotated[ + Optional[str], Field(description="Language code (e.g., 'en', 'es', 'fr')") + ] = None + + +class Assets47(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="URL reference")] + description: Annotated[ + Optional[str], Field(description="Description of what this URL points to") + ] = None + + +class Assets48(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + content: Annotated[str, Field(description="HTML content")] + version: Annotated[ + Optional[str], Field(description="HTML version (e.g., 'HTML5')") + ] = None + + +class ModuleType(Enum): + esm = "esm" + commonjs = "commonjs" + script = "script" + + +class Assets49(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + content: Annotated[str, Field(description="JavaScript content")] + module_type: Annotated[ + Optional[ModuleType], Field(description="JavaScript module type") + ] = None + + +class Method(Enum): + get = "GET" + post = "POST" + + +class ResponseType(Enum): + html = "html" + json = "json" + xml = "xml" + javascript = "javascript" + + +class Method7(Enum): + hmac_sha256 = "hmac_sha256" + api_key = "api_key" + none = "none" + + +class Security(BaseModel): + method: Annotated[Method7, Field(description="Authentication method")] + hmac_header: Annotated[ + Optional[str], + Field(description="Header name for HMAC signature (e.g., 'X-Signature')"), + ] = None + api_key_header: Annotated[ + Optional[str], Field(description="Header name for API key (e.g., 'X-API-Key')") + ] = None + + +class Assets50(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="Webhook URL to call for dynamic content")] + method: Annotated[Optional[Method], Field(description="HTTP method")] = "POST" + timeout_ms: Annotated[ + Optional[int], + Field( + description="Maximum time to wait for response in milliseconds", + ge=10, + le=5000, + ), + ] = 500 + supported_macros: Annotated[ + Optional[list[str]], + Field( + description="Universal macros that can be passed to webhook (e.g., {DEVICE_TYPE}, {COUNTRY})" + ), + ] = None + required_macros: Annotated[ + Optional[list[str]], + Field( + description="Universal macros that must be provided for webhook to function" + ), + ] = None + response_type: Annotated[ + ResponseType, Field(description="Expected content type of webhook response") + ] + security: Annotated[ + Security, Field(description="Security configuration for webhook calls") + ] + + +class Assets51(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + content: Annotated[str, Field(description="CSS content")] + media: Annotated[ + Optional[str], + Field(description="CSS media query context (e.g., 'screen', 'print')"), + ] = None + + +class DaastVersion(Enum): + field_1_0 = "1.0" + field_1_1 = "1.1" + + +class TrackingEvent18(Enum): + start = "start" + first_quartile = "firstQuartile" + midpoint = "midpoint" + third_quartile = "thirdQuartile" + complete = "complete" + impression = "impression" + pause = "pause" + resume = "resume" + skip = "skip" + mute = "mute" + unmute = "unmute" + + +class Assets52(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="URL endpoint that returns DAAST XML")] + content: Annotated[Optional[str], Field(description="Inline DAAST XML content")] = ( + None + ) + daast_version: Annotated[ + Optional[DaastVersion], Field(description="DAAST specification version") + ] = None + duration_ms: Annotated[ + Optional[int], + Field(description="Expected audio duration in milliseconds (if known)", ge=0), + ] = None + tracking_events: Annotated[ + Optional[list[TrackingEvent18]], + Field(description="Tracking events supported by this DAAST tag"), + ] = None + companion_ads: Annotated[ + Optional[bool], Field(description="Whether companion display ads are included") + ] = None + + +class Assets53(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[ + Optional[AnyUrl], Field(description="URL endpoint that returns DAAST XML") + ] = None + content: Annotated[str, Field(description="Inline DAAST XML content")] + daast_version: Annotated[ + Optional[DaastVersion], Field(description="DAAST specification version") + ] = None + duration_ms: Annotated[ + Optional[int], + Field(description="Expected audio duration in milliseconds (if known)", ge=0), + ] = None + tracking_events: Annotated[ + Optional[list[TrackingEvent18]], + Field(description="Tracking events supported by this DAAST tag"), + ] = None + companion_ads: Annotated[ + Optional[bool], Field(description="Whether companion display ads are included") + ] = None + + +class Logo(BaseModel): + url: Annotated[AnyUrl, Field(description="URL to the logo asset")] + tags: Annotated[ + Optional[list[str]], + Field( + description="Semantic tags describing the logo variant (e.g., 'dark', 'light', 'square', 'horizontal', 'icon')" + ), + ] = None + width: Annotated[Optional[int], Field(description="Logo width in pixels")] = None + height: Annotated[Optional[int], Field(description="Logo height in pixels")] = None + + +class Colors(BaseModel): + primary: Annotated[ + Optional[str], + Field( + description="Primary brand color (hex format)", pattern="^#[0-9A-Fa-f]{6}$" + ), + ] = None + secondary: Annotated[ + Optional[str], + Field( + description="Secondary brand color (hex format)", + pattern="^#[0-9A-Fa-f]{6}$", + ), + ] = None + accent: Annotated[ + Optional[str], + Field(description="Accent color (hex format)", pattern="^#[0-9A-Fa-f]{6}$"), + ] = None + background: Annotated[ + Optional[str], + Field(description="Background color (hex format)", pattern="^#[0-9A-Fa-f]{6}$"), + ] = None + text: Annotated[ + Optional[str], + Field(description="Text color (hex format)", pattern="^#[0-9A-Fa-f]{6}$"), + ] = None + + +class Fonts(BaseModel): + primary: Annotated[Optional[str], Field(description="Primary font family name")] = ( + None + ) + secondary: Annotated[ + Optional[str], Field(description="Secondary font family name") + ] = None + font_urls: Annotated[ + Optional[list[AnyUrl]], + Field(description="URLs to web font files if using custom fonts"), + ] = None + + +class AssetType(Enum): + image = "image" + video = "video" + audio = "audio" + text = "text" + + +class Asset(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + asset_id: Annotated[str, Field(description="Unique identifier for this asset")] + asset_type: Annotated[AssetType, Field(description="Type of asset")] + url: Annotated[AnyUrl, Field(description="URL to CDN-hosted asset file")] + tags: Annotated[ + Optional[list[str]], + Field( + description="Tags for asset discovery (e.g., 'holiday', 'lifestyle', 'product_shot')" + ), + ] = None + name: Annotated[Optional[str], Field(description="Human-readable asset name")] = ( + None + ) + description: Annotated[ + Optional[str], Field(description="Asset description or usage notes") + ] = None + width: Annotated[ + Optional[int], Field(description="Image/video width in pixels") + ] = None + height: Annotated[ + Optional[int], Field(description="Image/video height in pixels") + ] = None + duration_seconds: Annotated[ + Optional[float], Field(description="Video/audio duration in seconds") + ] = None + file_size_bytes: Annotated[ + Optional[int], Field(description="File size in bytes") + ] = None + format: Annotated[ + Optional[str], Field(description="File format (e.g., 'jpg', 'mp4', 'mp3')") + ] = None + metadata: Annotated[ + Optional[dict[str, Any]], + Field(description="Additional asset-specific metadata"), + ] = None + + +class FeedFormat(Enum): + google_merchant_center = "google_merchant_center" + facebook_catalog = "facebook_catalog" + custom = "custom" + + +class UpdateFrequency(Enum): + realtime = "realtime" + hourly = "hourly" + daily = "daily" + weekly = "weekly" + + +class ProductCatalog(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + feed_url: Annotated[AnyUrl, Field(description="URL to product catalog feed")] + feed_format: Annotated[ + Optional[FeedFormat], Field(description="Format of the product feed") + ] = "google_merchant_center" + categories: Annotated[ + Optional[list[str]], + Field( + description="Product categories available in the catalog (for filtering)" + ), + ] = None + last_updated: Annotated[ + Optional[AwareDatetime], + Field(description="When the product catalog was last updated"), + ] = None + update_frequency: Annotated[ + Optional[UpdateFrequency], + Field(description="How frequently the product catalog is updated"), + ] = None + + +class Disclaimer(BaseModel): + text: Annotated[str, Field(description="Disclaimer text")] + context: Annotated[ + Optional[str], + Field( + description="When this disclaimer applies (e.g., 'financial_products', 'health_claims', 'all')" + ), + ] = None + required: Annotated[ + Optional[bool], Field(description="Whether this disclaimer must appear") + ] = True + + +class Contact(BaseModel): + email: Annotated[Optional[EmailStr], Field(description="Contact email")] = None + phone: Annotated[Optional[str], Field(description="Contact phone number")] = None + + +class Metadata(BaseModel): + created_date: Annotated[ + Optional[AwareDatetime], + Field(description="When this brand manifest was created"), + ] = None + updated_date: Annotated[ + Optional[AwareDatetime], + Field(description="When this brand manifest was last updated"), + ] = None + version: Annotated[ + Optional[str], Field(description="Brand card version number") + ] = None + + +class BrandManifest(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[ + AnyUrl, + Field( + description="Primary brand URL for context and asset discovery. Creative agents can infer brand information from this URL." + ), + ] + name: Annotated[Optional[str], Field(description="Brand or business name")] = None + logos: Annotated[ + Optional[list[Logo]], + Field( + description="Brand logo assets with semantic tags for different use cases" + ), + ] = None + colors: Annotated[Optional[Colors], Field(description="Brand color palette")] = None + fonts: Annotated[ + Optional[Fonts], Field(description="Brand typography guidelines") + ] = None + tone: Annotated[ + Optional[str], + Field( + description="Brand voice and messaging tone (e.g., 'professional', 'casual', 'humorous', 'trustworthy', 'innovative')" + ), + ] = None + tagline: Annotated[Optional[str], Field(description="Brand tagline or slogan")] = ( + None + ) + assets: Annotated[ + Optional[list[Asset]], + Field( + description="Brand asset library with explicit assets and tags. Assets are referenced inline with URLs pointing to CDN-hosted files." + ), + ] = None + product_catalog: Annotated[ + Optional[ProductCatalog], + Field( + description="Product catalog information for e-commerce advertisers. Enables SKU-level creative generation and product selection." + ), + ] = None + disclaimers: Annotated[ + Optional[list[Disclaimer]], + Field( + description="Legal disclaimers or required text that must appear in creatives" + ), + ] = None + industry: Annotated[ + Optional[str], + Field( + description="Industry or vertical (e.g., 'retail', 'automotive', 'finance', 'healthcare')" + ), + ] = None + target_audience: Annotated[ + Optional[str], Field(description="Primary target audience description") + ] = None + contact: Annotated[ + Optional[Contact], Field(description="Brand contact information") + ] = None + metadata: Annotated[ + Optional[Metadata], Field(description="Additional brand metadata") + ] = None + + +Asset15 = Asset + + +ProductCatalog13 = ProductCatalog + + +class BrandManifest12(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[ + Optional[AnyUrl], + Field( + description="Primary brand URL for context and asset discovery. Creative agents can infer brand information from this URL." + ), + ] = None + name: Annotated[str, Field(description="Brand or business name")] + logos: Annotated[ + Optional[list[Logo]], + Field( + description="Brand logo assets with semantic tags for different use cases" + ), + ] = None + colors: Annotated[Optional[Colors], Field(description="Brand color palette")] = None + fonts: Annotated[ + Optional[Fonts], Field(description="Brand typography guidelines") + ] = None + tone: Annotated[ + Optional[str], + Field( + description="Brand voice and messaging tone (e.g., 'professional', 'casual', 'humorous', 'trustworthy', 'innovative')" + ), + ] = None + tagline: Annotated[Optional[str], Field(description="Brand tagline or slogan")] = ( + None + ) + assets: Annotated[ + Optional[list[Asset15]], + Field( + description="Brand asset library with explicit assets and tags. Assets are referenced inline with URLs pointing to CDN-hosted files." + ), + ] = None + product_catalog: Annotated[ + Optional[ProductCatalog13], + Field( + description="Product catalog information for e-commerce advertisers. Enables SKU-level creative generation and product selection." + ), + ] = None + disclaimers: Annotated[ + Optional[list[Disclaimer]], + Field( + description="Legal disclaimers or required text that must appear in creatives" + ), + ] = None + industry: Annotated[ + Optional[str], + Field( + description="Industry or vertical (e.g., 'retail', 'automotive', 'finance', 'healthcare')" + ), + ] = None + target_audience: Annotated[ + Optional[str], Field(description="Primary target audience description") + ] = None + contact: Annotated[ + Optional[Contact], Field(description="Brand contact information") + ] = None + metadata: Annotated[ + Optional[Metadata], Field(description="Additional brand metadata") + ] = None + + +class ProductSelectors(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + manifest_skus: Annotated[ + Optional[list[str]], + Field( + description="Direct product SKU references from the brand manifest product catalog" + ), + ] = None + manifest_tags: Annotated[ + Optional[list[str]], + Field( + description="Select products by tags from the brand manifest product catalog (e.g., 'organic', 'sauces', 'holiday')" + ), + ] = None + manifest_category: Annotated[ + Optional[str], + Field( + description="Select products from a specific category in the brand manifest product catalog (e.g., 'beverages/soft-drinks', 'food/sauces')" + ), + ] = None + manifest_query: Annotated[ + Optional[str], + Field( + description="Natural language query to select products from the brand manifest (e.g., 'all Kraft Heinz pasta sauces', 'organic products under $20')" + ), + ] = None + + +class Offering(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + name: Annotated[ + str, + Field(description="Offering name (e.g., 'Winter Sale', 'New Product Launch')"), + ] + description: Annotated[ + Optional[str], Field(description="Description of what's being offered") + ] = None + assets: Annotated[ + Optional[list[dict[str, Any]]], + Field(description="Assets specific to this offering"), + ] = None + + +class AssetType23(Enum): + image = "image" + video = "video" + audio = "audio" + text = "text" + html = "html" + css = "css" + javascript = "javascript" + + +class AssetSelectors(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + tags: Annotated[ + Optional[list[str]], + Field( + description="Select assets with specific tags (e.g., ['holiday', 'premium'])" + ), + ] = None + asset_types: Annotated[ + Optional[list[AssetType23]], + Field(description="Filter by asset type (e.g., ['image', 'video'])"), + ] = None + exclude_tags: Annotated[ + Optional[list[str]], Field(description="Exclude assets with these tags") + ] = None + + +class Assets54(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + brand_manifest: Annotated[ + Union[Union[BrandManifest, BrandManifest12], AnyUrl], + Field( + description="Brand manifest provided either as an inline object or a URL string pointing to a hosted manifest", + examples=[ + { + "description": "Inline brand manifest", + "data": { + "url": "https://acmecorp.com", + "name": "ACME Corporation", + "colors": {"primary": "#FF6B35"}, + }, + }, + { + "description": "URL string reference to hosted manifest", + "data": "https://cdn.acmecorp.com/brand-manifest.json", + }, + ], + title="Brand Manifest Reference", + ), + ] + product_selectors: Annotated[ + Optional[ProductSelectors], + Field( + description="Specification of products or offerings being promoted in a campaign. Supports multiple selection methods from the brand manifest that can be combined using UNION (OR) logic. When multiple selection methods are provided, products matching ANY of the criteria are selected (logical OR, not AND).", + examples=[ + { + "description": "Direct SKU selection for specific products from brand manifest", + "data": {"manifest_skus": ["SKU-12345", "SKU-67890"]}, + }, + { + "description": "UNION selection: products tagged 'organic' OR 'sauces' OR in 'food/condiments' category from brand manifest", + "data": { + "manifest_tags": ["organic", "sauces"], + "manifest_category": "food/condiments", + }, + }, + { + "description": "Natural language product selection from brand manifest", + "data": {"manifest_query": "all Kraft Heinz pasta sauces under $5"}, + }, + { + "description": "Select products by tags", + "data": {"manifest_tags": ["holiday"]}, + }, + ], + title="Promoted Products", + ), + ] = None + offerings: Annotated[ + Optional[list[Offering]], + Field( + description="Inline offerings for campaigns without a product catalog. Each offering has a name, description, and associated assets." + ), + ] = None + asset_selectors: Annotated[ + Optional[AssetSelectors], + Field( + description="Selectors to choose specific assets from the brand manifest" + ), + ] = None + + +class CreativeManifest(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + format_id: Annotated[ + FormatId, + Field( + description="Structured format identifier with agent URL and format name", + title="Format ID", + ), + ] + promoted_offering: Annotated[ + Optional[str], + Field( + description="Product name or offering being advertised. Maps to promoted_offerings in create_media_buy request to associate creative with the product being promoted." + ), + ] = None + assets: Annotated[ + dict[ + str, + Union[ + Assets, + Assets42, + Assets43, + Union[Assets44, Assets45], + Assets46, + Assets47, + Assets48, + Assets49, + Assets50, + Assets51, + Union[Assets52, Assets53], + Assets54, + ], + ], + Field( + description="Map of asset IDs to actual asset content. Each key MUST match an asset_id from the format's assets_required array (e.g., 'banner_image', 'clickthrough_url', 'video_file', 'vast_tag'). The asset_id is the technical identifier used to match assets to format requirements.\n\nIMPORTANT: Creative manifest validation MUST be performed in the context of the format specification. The format defines what type each asset_id should be, which eliminates any validation ambiguity." + ), + ] + + +class BuildCreativeRequest(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + message: Annotated[ + Optional[str], + Field( + description="Natural language instructions for the transformation or generation. For pure generation, this is the creative brief. For transformation, this provides guidance on how to adapt the creative." + ), + ] = None + creative_manifest: Annotated[ + Optional[CreativeManifest], + Field( + description="Complete specification of a creative with all assets needed for rendering in a specific format. Each asset is typed according to its asset_role from the format specification and contains the actual content/URL that fulfills the format requirements.", + title="Creative Manifest", + ), + ] = None + target_format_id: Annotated[ + Any, Field(description="Circular reference to /schemas/v1/core/format-id.json") + ] diff --git a/src/creative_agent/schemas_generated/_schemas_v1_media_buy_build_creative_response_json.py b/src/creative_agent/schemas_generated/_schemas_v1_media_buy_build_creative_response_json.py new file mode 100644 index 0000000..ff6bb12 --- /dev/null +++ b/src/creative_agent/schemas_generated/_schemas_v1_media_buy_build_creative_response_json.py @@ -0,0 +1,883 @@ +# generated by datamodel-codegen: +# filename: _schemas_v1_media-buy_build-creative-response_json.json + +from __future__ import annotations + +from enum import Enum +from typing import Annotated, Any, Optional, Union + +from pydantic import AnyUrl, AwareDatetime, BaseModel, ConfigDict, EmailStr, Field + + +class FormatId(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + agent_url: Annotated[ + AnyUrl, + Field( + description="URL of the agent that defines this format (e.g., 'https://creatives.adcontextprotocol.org' for standard formats, or 'https://publisher.com/.well-known/adcp/sales' for custom formats)" + ), + ] + id: Annotated[ + str, + Field( + description="Format identifier within the agent's namespace (e.g., 'display_300x250', 'video_standard_30s')", + pattern="^[a-zA-Z0-9_-]+$", + ), + ] + + +class Assets(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="URL to the image asset")] + width: Annotated[ + Optional[int], Field(description="Image width in pixels", ge=1) + ] = None + height: Annotated[ + Optional[int], Field(description="Image height in pixels", ge=1) + ] = None + format: Annotated[ + Optional[str], + Field(description="Image file format (jpg, png, gif, webp, etc.)"), + ] = None + alt_text: Annotated[ + Optional[str], Field(description="Alternative text for accessibility") + ] = None + + +class Assets56(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="URL to the video asset")] + width: Annotated[ + Optional[int], Field(description="Video width in pixels", ge=1) + ] = None + height: Annotated[ + Optional[int], Field(description="Video height in pixels", ge=1) + ] = None + duration_ms: Annotated[ + Optional[int], Field(description="Video duration in milliseconds", ge=0) + ] = None + format: Annotated[ + Optional[str], Field(description="Video file format (mp4, webm, mov, etc.)") + ] = None + bitrate_kbps: Annotated[ + Optional[int], Field(description="Video bitrate in kilobits per second", ge=1) + ] = None + + +class Assets57(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="URL to the audio asset")] + duration_ms: Annotated[ + Optional[int], Field(description="Audio duration in milliseconds", ge=0) + ] = None + format: Annotated[ + Optional[str], Field(description="Audio file format (mp3, wav, aac, etc.)") + ] = None + bitrate_kbps: Annotated[ + Optional[int], Field(description="Audio bitrate in kilobits per second", ge=1) + ] = None + + +class VastVersion(Enum): + field_2_0 = "2.0" + field_3_0 = "3.0" + field_4_0 = "4.0" + field_4_1 = "4.1" + field_4_2 = "4.2" + + +class TrackingEvent(Enum): + start = "start" + first_quartile = "firstQuartile" + midpoint = "midpoint" + third_quartile = "thirdQuartile" + complete = "complete" + impression = "impression" + click = "click" + pause = "pause" + resume = "resume" + skip = "skip" + mute = "mute" + unmute = "unmute" + fullscreen = "fullscreen" + exit_fullscreen = "exitFullscreen" + player_expand = "playerExpand" + player_collapse = "playerCollapse" + + +class Assets58(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="URL endpoint that returns VAST XML")] + content: Annotated[Optional[str], Field(description="Inline VAST XML content")] = ( + None + ) + vast_version: Annotated[ + Optional[VastVersion], Field(description="VAST specification version") + ] = None + vpaid_enabled: Annotated[ + Optional[bool], + Field( + description="Whether VPAID (Video Player-Ad Interface Definition) is supported" + ), + ] = None + duration_ms: Annotated[ + Optional[int], + Field(description="Expected video duration in milliseconds (if known)", ge=0), + ] = None + tracking_events: Annotated[ + Optional[list[TrackingEvent]], + Field(description="Tracking events supported by this VAST tag"), + ] = None + + +class Assets59(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[ + Optional[AnyUrl], Field(description="URL endpoint that returns VAST XML") + ] = None + content: Annotated[str, Field(description="Inline VAST XML content")] + vast_version: Annotated[ + Optional[VastVersion], Field(description="VAST specification version") + ] = None + vpaid_enabled: Annotated[ + Optional[bool], + Field( + description="Whether VPAID (Video Player-Ad Interface Definition) is supported" + ), + ] = None + duration_ms: Annotated[ + Optional[int], + Field(description="Expected video duration in milliseconds (if known)", ge=0), + ] = None + tracking_events: Annotated[ + Optional[list[TrackingEvent]], + Field(description="Tracking events supported by this VAST tag"), + ] = None + + +class Assets60(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + content: Annotated[str, Field(description="Text content")] + language: Annotated[ + Optional[str], Field(description="Language code (e.g., 'en', 'es', 'fr')") + ] = None + + +class Assets61(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="URL reference")] + description: Annotated[ + Optional[str], Field(description="Description of what this URL points to") + ] = None + + +class Assets62(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + content: Annotated[str, Field(description="HTML content")] + version: Annotated[ + Optional[str], Field(description="HTML version (e.g., 'HTML5')") + ] = None + + +class ModuleType(Enum): + esm = "esm" + commonjs = "commonjs" + script = "script" + + +class Assets63(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + content: Annotated[str, Field(description="JavaScript content")] + module_type: Annotated[ + Optional[ModuleType], Field(description="JavaScript module type") + ] = None + + +class Method(Enum): + get = "GET" + post = "POST" + + +class ResponseType(Enum): + html = "html" + json = "json" + xml = "xml" + javascript = "javascript" + + +class Method9(Enum): + hmac_sha256 = "hmac_sha256" + api_key = "api_key" + none = "none" + + +class Security(BaseModel): + method: Annotated[Method9, Field(description="Authentication method")] + hmac_header: Annotated[ + Optional[str], + Field(description="Header name for HMAC signature (e.g., 'X-Signature')"), + ] = None + api_key_header: Annotated[ + Optional[str], Field(description="Header name for API key (e.g., 'X-API-Key')") + ] = None + + +class Assets64(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="Webhook URL to call for dynamic content")] + method: Annotated[Optional[Method], Field(description="HTTP method")] = "POST" # type: ignore[assignment] + timeout_ms: Annotated[ + Optional[int], + Field( + description="Maximum time to wait for response in milliseconds", + ge=10, + le=5000, + ), + ] = 500 + supported_macros: Annotated[ + Optional[list[str]], + Field( + description="Universal macros that can be passed to webhook (e.g., {DEVICE_TYPE}, {COUNTRY})" + ), + ] = None + required_macros: Annotated[ + Optional[list[str]], + Field( + description="Universal macros that must be provided for webhook to function" + ), + ] = None + response_type: Annotated[ + ResponseType, Field(description="Expected content type of webhook response") + ] + security: Annotated[ + Security, Field(description="Security configuration for webhook calls") + ] + + +class Assets65(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + content: Annotated[str, Field(description="CSS content")] + media: Annotated[ + Optional[str], + Field(description="CSS media query context (e.g., 'screen', 'print')"), + ] = None + + +class DaastVersion(Enum): + field_1_0 = "1.0" + field_1_1 = "1.1" + + +class TrackingEvent22(Enum): + start = "start" + first_quartile = "firstQuartile" + midpoint = "midpoint" + third_quartile = "thirdQuartile" + complete = "complete" + impression = "impression" + pause = "pause" + resume = "resume" + skip = "skip" + mute = "mute" + unmute = "unmute" + + +class Assets66(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[AnyUrl, Field(description="URL endpoint that returns DAAST XML")] + content: Annotated[Optional[str], Field(description="Inline DAAST XML content")] = ( + None + ) + daast_version: Annotated[ + Optional[DaastVersion], Field(description="DAAST specification version") + ] = None + duration_ms: Annotated[ + Optional[int], + Field(description="Expected audio duration in milliseconds (if known)", ge=0), + ] = None + tracking_events: Annotated[ + Optional[list[TrackingEvent22]], + Field(description="Tracking events supported by this DAAST tag"), + ] = None + companion_ads: Annotated[ + Optional[bool], Field(description="Whether companion display ads are included") + ] = None + + +class Assets67(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[ + Optional[AnyUrl], Field(description="URL endpoint that returns DAAST XML") + ] = None + content: Annotated[str, Field(description="Inline DAAST XML content")] + daast_version: Annotated[ + Optional[DaastVersion], Field(description="DAAST specification version") + ] = None + duration_ms: Annotated[ + Optional[int], + Field(description="Expected audio duration in milliseconds (if known)", ge=0), + ] = None + tracking_events: Annotated[ + Optional[list[TrackingEvent22]], + Field(description="Tracking events supported by this DAAST tag"), + ] = None + companion_ads: Annotated[ + Optional[bool], Field(description="Whether companion display ads are included") + ] = None + + +class Logo(BaseModel): + url: Annotated[AnyUrl, Field(description="URL to the logo asset")] + tags: Annotated[ + Optional[list[str]], + Field( + description="Semantic tags describing the logo variant (e.g., 'dark', 'light', 'square', 'horizontal', 'icon')" + ), + ] = None + width: Annotated[Optional[int], Field(description="Logo width in pixels")] = None + height: Annotated[Optional[int], Field(description="Logo height in pixels")] = None + + +class Colors(BaseModel): + primary: Annotated[ + Optional[str], + Field( + description="Primary brand color (hex format)", pattern="^#[0-9A-Fa-f]{6}$" + ), + ] = None + secondary: Annotated[ + Optional[str], + Field( + description="Secondary brand color (hex format)", + pattern="^#[0-9A-Fa-f]{6}$", + ), + ] = None + accent: Annotated[ + Optional[str], + Field(description="Accent color (hex format)", pattern="^#[0-9A-Fa-f]{6}$"), + ] = None + background: Annotated[ + Optional[str], + Field(description="Background color (hex format)", pattern="^#[0-9A-Fa-f]{6}$"), + ] = None + text: Annotated[ + Optional[str], + Field(description="Text color (hex format)", pattern="^#[0-9A-Fa-f]{6}$"), + ] = None + + +class Fonts(BaseModel): + primary: Annotated[Optional[str], Field(description="Primary font family name")] = ( + None + ) + secondary: Annotated[ + Optional[str], Field(description="Secondary font family name") + ] = None + font_urls: Annotated[ + Optional[list[AnyUrl]], + Field(description="URLs to web font files if using custom fonts"), + ] = None + + +class AssetType(Enum): + image = "image" + video = "video" + audio = "audio" + text = "text" + + +class Asset(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + asset_id: Annotated[str, Field(description="Unique identifier for this asset")] + asset_type: Annotated[AssetType, Field(description="Type of asset")] + url: Annotated[AnyUrl, Field(description="URL to CDN-hosted asset file")] + tags: Annotated[ + Optional[list[str]], + Field( + description="Tags for asset discovery (e.g., 'holiday', 'lifestyle', 'product_shot')" + ), + ] = None + name: Annotated[Optional[str], Field(description="Human-readable asset name")] = ( + None + ) + description: Annotated[ + Optional[str], Field(description="Asset description or usage notes") + ] = None + width: Annotated[ + Optional[int], Field(description="Image/video width in pixels") + ] = None + height: Annotated[ + Optional[int], Field(description="Image/video height in pixels") + ] = None + duration_seconds: Annotated[ + Optional[float], Field(description="Video/audio duration in seconds") + ] = None + file_size_bytes: Annotated[ + Optional[int], Field(description="File size in bytes") + ] = None + format: Annotated[ + Optional[str], Field(description="File format (e.g., 'jpg', 'mp4', 'mp3')") + ] = None + metadata: Annotated[ + Optional[dict[str, Any]], + Field(description="Additional asset-specific metadata"), + ] = None + + +class FeedFormat(Enum): + google_merchant_center = "google_merchant_center" + facebook_catalog = "facebook_catalog" + custom = "custom" + + +class UpdateFrequency(Enum): + realtime = "realtime" + hourly = "hourly" + daily = "daily" + weekly = "weekly" + + +class ProductCatalog(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + feed_url: Annotated[AnyUrl, Field(description="URL to product catalog feed")] + feed_format: Annotated[ + Optional[FeedFormat], Field(description="Format of the product feed") + ] = "google_merchant_center" + categories: Annotated[ + Optional[list[str]], + Field( + description="Product categories available in the catalog (for filtering)" + ), + ] = None + last_updated: Annotated[ + Optional[AwareDatetime], + Field(description="When the product catalog was last updated"), + ] = None + update_frequency: Annotated[ + Optional[UpdateFrequency], + Field(description="How frequently the product catalog is updated"), + ] = None + + +class Disclaimer(BaseModel): + text: Annotated[str, Field(description="Disclaimer text")] + context: Annotated[ + Optional[str], + Field( + description="When this disclaimer applies (e.g., 'financial_products', 'health_claims', 'all')" + ), + ] = None + required: Annotated[ + Optional[bool], Field(description="Whether this disclaimer must appear") + ] = True + + +class Contact(BaseModel): + email: Annotated[Optional[EmailStr], Field(description="Contact email")] = None + phone: Annotated[Optional[str], Field(description="Contact phone number")] = None + + +class Metadata(BaseModel): + created_date: Annotated[ + Optional[AwareDatetime], + Field(description="When this brand manifest was created"), + ] = None + updated_date: Annotated[ + Optional[AwareDatetime], + Field(description="When this brand manifest was last updated"), + ] = None + version: Annotated[ + Optional[str], Field(description="Brand card version number") + ] = None + + +class BrandManifest(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[ + AnyUrl, + Field( + description="Primary brand URL for context and asset discovery. Creative agents can infer brand information from this URL." + ), + ] + name: Annotated[Optional[str], Field(description="Brand or business name")] = None + logos: Annotated[ + Optional[list[Logo]], + Field( + description="Brand logo assets with semantic tags for different use cases" + ), + ] = None + colors: Annotated[Optional[Colors], Field(description="Brand color palette")] = None + fonts: Annotated[ + Optional[Fonts], Field(description="Brand typography guidelines") + ] = None + tone: Annotated[ + Optional[str], + Field( + description="Brand voice and messaging tone (e.g., 'professional', 'casual', 'humorous', 'trustworthy', 'innovative')" + ), + ] = None + tagline: Annotated[Optional[str], Field(description="Brand tagline or slogan")] = ( + None + ) + assets: Annotated[ + Optional[list[Asset]], + Field( + description="Brand asset library with explicit assets and tags. Assets are referenced inline with URLs pointing to CDN-hosted files." + ), + ] = None + product_catalog: Annotated[ + Optional[ProductCatalog], + Field( + description="Product catalog information for e-commerce advertisers. Enables SKU-level creative generation and product selection." + ), + ] = None + disclaimers: Annotated[ + Optional[list[Disclaimer]], + Field( + description="Legal disclaimers or required text that must appear in creatives" + ), + ] = None + industry: Annotated[ + Optional[str], + Field( + description="Industry or vertical (e.g., 'retail', 'automotive', 'finance', 'healthcare')" + ), + ] = None + target_audience: Annotated[ + Optional[str], Field(description="Primary target audience description") + ] = None + contact: Annotated[ + Optional[Contact], Field(description="Brand contact information") + ] = None + metadata: Annotated[ + Optional[Metadata], Field(description="Additional brand metadata") + ] = None + + +Asset17 = Asset + + +ProductCatalog15 = ProductCatalog + + +class BrandManifest14(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + url: Annotated[ + Optional[AnyUrl], + Field( + description="Primary brand URL for context and asset discovery. Creative agents can infer brand information from this URL." + ), + ] = None + name: Annotated[str, Field(description="Brand or business name")] + logos: Annotated[ + Optional[list[Logo]], + Field( + description="Brand logo assets with semantic tags for different use cases" + ), + ] = None + colors: Annotated[Optional[Colors], Field(description="Brand color palette")] = None + fonts: Annotated[ + Optional[Fonts], Field(description="Brand typography guidelines") + ] = None + tone: Annotated[ + Optional[str], + Field( + description="Brand voice and messaging tone (e.g., 'professional', 'casual', 'humorous', 'trustworthy', 'innovative')" + ), + ] = None + tagline: Annotated[Optional[str], Field(description="Brand tagline or slogan")] = ( + None + ) + assets: Annotated[ + Optional[list[Asset17]], + Field( + description="Brand asset library with explicit assets and tags. Assets are referenced inline with URLs pointing to CDN-hosted files." + ), + ] = None + product_catalog: Annotated[ + Optional[ProductCatalog15], + Field( + description="Product catalog information for e-commerce advertisers. Enables SKU-level creative generation and product selection." + ), + ] = None + disclaimers: Annotated[ + Optional[list[Disclaimer]], + Field( + description="Legal disclaimers or required text that must appear in creatives" + ), + ] = None + industry: Annotated[ + Optional[str], + Field( + description="Industry or vertical (e.g., 'retail', 'automotive', 'finance', 'healthcare')" + ), + ] = None + target_audience: Annotated[ + Optional[str], Field(description="Primary target audience description") + ] = None + contact: Annotated[ + Optional[Contact], Field(description="Brand contact information") + ] = None + metadata: Annotated[ + Optional[Metadata], Field(description="Additional brand metadata") + ] = None + + +class ProductSelectors(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + manifest_skus: Annotated[ + Optional[list[str]], + Field( + description="Direct product SKU references from the brand manifest product catalog" + ), + ] = None + manifest_tags: Annotated[ + Optional[list[str]], + Field( + description="Select products by tags from the brand manifest product catalog (e.g., 'organic', 'sauces', 'holiday')" + ), + ] = None + manifest_category: Annotated[ + Optional[str], + Field( + description="Select products from a specific category in the brand manifest product catalog (e.g., 'beverages/soft-drinks', 'food/sauces')" + ), + ] = None + manifest_query: Annotated[ + Optional[str], + Field( + description="Natural language query to select products from the brand manifest (e.g., 'all Kraft Heinz pasta sauces', 'organic products under $20')" + ), + ] = None + + +class Offering(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + name: Annotated[ + str, + Field(description="Offering name (e.g., 'Winter Sale', 'New Product Launch')"), + ] + description: Annotated[ + Optional[str], Field(description="Description of what's being offered") + ] = None + assets: Annotated[ + Optional[list[dict[str, Any]]], + Field(description="Assets specific to this offering"), + ] = None + + +class AssetType26(Enum): + image = "image" + video = "video" + audio = "audio" + text = "text" + html = "html" + css = "css" + javascript = "javascript" + + +class AssetSelectors(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + tags: Annotated[ + Optional[list[str]], + Field( + description="Select assets with specific tags (e.g., ['holiday', 'premium'])" + ), + ] = None + asset_types: Annotated[ + Optional[list[AssetType26]], + Field(description="Filter by asset type (e.g., ['image', 'video'])"), + ] = None + exclude_tags: Annotated[ + Optional[list[str]], Field(description="Exclude assets with these tags") + ] = None + + +class Assets68(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + brand_manifest: Annotated[ + Union[Union[BrandManifest, BrandManifest14], AnyUrl], + Field( + description="Brand manifest provided either as an inline object or a URL string pointing to a hosted manifest", + examples=[ + { + "description": "Inline brand manifest", + "data": { + "url": "https://acmecorp.com", + "name": "ACME Corporation", + "colors": {"primary": "#FF6B35"}, + }, + }, + { + "description": "URL string reference to hosted manifest", + "data": "https://cdn.acmecorp.com/brand-manifest.json", + }, + ], + title="Brand Manifest Reference", + ), + ] + product_selectors: Annotated[ + Optional[ProductSelectors], + Field( + description="Specification of products or offerings being promoted in a campaign. Supports multiple selection methods from the brand manifest that can be combined using UNION (OR) logic. When multiple selection methods are provided, products matching ANY of the criteria are selected (logical OR, not AND).", + examples=[ + { + "description": "Direct SKU selection for specific products from brand manifest", + "data": {"manifest_skus": ["SKU-12345", "SKU-67890"]}, + }, + { + "description": "UNION selection: products tagged 'organic' OR 'sauces' OR in 'food/condiments' category from brand manifest", + "data": { + "manifest_tags": ["organic", "sauces"], + "manifest_category": "food/condiments", + }, + }, + { + "description": "Natural language product selection from brand manifest", + "data": {"manifest_query": "all Kraft Heinz pasta sauces under $5"}, + }, + { + "description": "Select products by tags", + "data": {"manifest_tags": ["holiday"]}, + }, + ], + title="Promoted Products", + ), + ] = None + offerings: Annotated[ + Optional[list[Offering]], + Field( + description="Inline offerings for campaigns without a product catalog. Each offering has a name, description, and associated assets." + ), + ] = None + asset_selectors: Annotated[ + Optional[AssetSelectors], + Field( + description="Selectors to choose specific assets from the brand manifest" + ), + ] = None + + +class CreativeManifest(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + format_id: Annotated[ + FormatId, + Field( + description="Structured format identifier with agent URL and format name", + title="Format ID", + ), + ] + promoted_offering: Annotated[ + Optional[str], + Field( + description="Product name or offering being advertised. Maps to promoted_offerings in create_media_buy request to associate creative with the product being promoted." + ), + ] = None + assets: Annotated[ + dict[ + str, + Union[ + Assets, + Assets56, + Assets57, + Union[Assets58, Assets59], + Assets60, + Assets61, + Assets62, + Assets63, + Assets64, + Assets65, + Union[Assets66, Assets67], + Assets68, + ], + ], + Field( + description="Map of asset IDs to actual asset content. Each key MUST match an asset_id from the format's assets_required array (e.g., 'banner_image', 'clickthrough_url', 'video_file', 'vast_tag'). The asset_id is the technical identifier used to match assets to format requirements.\n\nIMPORTANT: Creative manifest validation MUST be performed in the context of the format specification. The format defines what type each asset_id should be, which eliminates any validation ambiguity." + ), + ] + + +class Error(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + code: Annotated[str, Field(description="Error code for programmatic handling")] + message: Annotated[str, Field(description="Human-readable error message")] + field: Annotated[ + Optional[str], + Field( + description="Field path associated with the error (e.g., 'packages[0].targeting')" + ), + ] = None + suggestion: Annotated[ + Optional[str], Field(description="Suggested fix for the error") + ] = None + retry_after: Annotated[ + Optional[float], + Field(description="Seconds to wait before retrying the operation", ge=0.0), + ] = None + details: Annotated[ + Optional[Any], Field(description="Additional task-specific error details") + ] = None + + +class BuildCreativeResponse(BaseModel): + model_config = ConfigDict( + extra="forbid", + ) + creative_manifest: Annotated[ + CreativeManifest, + Field( + description="Complete specification of a creative with all assets needed for rendering in a specific format. Each asset is typed according to its asset_role from the format specification and contains the actual content/URL that fulfills the format requirements.", + title="Creative Manifest", + ), + ] + errors: Annotated[ + Optional[list[Error]], Field(description="Task-specific errors and warnings") + ] = None diff --git a/src/creative_agent/server.py b/src/creative_agent/server.py index 6a5559c..f512b81 100644 --- a/src/creative_agent/server.py +++ b/src/creative_agent/server.py @@ -18,9 +18,6 @@ get_format_by_id, ) from .schemas import ( - BuildCreativeRequest, - BuildCreativeResponse, - CreativeOutput, ListCreativeFormatsResponse, PreviewCreativeRequest, ) @@ -175,20 +172,14 @@ def preview_creative( creative_manifest: dict[str, Any], inputs: list[dict[str, Any]] | None = None, template_id: str | None = None, - brand_card: dict[str, Any] | None = None, - promoted_products: dict[str, Any] | None = None, - asset_filters: dict[str, Any] | None = None, ) -> ToolResult: """Generate preview renderings of a creative manifest. Args: format_id: Format identifier for rendering (string or FormatId object with agent_url and id) - creative_manifest: Complete creative manifest with all required assets + creative_manifest: Complete creative manifest with all required assets (including promoted_offerings if required by the format) inputs: Array of input sets for generating multiple preview variants template_id: Specific template for custom format rendering - brand_card: Brand information manifest providing context for dynamic previews - promoted_products: Products/offerings being promoted - asset_filters: Filters to select specific assets from brand card (tags, asset_types, exclude_tags) Returns: ToolResult with human-readable message and structured preview data @@ -213,9 +204,6 @@ def preview_creative( creative_manifest=creative_manifest, inputs=inputs_obj, template_id=template_id, - brand_card=brand_card, - promoted_products=promoted_products, - asset_filters=asset_filters, ) # Validate format exists @@ -426,492 +414,208 @@ def _generate_preview_variant( @mcp.tool() def build_creative( - message: str, - format_id: str | dict[str, Any], - gemini_api_key: str, - format_source: str | None = None, - context_id: str | None = None, - assets: list[dict[str, Any]] | None = None, - brand_card: dict[str, Any] | None = None, - promoted_offerings: dict[str, Any] | None = None, - output_mode: str = "manifest", - preview_options: dict[str, Any] | None = None, - finalize: bool = False, + target_format_id: str | dict[str, Any], + creative_manifest: dict[str, Any] | None = None, + message: str | None = None, + gemini_api_key: str | None = None, ) -> ToolResult: - """Build a creative manifest using AI generation (requires user's Gemini API key). + """Transform or generate a creative manifest using AI. Args: - message: Creative brief or refinement instructions - format_id: Format identifier to build for (string or FormatId object with agent_url and id) - gemini_api_key: User's Gemini API key (REQUIRED - we don't store or pay for API calls) - format_source: Optional sales agent URL for custom format lookup - context_id: Session ID for iterative refinement - assets: Optional asset library references - brand_card: (Deprecated) Brand information - use promoted_offerings instead - promoted_offerings: Brand and product information for AI generation (replaces brand_card) - output_mode: "manifest" for static creative or "code" for dynamic - preview_options: Preview generation options - finalize: Set to true to finalize the creative + target_format_id: Format ID to generate (string or FormatId object with agent_url and id) + creative_manifest: Source creative manifest with input assets (e.g., promoted_offerings for generative formats) + message: Natural language instructions for transformation or generation + gemini_api_key: User's Gemini API key for AI generation Returns: - ToolResult with human-readable message and structured build response + ToolResult with creative_manifest in structured_content """ try: - # Validate message length - if not message or len(message) > 10000: - error_msg = "Message must be between 1 and 10000 characters" - return ToolResult( - content=[TextContent(type="text", text=f"Error: {error_msg}")], - structured_content={"error": error_msg}, - ) + # Parse target_format_id + if isinstance(target_format_id, str): + fmt_id = FormatId(agent_url=AGENT_URL, id=target_format_id) + else: + fmt_id = FormatId(**target_format_id) - # Validate API key is provided - if not gemini_api_key: - error_msg = ( - "gemini_api_key is required. Please provide your own Gemini API key. Get one at https://ai.google.dev/" - ) + # Get format definition + fmt = get_format_by_id(fmt_id) + if not fmt: + error_msg = f"Format {fmt_id} not found" return ToolResult( content=[TextContent(type="text", text=f"Error: {error_msg}")], structured_content={"error": error_msg}, ) - # Import schema types - from pydantic import HttpUrl + # Initialize manifest if not provided + if creative_manifest is None: + creative_manifest = {"format_id": {"agent_url": str(AGENT_URL), "id": fmt_id.id}} - from .schemas.build import AssetReference - from .schemas.build import PreviewOptions as PreviewOptionsModel + # Ensure format_id is set in manifest + if "format_id" not in creative_manifest: + creative_manifest["format_id"] = {"agent_url": str(AGENT_URL), "id": fmt_id.id} - # Parse assets into AssetReference objects - asset_refs: list[AssetReference] = [] - if assets: - asset_refs = [AssetReference(**asset) for asset in assets] + # For generative formats, we need to generate the output creative + is_generative = fmt.output_format_ids and len(fmt.output_format_ids) > 0 - # Parse format_source into HttpUrl if provided - format_source_url: HttpUrl | None = None - if format_source: - format_source_url = HttpUrl(format_source) + if is_generative: + # Get output format + output_format_ids = fmt.output_format_ids + if not output_format_ids: # for mypy + error_msg = "Format has no output_format_ids" + return ToolResult( + content=[TextContent(type="text", text=f"Error: {error_msg}")], + structured_content={"error": error_msg}, + ) + output_fmt = get_format_by_id(output_format_ids[0]) + if not output_fmt: + error_msg = f"Output format {output_format_ids[0]} not found" + return ToolResult( + content=[TextContent(type="text", text=f"Error: {error_msg}")], + structured_content={"error": error_msg}, + ) - # Parse preview_options into PreviewOptions object - preview_opts: PreviewOptionsModel | None = None - if preview_options: - preview_opts = PreviewOptionsModel(**preview_options) + # Validate gemini_api_key is provided for generation + if not gemini_api_key: + error_msg = "gemini_api_key is required for generative formats. Get one at https://ai.google.dev/" + return ToolResult( + content=[TextContent(type="text", text=f"Error: {error_msg}")], + structured_content={"error": error_msg}, + ) - # Validate output_mode - if output_mode not in ["manifest", "code"]: - error_msg = f"Invalid output_mode '{output_mode}'. Must be 'manifest' or 'code'" - return ToolResult( - content=[TextContent(type="text", text=f"Error: {error_msg}")], - structured_content={"error": error_msg}, - ) + # Extract input assets from manifest + input_assets = creative_manifest.get("assets", {}) - # Extract format_id string (BuildCreativeRequest expects a string) - if isinstance(format_id, str): - format_id_str = format_id - else: # dict - format_id_str = format_id["id"] - - # Parse request - request = BuildCreativeRequest( - message=message, - format_id=format_id_str, - format_source=format_source_url, - context_id=context_id, - assets=asset_refs, - output_mode=output_mode, # type: ignore[arg-type] - preview_options=preview_opts, - finalize=finalize, - gemini_api_key=gemini_api_key, - ) + # Extract promoted_offerings if present + promoted_offerings = input_assets.get("promoted_offerings") + generation_prompt_asset = input_assets.get("generation_prompt") - # Get format definition - # BuildCreativeRequest.format_id is always a string, convert to FormatId for lookup - fmt_id = FormatId(agent_url=AGENT_URL, id=format_id_str) - fmt = get_format_by_id(fmt_id) - if not fmt: - error_msg = f"Format {request.format_id} not found" - return ToolResult( - content=[TextContent(type="text", text=f"Error: {error_msg}")], - structured_content={"error": error_msg}, - ) + # Build generation prompt + if not message and generation_prompt_asset: + message = generation_prompt_asset.get("content", "") - # For generative formats (identified by having output_format_ids), - # get the output format for asset requirements - output_fmt = fmt - if fmt.output_format_ids and len(fmt.output_format_ids) > 0: - # Use the first output format ID - output_fmt_result = get_format_by_id(fmt.output_format_ids[0]) - if not output_fmt_result: - error_msg = f"Output format {fmt.output_format_ids[0]} not found" + if not message: + error_msg = "message or generation_prompt asset is required for creative generation" return ToolResult( content=[TextContent(type="text", text=f"Error: {error_msg}")], structured_content={"error": error_msg}, ) - output_fmt = output_fmt_result - - # Initialize Gemini with user's API key using newer google-genai package - from google import genai - from google.genai import types - client = genai.Client(api_key=request.gemini_api_key) + # Generate creative using Gemini + from google import genai - # Build prompt for creative generation - # For generative formats, describe what we're generating (the output format) - is_generative = fmt.output_format_ids and len(fmt.output_format_ids) > 0 - target_format = output_fmt if is_generative else fmt + client = genai.Client(api_key=gemini_api_key) - format_spec = f"""Format: {fmt.name} -Type: {fmt.type.value} -Description: {fmt.description} + # Build prompt + format_spec = f"""Format: {output_fmt.name} +Type: {output_fmt.type.value} +Description: {output_fmt.description} """ - if is_generative: - format_spec += f"\nThis will generate a: {output_fmt.name}\n" if output_fmt.renders and len(output_fmt.renders) > 0: render = output_fmt.renders[0] if render.dimensions.width and render.dimensions.height: format_spec += f"Dimensions: {int(render.dimensions.width)}x{int(render.dimensions.height)}\n" - format_spec += "\nRequired Assets:\n" - - if target_format.assets_required: - from .schemas_generated._schemas_v1_core_format_json import AssetsRequired1 - - for asset_req in target_format.assets_required: - # Handle both AssetsRequired and AssetsRequired1 (repeatable groups) - if isinstance(asset_req, AssetsRequired1): - # This is a repeatable group (AssetsRequired1) - format_spec += f"- {asset_req.asset_group_id} (repeatable group, {asset_req.min_count}-{asset_req.max_count} items)\n" - for asset in asset_req.assets: - format_spec += f" - {asset.asset_id} ({asset.asset_type.value})" - if asset.requirements: - width = asset.requirements.get("width") - height = asset.requirements.get("height") - if width and height: - format_spec += f" - {width}x{height}" - desc = asset.requirements.get("description") - if desc: - format_spec += f": {desc}" - format_spec += "\n" - else: - # This is a regular asset (AssetsRequired) - format_spec += f"- {asset_req.asset_id} ({asset_req.asset_type.value})" - if asset_req.requirements: - width = asset_req.requirements.get("width") - height = asset_req.requirements.get("height") - if width and height: - format_spec += f" - {width}x{height}" - desc = asset_req.requirements.get("description") - if desc: - format_spec += f": {desc}" - format_spec += "\n" - - # Add brand context if provided (support both promoted_offerings and deprecated brand_card) - brand_data = promoted_offerings or brand_card - brand_context = "" - if brand_data: - brand_context = "\n\nBrand Context:\n" - - # Handle promoted_offerings structure - if "offerings" in brand_data: - for offering in brand_data["offerings"]: - if "name" in offering: - brand_context += f"- Product: {offering['name']}\n" - if "description" in offering: - brand_context += f" Description: {offering['description']}\n" - if "assets" in offering: - brand_context += f" Assets: {len(offering['assets'])} available\n" - - # Handle brand_card structure (backward compatibility) - if "url" in brand_data: - brand_context += f"- Brand Website: {brand_data['url']}\n" - if "colors" in brand_data: - brand_context += f"- Brand Colors: {', '.join(brand_data['colors'])}\n" - if "fonts" in brand_data: - brand_context += f"- Brand Fonts: {', '.join(brand_data['fonts'])}\n" - if "tone" in brand_data: - brand_context += f"- Brand Tone: {brand_data['tone']}\n" - if "assets" in brand_data and len(brand_data["assets"]) > 0: - brand_context += f"- Available Brand Assets: {len(brand_data['assets'])} assets (logos, images, etc.)\n" - - # Import Type and AssetType enums for comparisons - from .schemas_generated._schemas_v1_core_format_json import AssetType, Type - - # Check if we need to generate images (check target format, not input generative format) - generate_images = False - if target_format.assets_required: - generate_images = target_format.type == Type.display and any( - req.asset_type == AssetType.image for req in target_format.assets_required if hasattr(req, "asset_type") - ) - - # The format_id in the output manifest should be the OUTPUT format - # Extract string ID from FormatId object if needed - if is_generative: - output_format_id: str = ( - output_fmt.format_id.id if hasattr(output_fmt.format_id, "id") else str(output_fmt.format_id) - ) - else: - output_format_id = request.format_id - - if generate_images: - prompt = f"""You are a creative generation AI for advertising. - -{format_spec}{brand_context} - -User Request: {request.message} - -IMPORTANT: Generate actual images for the required image assets. Use the brand assets provided as style references. -After generating the images, output ONLY a JSON creative manifest with this structure: - -{{ - "format_id": "{output_format_id}", - "assets": {{ - // Map each required asset_id to appropriate asset data - // For images: {{"url": "GENERATED_IMAGE_PLACEHOLDER", "width": X, "height": Y, "format": "png"}} - // For text: {{"content": "..."}} - // For urls: {{"url": "..."}} - }}, - "metadata": {{ - "generated_by": "AdCP Creative Agent", - "timestamp": "{datetime.now(UTC).isoformat()}" - }} -}} + format_spec += "\nRequired Assets:\n" + if output_fmt.assets_required: + from .schemas_generated._schemas_v1_core_format_json import AssetsRequired1 + + for asset_req in output_fmt.assets_required: + if isinstance(asset_req, AssetsRequired1): + # Repeatable group + format_spec += f"- {asset_req.asset_group_id} (repeatable group)\n" + else: + # Regular asset + format_spec += f"- {asset_req.asset_id} ({asset_req.asset_type.value})\n" + + # Add brand context if provided + brand_context = "" + if promoted_offerings: + brand_context = "\n\nBrand Context:\n" + brand_manifest = promoted_offerings.get("brand_manifest", {}) + if "name" in brand_manifest: + brand_context += f"Brand: {brand_manifest['name']}\n" + if "description" in brand_manifest: + brand_context += f"Description: {brand_manifest['description']}\n" + if "tagline" in brand_manifest: + brand_context += f"Tagline: {brand_manifest['tagline']}\n" -Generate images first, then return the JSON manifest.""" - else: prompt = f"""You are a creative generation AI for advertising. Generate a creative manifest for the following request: {format_spec}{brand_context} -User Request: {request.message} +User Request: {message} Generate a JSON creative manifest with the following structure: {{ - "format_id": "{output_format_id}", + "format_id": "{output_fmt.format_id.id if hasattr(output_fmt.format_id, "id") else str(output_fmt.format_id)}", "assets": {{ // Map each required asset_id to appropriate asset data // For text: {{"content": "..."}} // For urls: {{"url": "..."}} - }}, - "metadata": {{ - "generated_by": "AdCP Creative Agent", - "timestamp": "{datetime.now(UTC).isoformat()}" }} }} Return ONLY the JSON manifest, no additional text.""" - # Prepare contents array with prompt and optional brand asset images - contents: list[Any] = [prompt] - - # Add brand asset images if provided (from promoted_offerings or brand_card) - import ipaddress - from urllib.parse import urlparse - - import httpx - - def is_safe_url(url: str) -> bool: - """Validate URL is safe to fetch (no localhost, private IPs, etc.).""" - try: - parsed = urlparse(url) - if parsed.scheme not in ["http", "https"]: - return False - - hostname = parsed.hostname - if not hostname: - return False - - # Block localhost - if hostname.lower() in ["localhost", "127.0.0.1", "::1"]: - return False - - # Block private IP ranges - try: - ip = ipaddress.ip_address(hostname) - if ip.is_private or ip.is_loopback or ip.is_link_local: - return False - except ValueError: - # Hostname is not an IP, that's fine - pass - - return True - except Exception: - return False - - # Collect all image assets from various sources - image_assets = [] - - if brand_data: - # Get images from promoted_offerings offerings - if "offerings" in brand_data: - for offering in brand_data["offerings"]: - if "assets" in offering: - image_assets.extend(offering["assets"]) - - # Get images from brand_card assets (backward compatibility) - if "assets" in brand_data: - image_assets.extend(brand_data["assets"]) - - # Fetch and add all image assets - for asset in image_assets: - if isinstance(asset, dict) and asset.get("asset_type") == "image": - img_url = asset.get("url") - if img_url: - if not is_safe_url(img_url): - print(f"Warning: Blocked potentially unsafe URL: {img_url}") - continue - try: - # Fetch image from URL - img_response = httpx.get(img_url, timeout=10.0, follow_redirects=False) - img_response.raise_for_status() - img_bytes = img_response.content - - # Determine mime type from content-type header or URL - mime_type = img_response.headers.get("content-type", "image/png") - if not mime_type.startswith("image/"): - mime_type = "image/png" - - # Add image as Part - contents.append(types.Part.from_bytes(data=img_bytes, mime_type=mime_type)) - except Exception as e: - # Log error but continue with other images - print(f"Warning: Failed to fetch brand asset image {img_url}: {e}") - - if generate_images: - # Use image generation model - response = client.models.generate_content( - model="gemini-2.5-flash-image", - contents=contents, - ) - else: - # Use text-only model + # Call Gemini response = client.models.generate_content( model="gemini-2.0-flash-exp", - contents=contents, + contents=prompt, ) - generated_text = "" - generated_images = [] - - if response.candidates and response.candidates[0].content and response.candidates[0].content.parts: - for part in response.candidates[0].content.parts: - if part.text is not None: - generated_text += part.text - elif part.inline_data is not None and part.inline_data.data is not None: - # Save generated image - import base64 - from io import BytesIO - - from PIL import Image - - image = Image.open(BytesIO(part.inline_data.data)) - # Convert to base64 data URI - buffered = BytesIO() - image.save(buffered, format="PNG") - img_data = base64.b64encode(buffered.getvalue()).decode() - generated_images.append(f"data:image/png;base64,{img_data}") - - # Extract JSON from response (handle markdown code blocks) - import re - - json_match = re.search(r"```json\s*(.*?)\s*```", generated_text, re.DOTALL) - if json_match: - manifest_json = json_match.group(1) - else: - # Try to find JSON directly - manifest_json = generated_text.strip() - - # Parse the generated manifest - manifest_data = json.loads(manifest_json) - - # If we generated images, inject them into the manifest - if generated_images and "assets" in manifest_data: - image_index = 0 - for _asset_id, asset_data in manifest_data["assets"].items(): - if isinstance(asset_data, dict) and asset_data.get("asset_type") == "image": - if image_index < len(generated_images): - # Use generated image as data URI - asset_data["url"] = generated_images[image_index] - image_index += 1 - - # Validate generated manifest assets - from .validation import validate_manifest_assets + generated_text = "" + if response.candidates and response.candidates[0].content and response.candidates[0].content.parts: + for part in response.candidates[0].content.parts: + if part.text: + generated_text += part.text - validation_errors = validate_manifest_assets( - manifest_data, - check_remote_mime=False, - format_obj=target_format, - ) - if validation_errors: - error_msg = "AI-generated creative failed validation" - return ToolResult( - content=[ - TextContent( - type="text", text=f"Error: {error_msg}. Please try again with more specific instructions." - ) - ], - structured_content={ - "error": error_msg, - "validation_errors": validation_errors, - "hint": "The AI generated invalid assets. Please try again with more specific instructions.", - }, - ) + # Extract JSON from response + import re - # Generate session context ID - session_context_id = request.context_id or str(uuid.uuid4()) + json_match = re.search(r"```json\s*(.*?)\s*```", generated_text, re.DOTALL) + if json_match: + manifest_json = json_match.group(1) + else: + manifest_json = generated_text.strip() - # Determine status - must be one of the literal types - from typing import Literal + # Parse generated manifest + output_manifest = json.loads(manifest_json) - status: Literal["draft", "ready", "finalized"] - if request.finalize: - status = "finalized" - elif "ready" in request.message.lower(): - status = "ready" - else: - status = "draft" - - # Build output_format_ids list for generative formats - output_format_ids_list = fmt.output_format_ids if is_generative else None - - # Build response - build_response = BuildCreativeResponse( - message=f"Generated {fmt.name} creative based on your request. {'Finalized and ready to use.' if request.finalize else 'Review and refine as needed.'}", - context_id=session_context_id, - status=status, - creative_output=CreativeOutput( - type="creative_manifest", - format_id=output_format_id, - output_format_ids=output_format_ids_list, - data=manifest_data, - ), - preview=None, # Could integrate with preview_creative here - refinement_suggestions=[ - "Consider A/B testing different headlines", - "Test various CTA button colors", - "Try different image crops for mobile vs desktop", - ] - if not request.finalize - else [], - ) + # Validate against output format + from .validation import validate_manifest_assets - # Return ToolResult with human message and structured data - message = f"Generated {fmt.name} creative. Status: {status}" - if request.finalize: - message += " (finalized and ready to use)" + validation_errors = validate_manifest_assets( + output_manifest, + check_remote_mime=False, + format_obj=output_fmt, + ) + if validation_errors: + error_msg = "AI-generated creative failed validation" + return ToolResult( + content=[TextContent(type="text", text=f"Error: {error_msg}")], + structured_content={ + "error": error_msg, + "validation_errors": validation_errors, + }, + ) + # Return the generated manifest + return ToolResult( + content=[TextContent(type="text", text=f"Generated {output_fmt.name} creative")], + structured_content={"creative_manifest": output_manifest}, + ) + # Non-generative: return manifest as-is (or with minimal transformation) return ToolResult( - content=[TextContent(type="text", text=message)], - structured_content=build_response.model_dump(mode="json"), + content=[TextContent(type="text", text=f"Creative manifest for {fmt.name}")], + structured_content={"creative_manifest": creative_manifest}, ) except json.JSONDecodeError as e: error_msg = f"Failed to parse AI-generated creative: {e}" return ToolResult( content=[TextContent(type="text", text=f"Error: {error_msg}")], - structured_content={ - "error": error_msg, - "context": {"line": e.lineno, "column": e.colno, "format_id": format_id}, - }, + structured_content={"error": error_msg}, ) except ValueError as e: error_msg = f"Invalid input: {e}" @@ -927,7 +631,7 @@ def is_safe_url(url: str) -> bool: content=[TextContent(type="text", text=f"Error: {error_msg}")], structured_content={ "error": error_msg, - "context": {"format_id": format_id, "traceback": traceback.format_exc()[-500:]}, + "traceback": traceback.format_exc()[-500:], }, ) diff --git a/tests/schemas/v1/_schemas_v1_core_assets_promoted-offerings-asset_json.json b/tests/schemas/v1/_schemas_v1_core_assets_promoted-offerings-asset_json.json deleted file mode 100644 index 4ea2b4f..0000000 --- a/tests/schemas/v1/_schemas_v1_core_assets_promoted-offerings-asset_json.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/schemas/v1/core/assets/promoted-offerings-asset.json", - "title": "Promoted Offerings Asset", - "description": "Reference to promoted offerings specification", - "type": "object", - "properties": { - "asset_type": { - "type": "string", - "const": "promoted_offerings" - }, - "url": { - "type": "string", - "format": "uri", - "description": "URL of the advertiser's brand or offering (e.g., https://retailer.com)" - }, - "colors": { - "type": "object", - "description": "Brand colors", - "properties": { - "primary": { - "type": "string" - }, - "secondary": { - "type": "string" - }, - "accent": { - "type": "string" - } - } - }, - "fonts": { - "type": "array", - "description": "Brand fonts", - "items": { - "type": "string" - } - }, - "tone": { - "type": "string", - "description": "Brand tone/voice" - } - }, - "required": [ - "asset_type" - ], - "additionalProperties": false -} diff --git a/tests/schemas/v1/_schemas_v1_core_creative-library-item_json.json b/tests/schemas/v1/_schemas_v1_core_creative-library-item_json.json deleted file mode 100644 index ff6c3a4..0000000 --- a/tests/schemas/v1/_schemas_v1_core_creative-library-item_json.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/schemas/v1/core/creative-library-item.json", - "title": "Creative Library Item", - "description": "Creative asset as it appears in the centralized library", - "type": "object", - "properties": { - "creative_id": { - "type": "string", - "description": "Unique identifier for the creative" - }, - "name": { - "type": "string", - "description": "Human-readable creative name" - }, - "format": { - "type": "string", - "description": "Creative format type (e.g., video, audio, display)" - }, - "status": { - "$ref": "/schemas/v1/enums/creative-status.json" - }, - "platform_id": { - "type": "string", - "description": "Platform-specific ID assigned to the creative" - }, - "created_date": { - "type": "string", - "format": "date-time", - "description": "When the creative was uploaded to the library" - }, - "last_updated": { - "type": "string", - "format": "date-time", - "description": "When the creative was last modified" - }, - "media_url": { - "type": "string", - "format": "uri", - "description": "URL of the creative file" - }, - "click_url": { - "type": "string", - "format": "uri", - "description": "Landing page URL for the creative" - }, - "duration": { - "type": "number", - "description": "Duration in milliseconds (for video/audio)", - "minimum": 0 - }, - "width": { - "type": "number", - "description": "Width in pixels (for video/display)", - "minimum": 0 - }, - "height": { - "type": "number", - "description": "Height in pixels (for video/display)", - "minimum": 0 - }, - "file_size": { - "type": "integer", - "description": "File size in bytes", - "minimum": 0 - }, - "assignments": { - "type": "array", - "description": "Current package assignments for this creative", - "items": { - "type": "string" - } - }, - "assignment_count": { - "type": "integer", - "description": "Number of active package assignments", - "minimum": 0 - }, - "performance_metrics": { - "type": "object", - "description": "Aggregated performance data across all assignments", - "properties": { - "total_impressions": { - "type": "integer", - "description": "Total impressions delivered", - "minimum": 0 - }, - "total_clicks": { - "type": "integer", - "description": "Total clicks generated", - "minimum": 0 - }, - "average_ctr": { - "type": "number", - "description": "Average click-through rate across assignments", - "minimum": 0, - "maximum": 1 - }, - "last_served": { - "type": "string", - "format": "date-time", - "description": "When this creative last served an impression" - } - }, - "additionalProperties": false - }, - "compliance": { - "type": "object", - "description": "Compliance review status", - "properties": { - "status": { - "type": "string", - "description": "Compliance status", - "enum": [ - "approved", - "pending", - "rejected", - "flagged" - ] - }, - "issues": { - "type": "array", - "description": "Array of compliance issues", - "items": { - "type": "string" - } - }, - "reviewed_date": { - "type": "string", - "format": "date-time", - "description": "When compliance review was completed" - } - }, - "required": [ - "status" - ], - "additionalProperties": false - }, - "review_feedback": { - "type": "string", - "description": "Latest feedback from platform review" - }, - "tags": { - "type": "array", - "description": "User-defined tags for organization", - "items": { - "type": "string" - } - }, - "assets": { - "type": "array", - "description": "Sub-assets for multi-asset formats like carousels", - "items": { - "$ref": "/schemas/v1/core/sub-asset.json" - } - } - }, - "required": [ - "creative_id", - "name", - "format", - "status", - "created_date" - ], - "additionalProperties": false -} diff --git a/tests/schemas/v1/_schemas_v1_creative_preview-creative-request_json.json b/tests/schemas/v1/_schemas_v1_creative_preview-creative-request_json.json index 22b3a82..9ac7508 100644 --- a/tests/schemas/v1/_schemas_v1_creative_preview-creative-request_json.json +++ b/tests/schemas/v1/_schemas_v1_creative_preview-creative-request_json.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/schemas/v1/creative/preview-creative-request.json", "title": "Preview Creative Request", - "description": "Request to generate a preview of a creative manifest in a specific format", + "description": "Request to generate a preview of a creative manifest in a specific format. The creative_manifest should include all assets required by the format (e.g., promoted_offerings for generative formats).", "type": "object", "properties": { "format_id": { @@ -11,7 +11,7 @@ }, "creative_manifest": { "$ref": "/schemas/v1/core/creative-manifest.json", - "description": "Complete creative manifest with all required assets" + "description": "Complete creative manifest with all required assets (including promoted_offerings if required by the format)" }, "inputs": { "type": "array", @@ -44,10 +44,6 @@ "template_id": { "type": "string", "description": "Specific template ID for custom format rendering" - }, - "promoted_offerings": { - "$ref": "/schemas/v1/core/promoted-offerings.json", - "description": "Complete offering specification for dynamic creative previews - includes brand manifest, product selectors, inline offerings, and asset filters" } }, "required": [ diff --git a/tests/schemas/v1/_schemas_v1_enums_snippet-type_json.json b/tests/schemas/v1/_schemas_v1_enums_snippet-type_json.json deleted file mode 100644 index 1632acb..0000000 --- a/tests/schemas/v1/_schemas_v1_enums_snippet-type_json.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/schemas/v1/enums/snippet-type.json", - "title": "Snippet Type", - "description": "Types of third-party creative snippets supported by AdCP", - "type": "string", - "enum": [ - "vast_xml", - "vast_url", - "html", - "javascript", - "iframe", - "daast_url" - ], - "enumDescriptions": { - "vast_xml": "Inline VAST XML content for video advertisements", - "vast_url": "URL endpoint that returns VAST XML for video advertisements", - "html": "HTML markup for display advertisements", - "javascript": "JavaScript code snippet for programmatic advertisements", - "iframe": "iFrame tag for secure third-party advertisement delivery", - "daast_url": "DAAST (Digital Audio Ad Serving Template) URL for audio advertisements" - }, - "examples": [ - "vast_xml", - "vast_url", - "html", - "javascript", - "iframe", - "daast_url" - ], - "usage": { - "vast_xml": { - "format_types": [ - "video" - ], - "security": "XML validation recommended", - "platforms": [ - "Connected TV", - "Video players", - "Mobile apps" - ] - }, - "vast_url": { - "format_types": [ - "video" - ], - "security": "URL validation required", - "platforms": [ - "Connected TV", - "Video players", - "Mobile apps" - ] - }, - "html": { - "format_types": [ - "display" - ], - "security": "HTML sanitization required", - "platforms": [ - "Web display", - "Mobile web" - ] - }, - "javascript": { - "format_types": [ - "display" - ], - "security": "Script validation and sandboxing required", - "platforms": [ - "Web display", - "Programmatic" - ] - }, - "iframe": { - "format_types": [ - "display" - ], - "security": "Secure iframe with restrictions", - "platforms": [ - "Web display", - "Mobile web" - ] - }, - "daast_url": { - "format_types": [ - "audio" - ], - "security": "URL validation required", - "platforms": [ - "Audio streaming", - "Podcast", - "Radio" - ] - } - } -} diff --git a/tests/schemas/v1/_schemas_v1_media-buy_build-creative-request_json.json b/tests/schemas/v1/_schemas_v1_media-buy_build-creative-request_json.json new file mode 100644 index 0000000..f1c0c72 --- /dev/null +++ b/tests/schemas/v1/_schemas_v1_media-buy_build-creative-request_json.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/build-creative-request.json", + "title": "Build Creative Request", + "description": "Request to transform or generate a creative manifest. Takes a source manifest (which may be minimal for pure generation) and produces a target manifest in the specified format. The source manifest should include all assets required by the target format (e.g., promoted_offerings for generative formats).", + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Natural language instructions for the transformation or generation. For pure generation, this is the creative brief. For transformation, this provides guidance on how to adapt the creative." + }, + "creative_manifest": { + "$ref": "/schemas/v1/core/creative-manifest.json", + "description": "Creative manifest to transform or generate from. For pure generation, this should include the target format_id and any required input assets (e.g., promoted_offerings for generative formats). For transformation (e.g., resizing, reformatting), this is the complete creative to adapt." + }, + "target_format_id": { + "$ref": "/schemas/v1/core/format-id.json", + "description": "Format ID to generate. The format definition specifies required input assets and output structure." + } + }, + "required": [ + "target_format_id" + ], + "additionalProperties": false +} diff --git a/tests/schemas/v1/_schemas_v1_media-buy_build-creative-response_json.json b/tests/schemas/v1/_schemas_v1_media-buy_build-creative-response_json.json new file mode 100644 index 0000000..ebc0cb9 --- /dev/null +++ b/tests/schemas/v1/_schemas_v1_media-buy_build-creative-response_json.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/build-creative-response.json", + "title": "Build Creative Response", + "description": "Response containing the transformed or generated creative manifest, ready for use with preview_creative or sync_creatives", + "type": "object", + "properties": { + "creative_manifest": { + "$ref": "/schemas/v1/core/creative-manifest.json", + "description": "The generated or transformed creative manifest" + }, + "errors": { + "type": "array", + "description": "Task-specific errors and warnings", + "items": { + "$ref": "/schemas/v1/core/error.json" + } + } + }, + "required": [ + "creative_manifest" + ], + "additionalProperties": false +} diff --git a/tests/schemas/v1/_schemas_v1_standard-formats_asset-types_index_json.json b/tests/schemas/v1/_schemas_v1_standard-formats_asset-types_index_json.json deleted file mode 100644 index 50db731..0000000 --- a/tests/schemas/v1/_schemas_v1_standard-formats_asset-types_index_json.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "/schemas/v1/standard-formats/asset-types/index.json", - "title": "AdCP Asset Type Definitions Registry", - "description": "Standardized definitions for all asset types used in AdCP creative formats", - "version": "2.0.0", - "lastUpdated": "2025-09-03", - "asset_types": { - "video": { - "description": "Video file asset", - "schema": "/schemas/v1/standard-formats/asset-types/video.json" - }, - "image": { - "description": "Static image asset", - "schema": "/schemas/v1/standard-formats/asset-types/image.json" - }, - "text": { - "description": "Text content asset", - "schema": "/schemas/v1/standard-formats/asset-types/text.json" - }, - "url": { - "description": "URL/link asset", - "schema": "/schemas/v1/standard-formats/asset-types/url.json" - }, - "audio": { - "description": "Audio file asset", - "schema": "/schemas/v1/standard-formats/asset-types/audio.json" - }, - "html": { - "description": "HTML5 creative asset", - "schema": "/schemas/v1/standard-formats/asset-types/html.json" - }, - "dynamic_endpoint": { - "description": "Dynamic creative endpoint (DCO webhook)", - "schema": "/schemas/v1/standard-formats/asset-types/dynamic_endpoint.json" - }, - "javascript": { - "description": "JavaScript-based dynamic creative", - "schema": "/schemas/v1/standard-formats/asset-types/javascript.json" - }, - "template": { - "description": "Template-based dynamic creative with data binding", - "schema": "/schemas/v1/standard-formats/asset-types/template.json" - } - }, - "common_properties": { - "asset_id": { - "description": "Unique identifier for this asset within the format, used by orchestrators to map uploaded assets", - "type": "string", - "pattern": "^[a-z0-9_]+$" - }, - "asset_type": { - "description": "The type of asset", - "type": "string", - "enum": [ - "video", - "image", - "text", - "url", - "audio", - "html", - "dynamic_endpoint", - "javascript", - "template" - ] - }, - "required": { - "description": "Whether this asset is mandatory for the creative format", - "type": "boolean" - } - } -} From e72b1da6af07f983327dfffdfe61e1a6e22011aa Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Mon, 20 Oct 2025 07:03:54 -0400 Subject: [PATCH 2/3] Fix schema generation: add type ignore for webhook method default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI check was failing because the schema generation script wasn't adding the necessary type: ignore comment for the webhook method field. This fix ensures the generated schemas are deterministic and pass mypy validation consistently. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- scripts/generate_schemas.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/generate_schemas.py b/scripts/generate_schemas.py index 50154cc..11f8082 100755 --- a/scripts/generate_schemas.py +++ b/scripts/generate_schemas.py @@ -260,6 +260,18 @@ def generate_schemas_from_json(schema_dir: Path, output_file: Path): creative_asset_file.write_text(content) print("✅ Fixed mypy issue in creative-asset schema") + # Fix mypy issue with webhook method default in build-creative-response + build_response_file = output_file / "_schemas_v1_media_buy_build_creative_response_json.py" + if build_response_file.exists(): + content = build_response_file.read_text() + # Add type: ignore comment to the problematic line + content = content.replace( + 'Field(description="HTTP method")] = "POST"', + 'Field(description="HTTP method")] = "POST" # type: ignore[assignment]', + ) + build_response_file.write_text(content) + print("✅ Fixed mypy issue in build-creative-response schema") + finally: # Clean up temp directory import shutil From e767e5a42b0df1793e8dc931f4534c37f21eb105 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Mon, 20 Oct 2025 07:08:23 -0400 Subject: [PATCH 3/3] Fix test imports after schema regeneration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After regenerating schemas, the auto-generated class names changed: - Assets31 (was UrlAsset) is now VastAsset - Assets33 is now UrlAsset - Assets28 is VideoAsset - Assets32 is TextAsset Updated all test imports to use the correct asset classes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tests/integration/test_preview_creative.py | 2 +- tests/integration/test_tool_response_formats.py | 2 +- tests/unit/test_preview_generation.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration/test_preview_creative.py b/tests/integration/test_preview_creative.py index 4e378e2..fc5fe0c 100644 --- a/tests/integration/test_preview_creative.py +++ b/tests/integration/test_preview_creative.py @@ -14,7 +14,7 @@ Assets as ImageAsset, ) from creative_agent.schemas_generated._schemas_v1_creative_preview_creative_request_json import ( - Assets31 as UrlAsset, + Assets33 as UrlAsset, ) from creative_agent.schemas_generated._schemas_v1_creative_preview_creative_request_json import ( CreativeManifest, diff --git a/tests/integration/test_tool_response_formats.py b/tests/integration/test_tool_response_formats.py index f065022..7ccfbd5 100644 --- a/tests/integration/test_tool_response_formats.py +++ b/tests/integration/test_tool_response_formats.py @@ -21,7 +21,7 @@ Assets as ImageAsset, ) from creative_agent.schemas_generated._schemas_v1_creative_preview_creative_request_json import ( - Assets31 as UrlAsset, + Assets33 as UrlAsset, ) from creative_agent.schemas_generated._schemas_v1_creative_preview_creative_request_json import ( CreativeManifest, diff --git a/tests/unit/test_preview_generation.py b/tests/unit/test_preview_generation.py index e58802e..ce883cc 100644 --- a/tests/unit/test_preview_generation.py +++ b/tests/unit/test_preview_generation.py @@ -11,13 +11,13 @@ Assets as ImageAsset, ) from creative_agent.schemas_generated._schemas_v1_creative_preview_creative_request_json import ( - Assets30 as VideoAsset, + Assets28 as VideoAsset, ) from creative_agent.schemas_generated._schemas_v1_creative_preview_creative_request_json import ( - Assets31 as UrlAsset, + Assets32 as TextAsset, ) from creative_agent.schemas_generated._schemas_v1_creative_preview_creative_request_json import ( - Assets34 as TextAsset, + Assets33 as UrlAsset, ) from creative_agent.schemas_generated._schemas_v1_creative_preview_creative_request_json import ( CreativeManifest,