Resolve geos via typeahead; drop dead targeting/payload helpers - #26
Merged
Conversation
Geo targeting previously relied on a hardcoded ~15-country ISO->URN map, so
any country outside it (e.g. PT) was silently dropped. Replace it with the
same typeahead path the other open facets use: expand each ISO 3166-1 alpha-2
code to a country name via pycountry, resolve it through the locations
typeahead, and surface codes that don't resolve under `unresolved["geos"]`
instead of dropping them. Geo logic now lives only in TargetingResolver.
Also remove confirmed dead code uncovered while unifying this:
* linkedin mapping.audience_to_targeting + line_item_payload (only the dead
payload helper used the geo-only baseline; the publish flow uses
TargetingResolver) and the now-unused ISO_TO_LINKEDIN_GEO_URN map;
* meta mapping.line_item_payload (never called);
* the write-only `rejection_reason` AgentState field (the reason is already
captured in the audit entry).
line_item_locale now validates the country via pycountry instead of the
deleted map.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes LinkedIn geo targeting silently dropping non-hardcoded ISO country codes by resolving geos through the same locations typeahead flow used for other open facets (ISO code → country name via pycountry → typeahead → URN), and removes dead/zero-reference targeting/payload helpers uncovered during the cleanup.
Changes:
- Resolve LinkedIn
Audience.geosvia locations typeahead (with a US fallback URN) and surface unknown/invalid codes underunresolved["geos"]. - Remove dead mapping/payload helpers (LinkedIn + Meta) and drop the hardcoded LinkedIn ISO→geo URN map; remove the write-only
rejection_reasonfrom campaign setup state. - Add
pycountryto thelinkedinoptional dependency extra and extend integration tests to pin the new geo-resolution behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/integrations/test_linkedin_targeting.py | Adds coverage for geo resolution via typeahead by country name and for surfacing unknown ISO codes as unresolved. |
| tests/integrations/test_linkedin_publish.py | Updates the publish test client stub to support typeahead targeting lookups used by TargetingResolver. |
| src/yieldagent/integrations/meta/mapping.py | Removes unused line_item_payload helper and cleans up imports. |
| src/yieldagent/integrations/linkedin/targeting.py | Implements geo resolution via pycountry + locations typeahead, adds DEFAULT_GEO_URN, and reports unresolved geo codes. |
| src/yieldagent/integrations/linkedin/mapping.py | Removes static geo map + dead payload helpers; updates line_item_locale to validate ISO codes via pycountry. |
| src/yieldagent/agents/campaign_setup/state.py | Removes rejection_reason from AgentState (write-only field). |
| src/yieldagent/agents/campaign_setup/nodes.py | Stops writing rejection_reason in human_gate output. |
| pyproject.toml | Adds pycountry dependency to the linkedin extra. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+76
to
78
| country = audience.geos[0].upper() if audience.geos else "US" | ||
| if pycountry.countries.get(alpha_2=country) is None: | ||
| country = "US" |
Comment on lines
+196
to
+208
| for code in audience.geos: | ||
| name = _country_name(code) | ||
| urn = None | ||
| if name: | ||
| hits = await self._client.typeahead_targeting_entities( | ||
| facet=FACET_LOCATIONS, query=name | ||
| ) | ||
| urn = _best_typeahead_match(name, hits) | ||
| if urn: | ||
| urns.append(urn) | ||
| else: | ||
| unresolved.append(code) | ||
| return urns or [DEFAULT_GEO_URN], unresolved |
th0rz05
added a commit
that referenced
this pull request
Jun 3, 2026
Copilot flagged that geo handling normalized inconsistently:
* line_item_locale uppercased but didn't strip, so a brief geo like "PT "
failed the pycountry lookup and silently fell back to US, even though the
targeting resolver (which strips) resolved it fine;
* _resolve_geos recorded unresolved entries using the raw padded/lowercased
input rather than the normalized code.
Normalize each geo code once with strip().upper() and use that form for the
lookup, the locale, and the unresolved report. Adds tests for whitespace/case
tolerance in both resolution and locale, and that unresolved codes surface
normalized.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
th0rz05
added a commit
that referenced
this pull request
Jun 9, 2026
Geo targeting previously relied on a hardcoded ~15-country ISO->URN map, so
any country outside it (e.g. PT) was silently dropped. Replace it with the
same typeahead path the other open facets use: expand each ISO 3166-1 alpha-2
code to a country name via pycountry, resolve it through the locations
typeahead, and surface codes that don't resolve under `unresolved["geos"]`
instead of dropping them. Geo logic now lives only in TargetingResolver.
Also remove confirmed dead code uncovered while unifying this:
* linkedin mapping.audience_to_targeting + line_item_payload (only the dead
payload helper used the geo-only baseline; the publish flow uses
TargetingResolver) and the now-unused ISO_TO_LINKEDIN_GEO_URN map;
* meta mapping.line_item_payload (never called);
* the write-only `rejection_reason` AgentState field (the reason is already
captured in the audit entry).
line_item_locale now validates the country via pycountry instead of the
deleted map.
th0rz05
added a commit
that referenced
this pull request
Jun 9, 2026
Copilot flagged that geo handling normalized inconsistently:
* line_item_locale uppercased but didn't strip, so a brief geo like "PT "
failed the pycountry lookup and silently fell back to US, even though the
targeting resolver (which strips) resolved it fine;
* _resolve_geos recorded unresolved entries using the raw padded/lowercased
input rather than the normalized code.
Normalize each geo code once with strip().upper() and use that form for the
lookup, the locale, and the unresolved report. Adds tests for whitespace/case
tolerance in both resolution and locale, and that unresolved codes surface
normalized.
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
Fixes the silent geo-drop gap and removes dead code uncovered while unifying geo resolution.
Geo resolution (the PT bug): previously geos were matched against a hardcoded ~15-country
ISO_TO_LINKEDIN_GEO_URNmap, so any country outside it (e.g.PT) was silently dropped from targeting. Now geos use the same typeahead path as the other open facets:pycountry(preferscommon_name, e.g. "South Korea"),locationstypeahead finder,unresolved["geos"]instead of dropping them.This also de-duplicates geo logic — it previously lived in both
mapping.audience_to_targetingandtargeting._geo_urns; now it lives only inTargetingResolver. Any country works, not just a shortlist.Dead code removed (all confirmed zero-reference):
linkedin/mapping.py:audience_to_targeting+line_item_payload(only the dead payload helper used the geo-only baseline; the publish flow usesTargetingResolver) and the now-unusedISO_TO_LINKEDIN_GEO_URNmap.meta/mapping.py:line_item_payload(never called).campaign_setup: the write-onlyrejection_reasonAgentStatefield (the reason is already captured in the audit entry).line_item_localenow validates the country viapycountryinstead of the deleted map.Net effect
+96 / −114 lines. Adds
pycountryto thelinkedinextra.Verification
US+PT→ both URNs) and the no-guess contract for unknown codes (ZZ→unresolved["geos"], falls back to default location)."Portugal"→urn:li:geo:100364837(exact match over "Porto, Portugal"), and"United States"/"United Kingdom"resolve to the same URNs the old map hardcoded.UP017/E501warnings are untouched — a dedicated lint-clean PR is the next cleanup item.)Test plan
pytest -qgreen (43)ruff checkclean on changed code