Repository navigation
ChatGPT Ads: reads, campaign management and conversion diagnostics - #127
illia-sapryga wants to merge 7 commits into
Conversation
|
Thanks for this, Illia. ChatGPT Ads is welcome in the open-source package. Three decisions first, then the review. How we'd like to land it
Must fix
Should fix
Before merge |
01a3a79 to
0d2b24b
Compare
Microsoft Advertising (Bing) through the REST API v13: Entra sign-in (authorization code + PKCE, device code as fallback), account, campaign, ad group, keyword, ad and extension reads, performance, search-term and location reports, the conversion setup audit, and campaign management behind the draft -> preview -> confirm_and_apply gate: budgets, status, campaigns and targeting, ad groups, keywords and bids, negatives and shared lists, RSAs, sitelinks, callouts, snippets, call and image extensions, and removal. Rebased onto the shared write-gate change instead of kLOsk#127.
|
Thanks, Daniel. All done; the branch is force-pushed. Landing
Must fix
Should fix
Before merge
Full suite: 2162 passed, 2 skipped. |
* Write gate: re-check blocked operations at apply, retire partial applies, shared draft helpers - confirm_and_apply refuses a plan whose operation was blocked after the draft (BLOCKED, dry run and apply alike), logs the refusal, and keeps the plan pending. The confirmation dialog is not shown for such a plan. - safety/outcomes.py: PartialApplyError. An applier raises it when part of a plan reached the account or a create's outcome is unknown; confirm_and_apply retires the plan and answers PARTIAL_APPLY or UNKNOWN_OUTCOME with what was created. - safety/guards.py: blocked_operation_error, store_draft and blocked_plan_refusal, used by Reddit and Tag Manager instead of their own copies. - Approval card: Microsoft Advertising and ChatGPT Ads labels, as in the confirmation dialog. * confirm_and_apply: the docstring names BLOCKED, PARTIAL_APPLY and UNKNOWN_OUTCOME * Microsoft Advertising: reads, reports and campaign management Microsoft Advertising (Bing) through the REST API v13: Entra sign-in (authorization code + PKCE, device code as fallback), account, campaign, ad group, keyword, ad and extension reads, performance, search-term and location reports, the conversion setup audit, and campaign management behind the draft -> preview -> confirm_and_apply gate: budgets, status, campaigns and targeting, ad groups, keywords and bids, negatives and shared lists, RSAs, sitelinks, callouts, snippets, call and image extensions, and removal. Rebased onto the shared write-gate change instead of #127. * Microsoft Advertising: review fixes - Broad match only with conversion-based bidding (MaxConversions, MaxConversionValue, TargetCpa, TargetRoas); Maximize Clicks no longer counts. Moving to any other strategy is refused while active broad keywords exist, at the draft and again at the dry run and apply; the keyword read's insight follows the same rule. - remove_microsoft_entity reads a campaign criterion at the draft, names its type and target, and refuses removing the last targeted location (the campaign would serve everywhere), again at the dry run. - Request bodies, snapshots and ids move from plan.changes to ChangePlan.apply_only_payload; preflight and apply read plan.apply_payload(). Drafts use the shared store_draft and blocked_operation_error. - MaxConversions without an active conversion goal is refused at the draft and the dry run (Microsoft refused such a create live with ConversionGoalCriteriaNotMetForBiddingScheme) and names the alternatives. - Throttling: codes 117 and 207 (and HTTP 429) raise at once with retry_after 60 instead of retrying after 5 s. - Plans store their campaign and ad group, so dry runs and applies read three lists instead of every ad group of the account; campaign reads ask once per campaign for its targeting (CriterionType Targets, with the per-type queries as fallback). - Reads: criterion type Radius, Audience Network inherited from the campaign when an ad group has no adjustment of its own; new campaigns warn about the Audience Network default. - Sign-in binds a free localhost port (Entra ignores the port), keeps listening until the state matches, and writes the token through an O_EXCL temp file. A bare 105 after a working sign-in is explained: the identity is not a user of the account, often because its users sign in with Google, which is not supported yet. - languages=['All'] is refused; target_cpa and max_cpc on a strategy that cannot use them are refused instead of dropped. - Dead code removed; the schedule limit is 7 windows a day throughout. * Microsoft Advertising: split manage.py into per-area handlers manage.py (2.4k lines, two parallel ~20-branch dispatchers) becomes the package microsoft_ads/manage: _common (constants, request and plan helpers, bidding, Step and Handler), campaigns, targeting, ad_groups, negatives, ads, extensions and removal. Each area drafts its plans and registers its operations in HANDLERS (operation -> dry-run check, apply); the package merges them into one table that preflight and apply_plan look up. The code of every check and apply moved unchanged; imports from adloop.microsoft_ads.manage keep working. * Microsoft Advertising: campaign reads ask for their criteria types in one query Found in the live run: CampaignCriterions/QueryByIds refuses the "Targets" shorthand (4503 CampaignCriterionTypeInvalid, "Invalid API Campaign Criterion Type : Targets on API tier"), which adds, updates and deletes accept. The types joined by commas ("Location,LocationIntent,Radius") are accepted, so get_microsoft_campaigns now makes one criterion call per campaign; the per-type fallback stays for a refusal. The test fakes refuse "Targets" in a query, as Microsoft does.
|
Thanks, Illia. #134 and #130 are merged, so this one now conflicts with main where both platforms plug in: While you're at it, please add The review fixes look right from what I've read so far. I'll do a last pass after the rebase, then merge. |
ChatGPT Ads (OpenAI Ads Manager) through the OpenAI Advertiser API: account, campaign, ad group and ad reads, performance and conversions, the conversion setup audit, the GA4 attribution check, and paused-creating campaign, ad group and ad writes behind the draft -> preview -> confirm_and_apply gate. Rebased onto the shared write-gate change; the blocked-operation re-check and the approval display now come from there.
- Payloads: the request, the snapshot, the account selector and the image
pin move to ChangePlan.apply_only_payload. changes keep only what a person
approves (empty values dropped; updates show before/after under
"display"). Drafts use safety.guards.store_draft and
blocked_operation_error; _Refusal, _guard, _store copies,
is_chatgpt_plan and _IMAGE_TYPES are gone. A plan store that drops the
payload makes the apply refuse instead of sending an empty request.
- Budgets: changes are judged on daily equivalents. Lifetime 500 over 31
days to daily 100 is about 6.2x the daily spend: double confirmation and
a warning that says so. Shortening a lifetime flight counts as a raise.
- The dry run and the apply read the target (or the parent of a create)
with GET /campaigns|ad_groups|ads/{id}; the account scan stays for the
parent names a draft shows.
- Images: the draft pins the sha256 of the bytes fetched through the
public-only fetcher (or read from the file); the dry run and the apply
read them again and refuse different bytes, and the apply uploads exactly
the checked bytes as multipart, never the URL.
- Lost answers: a timeout, dropped connection or 5xx on a write raises
ChatGPTAdsOutcomeUnknown ("whether it changed anything is unknown").
Reads and requests that never connected say nothing changed. The dry
run recognises fields already at the planned value, so a retry sends
nothing (or only what is still missing); a create retry resends the same
Idempotency-Key and body.
- Change detection compares only the subfields the plan sends (budget
limits, sent bidding_config keys with normalised multipliers, sent
creative keys, targeting in its writable form); response-only fields
such as creative.image_url no longer block a dry run.
- Audience multipliers: [] is sent only to clear existing ones when an ad
group moves to automatic bidding (as OpenAI's bidding guide does);
otherwise the field is left out.
- Micros conversions are the Reddit client's from_micro / to_micro.
- Context hints: previews list 20 and count the rest; get_chatgpt_ad_groups
lists 50 per ad group with context_hint_count and has a compact mode.
- chatgpt_attribution_check carries the ga4 tag too (CROSS_PLATFORM_TOOLS
in server.py, the one toolset tag rule allows it); the confirm_and_apply
docstring describes ChatGPT plans.
- A ChatGPT-only hosted-provider health test.
- Rules, README and tool descriptions claim what OpenAI documents about
Idempotency-Key: the existing object comes back when a matching prior
create is visible.
Found in the first live write: OpenAI's spec allows daily_spend_limit_micros from 1,000,000, but POST /campaigns answers 400 "Daily campaign budget must be at least 25000000 micros". The draft (and the update) now refuse it, so a dry run cannot pass a create the apply cannot make.
…y hides parent names Found in the live run: a campaign created a moment earlier was missing from GET /campaigns, so drafting its ad group answered "not found". find_entity now reads the entity by id and scans the lists only for its parents' names; when the scan misses, the draft says what it could not check. An ad group update, which needs its campaign's bidding, asks to retry in a few minutes.
Adds chatgpt_ads to _PLATFORM_SETUP, next to Reddit and Microsoft Advertising: without a key in chatgpt_ads.api_key, chatgpt_ads.accounts or OPENAI_ADS_API_KEY, the local server keeps the ChatGPT Ads tools out of tools/list, and health_check names the key to add under hidden_toolsets. - The check looks where the client looks for a key (local_api_key), so a blank OPENAI_ADS_API_KEY is no key. health_check's own check uses it too. - chatgpt_attribution_check carries ga4 as well. A hidden toolset hides every tool that carries it, so the cross-check leaves with the ChatGPT tools and the GA4 tools stay. - The hiding tests clear OPENAI_ADS_API_KEY, so a key in the developer's environment doesn't change what they see.
As for Microsoft Advertising: "chatgpt_ads": "not_configured" appears only when the caller is offered the ChatGPT Ads tools. A hosted runtime that doesn't offer them no longer sends its users to set them up, and a local server that hid them says so once, under hidden_toolsets. Hosted, the key is read only where the runtime offers the tools, so a workspace without ChatGPT Ads gets no ChatGPT Ads error.
Cloud does not offer the community-contributed ChatGPT Ads tools, so the sync drops their section and the two ChatGPT Ads orchestration patterns, and takes ChatGPT Ads out of the health_check line, as it does for Microsoft Advertising. Without this the sync stopped on the changed health_check line, and the section's config instructions have no Cloud rewrite. The Cloud files generated from these rules are the same as from main's.
0d2b24b to
448b718
Compare
|
Thanks, Daniel. Rebased onto main (0.41.2). ChatGPT Ads now sits next to Microsoft in
The full suite passes locally (2,423 tests), and the description is updated. |
|
Thanks, Illia. The rebase is clean against main (0.41.4), and the earlier points check out in the code: the apply-only payload, the image checksum, the unknown-outcome handling, the account pin at apply, and the tools hidden without a key. Two things from the last pass, one of them before merge. Must fix
Should fix
#121 and #122 are merged, thanks for both. They touch |
Rebased onto main (0.41.2, after #134 and #130). The commits are the ChatGPT Ads module, the review fixes, two fixes from the live run, then
_PLATFORM_SETUP, health_check and plugin-sync commits that match what main does for Microsoft.ChatGPT ads went self-serve in OpenAI Ads Manager, and OpenAI published an Advertiser API for it. This adds it as a new toolset,
chatgpt_ads, on the same draft → preview →confirm_and_applygate as Google Ads and Reddit.Reads: account (reviews, spending limits, account-level blockers), campaigns, ad groups, ads, performance with breakdowns and compact mode, conversions (click-through and view-through, plus every attributed event), a conversion-setup audit with a verdict, change history,
chatgpt_attribution_checkagainst GA4, and location search.Writes: pause / enable / archive,
update_chatgpt_campaign/_ad_group/_ad, anddraft_chatgpt_campaign/_ad_group/_ad. New entities are created paused.Connection. An Advertiser API key from Ads Manager → Settings → API keys. There's no OAuth and no developer token. Each key belongs to exactly one ad account, so
ad_account_idon a tool only picks which configured key to use. Config ischatgpt_ads.api_key(orOPENAI_ADS_API_KEY) plus an optionalchatgpt_ads.accountsmap for agencies.CredentialsProvidergainschatgpt_ads_api_key(). A hosted provider without it doesn't get the tools, andhealth_checkleaveschatgpt_adsout there. A local server without a key hides the tools (_PLATFORM_SETUP).The gate. OpenAI has no validate-only mode for single resources, so the dry run works like Reddit's. It re-reads the target and refuses when a field the plan changes was edited since the preview. It also re-checks:
max_daily_budget, with a lifetime budget judged by its daily equivalent over the flight, re-computed whenever the budget or either date changesmax_bid_increase_pctThe real apply runs the same checks first, reading the target by id. Creates send an
Idempotency-Key, which OpenAI documents on POST /campaigns, /ad_groups and /ads: "If a matching prior create is visible, the existing object is returned instead of creating a duplicate". Live, replaying each create with the same key and body returned the same object. Images are fetched throughimage_fetch, so the public-only and redirect checks apply. The draft pins their sha256, and the apply uploads exactly those bytes. A new ad's file id is kept in the plan, so a retry after a lost response sends the same request.Shared changes are in #134 now: the
blocked_operationsre-check inconfirm_and_applyand the shared draft helpers. The name-based display hiding is gone. Request payloads, snapshots and the image pin live inChangePlan.apply_only_payload, sochangesholds only what a person approves.Cloud contract.
TOOLSETSgainschatgpt_ads, soconfig/toolsets.phpand the runtime filter need the slug too. I updatedtest_toolset_slugs_match_the_cross_repo_contract.Review fixes (801acd3, plus two found in the live run: 236bc01 and c97694d) are listed in the review reply below. The live transcript of every write is linked there.
What it does not do:
adloop initstep. The key goes in the config by hand, as the README says.Tested with the full suite plus
tests/test_chatgpt_ads.pyandtests/test_chatgpt_ads_write.py. The write tests drive the real gate against a stateful fake of the Advertiser API. It enforces the contract details: whole-object updates, status endpoints, and idempotency keys that must come back with the same body. The tests cover lost responses after a successful create, edits made between preview and apply, a key that moves to another account, and private image URLs. Every write tool has also run live against a real ad account: create, update, enable, pause and archive for campaigns, ad groups and ads. The redacted transcript is linked in the review reply.I also changed the rules files (a tool section and two orchestration patterns), the README and
config.yaml.example. I didn't regeneratereference/tools.mdxin the docs repo.