feat(shared-set): add attach/detach tools for CampaignSharedSet#22
feat(shared-set): add attach/detach tools for CampaignSharedSet#22SullyGitHub wants to merge 1 commit intokLOsk:mainfrom
Conversation
kLOsk
left a comment
There was a problem hiding this comment.
Thanks — this fills a real gap between "create a shared list" and "actually have it apply to a campaign." The composite {campaign_id}~{shared_set_id} resource-name trick for detach is the right call and avoids a GAQL lookup. Validation helper sharing between attach/detach is clean, and the test coverage is solid.
One blocking change:
The PR description says detaching a non-attached set "will fail for that specific operation but does not affect the others; surfaced in the apply response." That's only true with partial_failure=True on the mutate request — and looking at both _apply_attach_shared_set_to_campaigns and _apply_detach_shared_set_from_campaigns, neither passes it. As written, the entire batch fails on the first non-existent linkage (or first duplicate attach attempt), which is the opposite of what the docstring promises.
Two acceptable fixes:
- Set
partial_failure=Trueon bothmutate_campaign_shared_setscalls and surface per-op errors in the response (preferred — matches the docstring, more recoverable for the LLM, and consistent with how Google Ads partial-failure responses work). - Update the docstrings to say "all-or-nothing — call
get_negative_keyword_list_campaignsfirst to verify attachments exist" if you'd rather keep the simple semantics.
Either is fine but the current state is a real bug.
Non-blocking follow-ups (we can handle):
.cursor/rules/adloop.mdc— the "When user asks to add negative keywords" section currently only covers content (creating lists, adding keywords). It should also cover the attachment lifecycle: when to attach vs create new, when to detach. Without rules updates the LLM won't know these tools exist.- Idempotency note: calling
attach_shared_set_to_campaignstwice for the same campaign fails on the second call. Worth surfacing as a known limitation in the docstring. - Detach safety: detaching a list with many keywords from a high-spend campaign is operationally significant. Consider showing the linkage's keyword count in the preview (one extra read), or at least a generic warning. Lower priority.
Once partial_failure=True is in (or the docstring is updated to match), this is good to merge.
AdLoop currently exposes shared-set reads (get_negative_keyword_lists, get_negative_keyword_list_campaigns) and shared-criterion writes (add_to_negative_keyword_list, propose_negative_keyword_list) but no way to attach or detach a shared set from a campaign. The CampaignSharedSet linkage is what makes a shared negative list actually apply to a campaign — without it you have a list with no effect.
Adds two new write tools that follow the existing draft -> confirm_and_apply pattern:
- attach_shared_set_to_campaigns(shared_set_id, campaign_ids) — creates CampaignSharedSet linkages so the campaigns inherit the shared set's criteria.
- detach_shared_set_from_campaigns(shared_set_id, campaign_ids) — removes the linkages. Uses the composite resource name format ({campaign_id}~{shared_set_id}) so no lookup is needed.
Also exposes both via the FastMCP server.
Tests: 10 new unit tests (validation + apply paths) covering required-field rejection, non-numeric ID rejection, deduplication, preview shape, per-campaign op generation, and the composite remove resource name. uv run pytest -- 152/152 pass.
cea1cec to
728ef49
Compare
|
Addressed the blocking change (option 1) — What changed:
Branch was rebased onto current Happy to take the rules-doc update ( |
Summary
Adds two write tools for managing
CampaignSharedSetlinkages — the missing piece between "create a shared negative list" and "actually have it apply to a campaign."attach_shared_set_to_campaigns(shared_set_id, campaign_ids)— createsCampaignSharedSetlinkages so the campaigns inherit the shared set's criteria.detach_shared_set_from_campaigns(shared_set_id, campaign_ids)— removes the linkages. Uses the composite resource name{campaign_id}~{shared_set_id}directly so no lookup is needed.Both follow the existing draft →
confirm_and_applypattern and are exposed via the FastMCP server.Why
AdLoop already covers reads (
get_negative_keyword_lists,get_negative_keyword_list_campaigns) and shared-criterion writes (add_to_negative_keyword_list,propose_negative_keyword_list), but there's no way to attach or detach a shared set from a campaign through the MCP layer.The practical impact: when you build a new campaign and want it to inherit the existing shared negative list, the only options today are (a) bounce out to the Google Ads UI, or (b) drop into
google-ads-pythondirectly. Hit this myself recently — newly-built campaigns launched without inheriting 1800+ keywords of historical waste-blockers because the shared list wasn't attached. Hand-rolled the attach via raw SDK; this PR brings that into AdLoop.Design
Validation lives in a small helper
_normalize_shared_set_attachment_argsshared by both attach and detach drafts:shared_set_idrequired, must be numeric.campaign_idsnon-empty.campaign_idmust be numeric (otherwise rejected with the offending value in the error).Attach apply path uses
CampaignSharedSetService.mutate_campaign_shared_setswith onecreateop per campaign, building paths via the existing service helpers.Detach apply path constructs the composite resource name directly:
No GAQL lookup needed — the
{campaign_id}~{shared_set_id}format is stable.Tests
10 new unit tests in
tests/test_ads_write.py:shared_set_id, empty/non-numericcampaign_ids, dedup with whitespace, preview shape includesplan_id+ correctentity_type/entity_id/changes._apply_attach_shared_set_to_campaignsproduces one op per campaign with correctcreate.campaignandcreate.shared_setpaths;_apply_detach_shared_set_from_campaignsuses the composite{campaign_id}~{shared_set_id}remove path.uv run pytest— all 152 tests pass (142 baseline + 10 new).Test plan
uv run pytest— 152 passRelated
Filed alongside #21 (RSA pinning). Both grew out of the same migration pain point of needing to hand-roll work that should fit cleanly inside AdLoop's existing safety-rail pattern.