Skip to content

feat(rsa): mutate headlines + descriptions in place via AdService#42

Open
illia-sapryga wants to merge 3 commits into
kLOsk:mainfrom
illia-sapryga:feat/rsa-headline-mutation
Open

feat(rsa): mutate headlines + descriptions in place via AdService#42
illia-sapryga wants to merge 3 commits into
kLOsk:mainfrom
illia-sapryga:feat/rsa-headline-mutation

Conversation

@illia-sapryga
Copy link
Copy Markdown
Contributor

Summary

  • Extends update_responsive_search_ad to accept headlines and descriptions list-replace params alongside the existing URL/path mutations
  • Routes through AdService.mutate_ads with a field-mask on responsive_search_ad.headlines / descriptions (Google's AdGroupAdService doesn't permit these, but AdService does — contrary to the prior tool docstring)
  • Avoids the pause-old + create-new + learning-reset cycle for straightforward copy rewrites

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_rsa asset 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_ad function. The diff against main is large for that reason; the actual change in this PR is the single commit a041535. After #34 merges, this PR rebases to a focused +585 / -29 diff.

Test plan

  • 23 new unit tests 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 local test suite: 541 passed, 0 failed
  • Live-tested on a paused BBP ad (id 795877178562, ad group 196473554367 "Water Heater Repair" — PAUSED, so zero serving risk):

illia-sapryga and others added 3 commits May 27, 2026 18:31
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant