feat(rsa): mutate headlines + descriptions in place via AdService#42
Open
illia-sapryga wants to merge 3 commits into
Open
feat(rsa): mutate headlines + descriptions in place via AdService#42illia-sapryga wants to merge 3 commits into
illia-sapryga wants to merge 3 commits into
Conversation
Adds the complete asset-extension and conversion-action management
surface that AdLoop was missing. Three logical chunks bundled here
because they share `ads/write.py`'s dispatch + apply infrastructure
and would conflict if split into separate PRs.
- `draft_call_asset` — campaign or customer scope, E.164 normalization,
ad-schedule restriction, optional conversion-action override
- `draft_location_asset` — Google Business Profile-backed AssetSet
(LOCATION_SYNC), with label/listing filters
- `draft_image_assets` — campaign image extensions from local files
with MIME + dimension validation
- `draft_callouts`, `draft_structured_snippets`, `draft_sitelinks` —
refactored to support BOTH campaign-scope AND customer-scope (the
account-level CustomerAsset path that propagates to every eligible
campaign automatically)
- `add_ad_schedule` — Mon-Sat 8am-9pm-style scheduling via
AdScheduleInfo CampaignCriterion
- `add_geo_exclusions` — negative geo CampaignCriterion records to
shrink a broad include list
- `_apply_assets()` shared helper routing a populate fn through either
CampaignAsset or CustomerAsset linkage based on scope
- Phone-number E.164 normalization with US/CA + EU trunk-prefix handling
- Update existing RSAs without delete-then-recreate
- Partial update via FieldMask — only the fields the caller passes
are modified
- Headlines/descriptions accept either bare strings (unpinned) or
{"text": "...", "pinned_field": "HEADLINE_1"} dicts (pinned)
- `draft_create_conversion_action` — AD_CALL / WEBSITE_CALL / WEBPAGE
/ GA4_CUSTOM with value, threshold, attribution model, counting type
- `draft_update_conversion_action` — partial update with FieldMask;
rename / promote-demote / set value / change duration threshold
- `draft_remove_conversion_action` — irreversible removal (warns that
SMART_CAMPAIGN_* and GOOGLE_HOSTED types reject mutation)
The 3 conversion-action tools live in their own module
`adloop/ads/conversion_actions.py` and route through dispatch via
`_apply_*_conversion_action_route` shims kept in `ads/write.py`.
- `link_asset_to_customer` — promote existing Asset rows from
campaign-scope to account-level (CustomerAsset)
- `update_call_asset` / `update_sitelink` / `update_callout` —
in-place asset updates with FieldMask
- `draft_promotion` / `update_promotion` — PromotionAsset create
+ swap (PromotionAsset is immutable; update is implemented as
create-new-link-old-unlink)
- Promotion module uses `enum_names("PromotionExtensionOccasionEnum")`
+ `enum_names("PromotionExtensionDiscountModifierEnum")` from
the dynamic-enums helper
- Conversion-actions module uses `enum_names("...")` for all 4
Google Ads enums it validates against — drops 4 hardcoded enum
sets that were drifting from the SDK
- Auto-cleanup script `scripts/cleanup_sitelink_links.py` for
duplicate sitelink CampaignAsset detection
- `tests/test_ads_extensions.py` — comprehensive validation +
apply-handler tests for every new function (uses fake services
mirroring the google-ads SDK protos; no network)
- `tests/test_conversion_actions.py` — 29 tests
- `tests/test_update_rsa.py` — RSA update integration tests
- All 430 tests pass
Add two new MCP tools for pushing offline conversion data directly to Google Ads, complementing the existing conversion-action management: - draft_upload_call_conversions: uploads call conversions (Caller ID + Call Start Time) to UPLOAD_CALLS-typed actions via ConversionUploadService.UploadCallConversions - draft_upload_enhanced_conversions_for_leads: uploads hashed PII to UPLOAD_CLICKS-typed actions via UploadClickConversions with user_identifiers populated. Works retroactively — no "action must exist before the conversion" constraint that UPLOAD_CALLS has. Both tools follow the AdLoop draft → confirm_and_apply pattern with plan storage, dry-run, audit logging, and per-row error reporting via partial_failure mode. Adds 25 new unit tests covering CSV parsing, action-name resolution, payload shape, partial-failure handling, and type-mismatch rejection (UPLOAD_CALLS vs UPLOAD_CLICKS). Also removes scripts/cleanup_sitelink_links.py, which was scoped to a specific client cleanup and shouldn't ship in the public tool.
update_responsive_search_ad now accepts `headlines` and `descriptions` list-replace params in addition to URL/paths. The mutation routes through AdService.mutate_ads with a field-mask covering `responsive_search_ad.headlines` / `descriptions` (Google's AdGroupAdService doesn't permit these — AdService does, contrary to the prior tool docstring). This avoids the pause-old + create-new + learning-reset cycle for straightforward copy rewrites — most useful for compliance-driven swaps (e.g. removing a "Life Time Warranty" claim) where the rest of the ad copy should stay verbatim. Validation: - _validate_rsa asset checks factored into _validate_rsa_assets, shared by draft (full-RSA-create) and update paths - update path only enforces 3-15 / 2-4 count gates on the list being replaced — the untouched list keeps whatever Google already has Tests: +23 cases covering count/char/pin validation, plan construction, field-mask + AdTextAsset population, pinned-field enum propagation, and the full draft -> confirm_and_apply round trip. Full suite: 541 passed. Live-tested on a paused BBP ad (id 795877178562, ad group PAUSED): headline kLOsk#6 swapped from "Life Time Warranty" to "7-Year Warranty Standard", final_url corrected from tankless to water-heater-repair — both verified via GAQL after apply.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
update_responsive_search_adto acceptheadlinesanddescriptionslist-replace params alongside the existing URL/path mutationsAdService.mutate_adswith a field-mask onresponsive_search_ad.headlines/descriptions(Google'sAdGroupAdServicedoesn't permit these, butAdServicedoes — contrary to the prior tool docstring)Why
Most useful for compliance-driven swaps where the rest of the ad copy should stay verbatim — for example, removing a "Life Time Warranty" claim from one headline without resetting the ad's serving history. Previously the only path was draft → new RSA + pause old, which loses optimization signal and creates ad-group churn.
Validation
_validate_rsaasset checks factored into_validate_rsa_assets, shared by both the full-RSA-create path and the in-place update path. The update path only enforces 3-15 headlines / 2-4 descriptions count gates on the list being replaced — the untouched list keeps whatever Google already has, so its size on the wire isn't ours to validate.Depends on
This PR sits on top of #34 (asset + conversion tools), which introduced the original
update_responsive_search_adfunction. The diff againstmainis large for that reason; the actual change in this PR is the single commita041535. After #34 merges, this PR rebases to a focused +585 / -29 diff.Test plan
795877178562, ad group196473554367"Water Heater Repair" — PAUSED, so zero serving risk):Life Time Warranty→7-Year Warranty Standardfinal_urlcorrected:tankless-water-heater-installation→water-heater-repair