Skip to content

feat(demographics): add age/gender/parental/income targeting tools#38

Open
chrmoller wants to merge 4 commits into
kLOsk:mainfrom
chrmoller:feat/demographic-targeting
Open

feat(demographics): add age/gender/parental/income targeting tools#38
chrmoller wants to merge 4 commits into
kLOsk:mainfrom
chrmoller:feat/demographic-targeting

Conversation

@chrmoller
Copy link
Copy Markdown
Contributor

  • get_demographic_targeting (read): list existing demographic criteria on an ad group or campaign, with a composite remove_id ready for remove_entity
  • draft_demographic_targeting (write): propose exclusion (default) or positive demographic criteria. Accepts canonical enums and human-readable aliases (e.g. "25-34", "female", "top-10"). Surfaces warnings for near-total-dimension exclusions and positive narrowing.
  • _apply_add_demographic_criteria: applies AGE_RANGE / GENDER / PARENTAL_STATUS / INCOME_RANGE criteria at ad-group or campaign level
  • remove_entity: accepts ad_group_criterion and campaign_criterion as semantic aliases so removals read sensibly in plans and audit logs
  • Orchestration rules document the bucket mismatch case (e.g. "23-35" has no exact mapping) and the audience-vs-demographic distinction

chrmoller and others added 4 commits May 22, 2026 10:27
- get_demographic_targeting (read): list existing demographic criteria on
  an ad group or campaign, with a composite remove_id ready for remove_entity
- draft_demographic_targeting (write): propose exclusion (default) or
  positive demographic criteria. Accepts canonical enums and human-readable
  aliases (e.g. "25-34", "female", "top-10"). Surfaces warnings for
  near-total-dimension exclusions and positive narrowing.
- _apply_add_demographic_criteria: applies AGE_RANGE / GENDER /
  PARENTAL_STATUS / INCOME_RANGE criteria at ad-group or campaign level
- remove_entity: accepts ad_group_criterion and campaign_criterion as
  semantic aliases so removals read sensibly in plans and audit logs
- Orchestration rules document the bucket mismatch case (e.g. "23-35"
  has no exact mapping) and the audience-vs-demographic distinction

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The draft_demographic_targeting tool used raw list[str] | None for its
four demographic-list parameters, which skipped the project's
JSON-string-to-list coercion (the _StrListOpt validator introduced in
kLOsk#28). MCP clients that pre-serialize list params as JSON strings —
e.g. age_ranges="[\"25-34\"]" — hit Pydantic's list_type error.

Switch all four to _StrListOpt to match the convention used by every
other list-accepting tool in server.py (geo_target_ids, language_ids,
keywords, callouts, image_paths, campaign_ids, etc.). Bare-scalar
inputs like "65+" still fail by design — clients must send a real
array or a JSON-encoded list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same root cause as the draft_demographic_targeting fix
(642d870): the headlines and descriptions params used raw
list[str | dict] instead of going through the project's
JSON-string-to-list coercion validator.

Add a new _StrOrDictList alias (no existing one fits because the
RSA params accept mixed string-or-dict entries for unpinned vs
pinned assets) and apply it. Add a regression test that submits
JSON-encoded list strings — the same failure mode users would
hit with pre-serializing MCP clients.

Audit complete: every other list-accepting MCP tool in server.py
already uses _StrList/_StrListOpt/_DictList/_DictListOpt, so this
is the last remaining instance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The four demographic list params (age_ranges, genders,
parental_statuses, income_ranges) were declared as
`_StrListOpt = None`, which Pydantic encodes as
`{"anyOf": [{"type": "array"}, {"type": "null"}], "default": null}`.

That's spec-correct but some MCP clients only inspect the top-level
`type` field and never traverse `anyOf`, so they see no `type: array`
and refuse to serialize a list. Practical symptom: clients send the
list as a bare string and the Pydantic validator rejects it.

Switch to `_StrList = []` (with the same noqa: B006 comment used by
discover_keywords' seed_keywords). The resulting schema is flat:
`{"type": "array", "items": {...}, "default": []}` — every client
sees `type: array` and serializes correctly. The "at least one
dimension must be non-empty" validation already works with `[]` as
the empty sentinel (no change needed).

This is a targeted fix; the same pattern exists on other tools
(`get_recommendations.recommendation_types`,
`attribution_check.conversion_events`, etc.) and would benefit
from the same cleanup — left for a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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