Skip to content

Resolve B2B targeting facets into LinkedIn targetingCriteria URNs - #25

Merged
th0rz05 merged 1 commit into
mainfrom
feat/linkedin-b2b-targeting-resolver
Jun 3, 2026
Merged

Resolve B2B targeting facets into LinkedIn targetingCriteria URNs#25
th0rz05 merged 1 commit into
mainfrom
feat/linkedin-b2b-targeting-resolver

Conversation

@th0rz05

@th0rz05 th0rz05 commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds TargetingResolver, which turns the LLM-extracted free-text B2B facet names from a brief (seniorities, job functions, industries, titles, skills, company sizes) into the entity URNs LinkedIn's targetingCriteria requires.
  • Picks a resolution path per facet based on what the API actually exposes:
    • Closed enums (seniorities, jobFunctions) — no typeahead finder; resolved against the standardized /seniorities and /functions lists by display name. Lists are fetched once and cached per publish.
    • Open taxonomies (industries, titles, skills) — resolved via the typeahead finder, preferring an exact normalized name match, else LinkedIn's top-ranked hit.
    • company_sizes — mapped statically to urn:li:staffCountRange:(min,max) tuple URNs (not the SIZE_* enum the facet listing returns; the top bucket uses INT_MAX as its upper bound).
    • locations — from the static ISO→geo map.
  • No-guess contract: any name that resolves to no real LinkedIn URN is surfaced in ResolvedTargeting.unresolved (caller flags it as a manual Campaign Manager step) and is never fabricated into a URN.
  • Wires the resolver into the publish flow (server.py), replacing the static geo-only targeting. mapping.audience_to_targeting is trimmed to the geo-only baseline it now is, retained for inspection/tests.
  • Adds three read-only client methods: typeahead_targeting_entities, list_seniorities, list_functions.

Verification

  • 41 unit tests pass; ruff clean on touched files.
  • Verified live against a DRAFT campaign: the full resolved targetingCriteria (staffCountRange tuple URNs + seniorities, jobFunctions, industries, skills, locations) was accepted by LinkedIn and read back from the API. All test resources and a prior-session orphan were deleted; account confirmed clean. Nothing can spend — everything stayed DRAFT.

Test plan

  • pytest -q green
  • ruff check clean
  • Live DRAFT publish accepted by LinkedIn + torn down

An LLM extracts free-text facet names from a brief (seniorities, job
functions, industries, titles, skills, company sizes); LinkedIn's
targetingCriteria needs entity URNs. TargetingResolver bridges the two,
choosing a resolution path per facet from what the API exposes:

  * closed enums (seniorities, jobFunctions) come from the standardized
    /seniorities and /functions endpoints, matched by display name;
  * open taxonomies (industries, titles, skills) resolve via the
    typeahead finder, preferring an exact name match else the top hit;
  * company_sizes map statically to staffCountRange tuple URNs
    (urn:li:staffCountRange:(min,max) — not the SIZE_* enum the facet
    listing returns);
  * locations come from the static ISO geo map.

Names that match no real LinkedIn URN are surfaced in
ResolvedTargeting.unresolved for a manual Campaign Manager step, never
fabricated. Verified live against a DRAFT campaign and torn down.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 2, 2026 16:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end resolution of free-text B2B audience facets into LinkedIn targetingCriteria URNs, and wires that resolver into the LinkedIn publish flow while preserving the “no-guess” contract (unmatched values are surfaced, not fabricated).

Changes:

  • Introduces TargetingResolver to resolve seniorities/job functions via standardized lists and industries/titles/skills via typeahead, plus static staffCountRange and ISO→geo mapping.
  • Updates LinkedIn publish flow to use resolved targetingCriteria and report unresolved facet values as publish notes.
  • Adds LinkedIn client helpers for typeahead + standardized list endpoints and a focused integration test suite for resolver behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/integrations/test_linkedin_targeting.py Adds resolver integration tests for enum vs typeahead paths, no-guess behavior, staffCountRange mapping, and caching.
src/yieldagent/integrations/linkedin/targeting.py New resolver module that converts Audience facets into LinkedIn targeting URNs with unresolved surfacing.
src/yieldagent/integrations/linkedin/server.py Switches publish flow to use TargetingResolver output and updates unresolved targeting notes.
src/yieldagent/integrations/linkedin/mapping.py Trims audience_to_targeting down to the geo-only baseline and updates its docstring accordingly.
src/yieldagent/integrations/linkedin/client.py Adds read-only client methods for typeahead targeting entities and standardized seniority/function taxonomies.
briefs/linkedin_live_test_brief.md Adds a live-test campaign brief used for manual verification/reference.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@th0rz05
th0rz05 merged commit 4e72e8f into main Jun 3, 2026
1 check passed
@th0rz05
th0rz05 deleted the feat/linkedin-b2b-targeting-resolver branch June 3, 2026 10:55
th0rz05 added a commit that referenced this pull request Jun 9, 2026
An LLM extracts free-text facet names from a brief (seniorities, job
functions, industries, titles, skills, company sizes); LinkedIn's
targetingCriteria needs entity URNs. TargetingResolver bridges the two,
choosing a resolution path per facet from what the API exposes:

  * closed enums (seniorities, jobFunctions) come from the standardized
    /seniorities and /functions endpoints, matched by display name;
  * open taxonomies (industries, titles, skills) resolve via the
    typeahead finder, preferring an exact name match else the top hit;
  * company_sizes map statically to staffCountRange tuple URNs
    (urn:li:staffCountRange:(min,max) — not the SIZE_* enum the facet
    listing returns);
  * locations come from the static ISO geo map.

Names that match no real LinkedIn URN are surfaced in
ResolvedTargeting.unresolved for a manual Campaign Manager step, never
fabricated. Verified live against a DRAFT campaign and torn down.
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.

2 participants