thoughtleaders-cli 0.5.2__tar.gz → 0.6.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/AGENTS.md +3 -1
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/PKG-INFO +22 -8
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/README.md +21 -7
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/commands/tl.md +2 -2
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/docs/architecture.md +6 -1
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/skills/tl/SKILL.md +80 -40
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/skills/tl/references/firebolt-schema.md +6 -5
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/__init__.py +1 -1
- thoughtleaders_cli-0.6.1/src/tl_cli/commands/recommender.py +314 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/main.py +2 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/uv.lock +2 -2
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/.gitignore +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/LICENSE +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/commands/tl-balance.md +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/commands/tl-reports.md +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/commands/tl-sponsorships.md +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/skills/tl/references/postgres-schema.md +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/ask.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/channels.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/comments.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/reports.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/tests/test_sponsorships.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Project Overview
|
|
2
2
|
|
|
3
|
-
**tl-cli** is a Python CLI for querying ThoughtLeaders sponsorship data (sponsorships, channels, brands, uploads, snapshots, reports). Built with Typer + Rich + httpx. Designed as an "agent-first tool" — the CLI handles structured commands and output, while the user's AI agent (Claude) provides intelligence.
|
|
3
|
+
**tl-cli** is a Python CLI for querying ThoughtLeaders sponsorship data (sponsorships, channels, brands, uploads, snapshots, reports, vector recommender). Built with Typer + Rich + httpx. Designed as an "agent-first tool" — the CLI handles structured commands and output, while the user's AI agent (Claude) provides intelligence.
|
|
4
4
|
|
|
5
5
|
# Architecture
|
|
6
6
|
|
|
@@ -20,6 +20,8 @@ When adding a new data command, follow this pattern. See `sponsorships.py` for t
|
|
|
20
20
|
|
|
21
21
|
`deals`, `matches`, and `proposals` are shortcut commands that delegate to sponsorships' `do_list`/`do_show`/`do_create` with a pre-set status filter. They reject explicit `status:` filters — users should use `tl sponsorships list` for finer-grained status filtering.
|
|
22
22
|
|
|
23
|
+
`recommender` (`commands/recommender.py`) wraps the vector-recommender API at `/api/cli/v1/recommender/*` — `tags` (free), `top-channels` / `top-profiles` / `top-brands`, `inspect-channel`, `inspect-brand`, `similar-to-profile` (all 50 credits flat, Intelligence-gated). The three `top-*` URLs share one server resolver; `top-brands` dedupes the underlying profile rows by brand. Channel→channel and brand→brand similarity stay on `tl channels similar` / `tl brands similar`. When updating the SKILL or examples, prefer steering category/topic discovery (e.g. "Cooking channels") to `tl recommender top-channels "<tag>"` rather than `WHERE content_category = <code>` SQL — the recommender is ranked, not equality-based. The underlying recommender code uses "element"/"field_name" terminology; the CLI/API layer renames these to "tag" at the boundary.
|
|
24
|
+
|
|
23
25
|
## Filter Parsing (`filters.py`)
|
|
24
26
|
|
|
25
27
|
`parse_filters()` handles `key:value` and `key:"quoted value"` syntax. Returns `dict[str, str]` passed as query params. Date filter keys (listed in `DATE_FILTER_KEYS` — e.g. `since`, `created-at`, `created-at-start`, `publish-date-end`) accept keywords `today`, `yesterday`, `tomorrow`. Sponsorship date fields (`created-at`, `publish-date`, `purchase-date`, `send-date`) each expose three filter shapes: bare `<field>:<date>` matches within that date/period, and `<field>-start:` / `<field>-end:` give inclusive lower/upper bounds (both sides inclusive; partial dates expand to the whole period). Empty-string values result in `IS NULL` queries on the backend.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence
|
|
5
5
|
Project-URL: Homepage, https://thoughtleaders.io
|
|
6
6
|
Project-URL: Repository, https://github.com/ThoughtLeaders-io/thoughtleaders-cli
|
|
@@ -81,13 +81,16 @@ tl uploads list q:code --csv
|
|
|
81
81
|
# Show upload details (supports colon-containing IDs)
|
|
82
82
|
tl uploads show 1174310:0BehkmVa7ak
|
|
83
83
|
|
|
84
|
-
# Search channels
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
tl
|
|
90
|
-
|
|
84
|
+
# Search channels via raw SQL — `tl db pg` against thoughtleaders_channel
|
|
85
|
+
# (run `tl schema pg` once to confirm the live column set).
|
|
86
|
+
tl db pg "SELECT id, channel_name, total_views FROM thoughtleaders_channel
|
|
87
|
+
WHERE content_category = <COOKING_CODE> AND total_views >= 100000
|
|
88
|
+
ORDER BY total_views DESC LIMIT 50 OFFSET 0"
|
|
89
|
+
tl db pg "SELECT id, channel_name FROM thoughtleaders_channel
|
|
90
|
+
WHERE is_tl_channel = TRUE LIMIT 200 OFFSET 0" # all TPP channels (~169)
|
|
91
|
+
# MSN status (media_selling_network_join_date) is scrubbed from the
|
|
92
|
+
# advertiser sandbox view — for MSN-only / non-MSN lookups, the
|
|
93
|
+
# structured filter is the right tool: `tl channels list msn:yes|no`.
|
|
91
94
|
|
|
92
95
|
# Show channel detail — accepts numeric ID or channel name.
|
|
93
96
|
# Names that match more than one active channel print a candidate list
|
|
@@ -102,6 +105,17 @@ tl channels show "Economics Explained"
|
|
|
102
105
|
tl channels similar 12345 --limit 10
|
|
103
106
|
tl channels similar "Tremending girls" min-score:0.85 --limit 5
|
|
104
107
|
|
|
108
|
+
# Vector recommender — discovery by category/demographic tag (Intelligence plan).
|
|
109
|
+
# `tags` is free; `top`, `inspect-*`, and `similar-to-profile` cost 50 credits flat.
|
|
110
|
+
tl recommender tags # List every tag (free)
|
|
111
|
+
tl recommender tags cooking # Search tag names by substring
|
|
112
|
+
tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels for a tag
|
|
113
|
+
tl recommender top-profiles "Cooking" mbn:yes --limit 30 # Top brand profiles (one brand → potentially multiple profiles)
|
|
114
|
+
tl recommender top-brands "Cooking" --limit 30 # Top brands (deduped from profiles)
|
|
115
|
+
tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
|
|
116
|
+
tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal vector
|
|
117
|
+
tl recommender similar-to-profile 842 # Channels closest to a brand profile
|
|
118
|
+
|
|
105
119
|
# Brand intelligence
|
|
106
120
|
tl brands show Nike
|
|
107
121
|
|
|
@@ -54,13 +54,16 @@ tl uploads list q:code --csv
|
|
|
54
54
|
# Show upload details (supports colon-containing IDs)
|
|
55
55
|
tl uploads show 1174310:0BehkmVa7ak
|
|
56
56
|
|
|
57
|
-
# Search channels
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
tl
|
|
63
|
-
|
|
57
|
+
# Search channels via raw SQL — `tl db pg` against thoughtleaders_channel
|
|
58
|
+
# (run `tl schema pg` once to confirm the live column set).
|
|
59
|
+
tl db pg "SELECT id, channel_name, total_views FROM thoughtleaders_channel
|
|
60
|
+
WHERE content_category = <COOKING_CODE> AND total_views >= 100000
|
|
61
|
+
ORDER BY total_views DESC LIMIT 50 OFFSET 0"
|
|
62
|
+
tl db pg "SELECT id, channel_name FROM thoughtleaders_channel
|
|
63
|
+
WHERE is_tl_channel = TRUE LIMIT 200 OFFSET 0" # all TPP channels (~169)
|
|
64
|
+
# MSN status (media_selling_network_join_date) is scrubbed from the
|
|
65
|
+
# advertiser sandbox view — for MSN-only / non-MSN lookups, the
|
|
66
|
+
# structured filter is the right tool: `tl channels list msn:yes|no`.
|
|
64
67
|
|
|
65
68
|
# Show channel detail — accepts numeric ID or channel name.
|
|
66
69
|
# Names that match more than one active channel print a candidate list
|
|
@@ -75,6 +78,17 @@ tl channels show "Economics Explained"
|
|
|
75
78
|
tl channels similar 12345 --limit 10
|
|
76
79
|
tl channels similar "Tremending girls" min-score:0.85 --limit 5
|
|
77
80
|
|
|
81
|
+
# Vector recommender — discovery by category/demographic tag (Intelligence plan).
|
|
82
|
+
# `tags` is free; `top`, `inspect-*`, and `similar-to-profile` cost 50 credits flat.
|
|
83
|
+
tl recommender tags # List every tag (free)
|
|
84
|
+
tl recommender tags cooking # Search tag names by substring
|
|
85
|
+
tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels for a tag
|
|
86
|
+
tl recommender top-profiles "Cooking" mbn:yes --limit 30 # Top brand profiles (one brand → potentially multiple profiles)
|
|
87
|
+
tl recommender top-brands "Cooking" --limit 30 # Top brands (deduped from profiles)
|
|
88
|
+
tl recommender inspect-channel 12345 # Per-tag breakdown of a channel's vector
|
|
89
|
+
tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal vector
|
|
90
|
+
tl recommender similar-to-profile 842 # Channels closest to a brand profile
|
|
91
|
+
|
|
78
92
|
# Brand intelligence
|
|
79
93
|
tl brands show Nike
|
|
80
94
|
|
|
@@ -18,8 +18,8 @@ The user wants to query ThoughtLeaders data. Translate their request into the ri
|
|
|
18
18
|
## Examples
|
|
19
19
|
|
|
20
20
|
- "/tl sold sponsorships for Nike in Q1" → `tl sponsorships list status:sold brand:"Nike" purchase-date-start:2026-01-01 purchase-date-end:2026-03-31`
|
|
21
|
-
- "/tl cooking channels over 100k subs" → `tl
|
|
22
|
-
- "/tl mobile-first US cooking channels" → `tl
|
|
21
|
+
- "/tl cooking channels over 100k subs" → `tl db pg "SELECT id, channel_name, total_views FROM thoughtleaders_channel WHERE content_category = <COOKING_CODE> AND total_views >= 100000 ORDER BY total_views DESC LIMIT 50 OFFSET 0"`
|
|
22
|
+
- "/tl mobile-first US cooking channels" → `tl db pg "SELECT id, channel_name, demographic_usa_share FROM thoughtleaders_channel WHERE content_category = <COOKING_CODE> AND demographic_device_primary = 'mobile' AND demographic_usa_share >= 50 ORDER BY total_views DESC LIMIT 50 OFFSET 0"`
|
|
23
23
|
- "/tl Nike's sponsorship activity" → `tl brands show Nike`
|
|
24
24
|
- "/tl run my Q1 report" → `tl reports --json` then `tl reports run <id>`
|
|
25
25
|
- "/tl check my balance" → `tl balance`
|
|
@@ -75,7 +75,12 @@ Filters are passed as `key:value` pairs after `list`:
|
|
|
75
75
|
tl sponsorships list status:sold brand:"Nike" purchase-date:2026-01
|
|
76
76
|
tl sponsorships list status:pending send-date:2026-03
|
|
77
77
|
tl uploads list channel:12345 type:longform since:2026-03
|
|
78
|
-
tl channels list
|
|
78
|
+
# Channel discovery is raw SQL — the structured `tl channels list` covers
|
|
79
|
+
# only narrow lookups. Default to:
|
|
80
|
+
tl db pg "SELECT id, channel_name, total_views FROM thoughtleaders_channel
|
|
81
|
+
WHERE content_category = <COOKING_CODE> AND language = 'en'
|
|
82
|
+
AND total_views >= 1000000
|
|
83
|
+
ORDER BY total_views DESC LIMIT 50 OFFSET 0"
|
|
79
84
|
```
|
|
80
85
|
|
|
81
86
|
Sponsorship date filtering (on `created-at`, `publish-date`, `purchase-date`, `send-date`) exposes three shapes per field:
|
|
@@ -51,12 +51,12 @@ ThoughtLeaders is a sponsorship marketplace connecting **Brands** (advertisers /
|
|
|
51
51
|
|
|
52
52
|
The centre of the data model is **Sponsorships** — business relationships between brands and channels. Sponsorships have a funnel of types, from broad to narrow:
|
|
53
53
|
|
|
54
|
-
- **Sponsorships** — the broadest category, encompassing all stages
|
|
54
|
+
- **Sponsorships** — the broadest category, encompassing all stages, stored in the `thoughtleaders_adlink` table.
|
|
55
55
|
- **Matches** — possible brand-channel pairings that ThoughtLeaders thinks could work
|
|
56
56
|
- **Proposals** — matches that have been proposed to both sides to consider
|
|
57
57
|
- **Deals** — contractually agreed-upon sponsorships (sold), either in production or published
|
|
58
58
|
|
|
59
|
-
Sponsorships are sometimes called "Ads" or "Ad campaigns".
|
|
59
|
+
Sponsorships are sometimes called "Ads" or "Ad campaigns". An obsolete name for "sponsorship" is an "adlink".
|
|
60
60
|
|
|
61
61
|
The CLI has shortcut commands for each type: `tl matches`, `tl proposals`, `tl deals`. These filter `tl sponsorships` by status.
|
|
62
62
|
|
|
@@ -66,15 +66,15 @@ Other key concepts:
|
|
|
66
66
|
- **Reports** — saved report configurations that can be re-run
|
|
67
67
|
- **Comments** — notes attached to sponsorships
|
|
68
68
|
- **Adspots** — types of ads a channel carries (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost.
|
|
69
|
-
- **MSN** (Media Selling Network) — the ~11k YouTube channels that have opted in to receive sponsorship offers.
|
|
70
|
-
- **TPP** (ThoughtLeaders Partner Program, a.k.a. "TL channels") — the smaller, exclusive ~169 channels TL manages directly.
|
|
69
|
+
- **MSN** (Media Selling Network) — the ~11k YouTube channels that have opted in to receive sponsorship offers. A channels is in the MSN group if the `channel.media_selling_network_join_date` field is not null.
|
|
70
|
+
- **TPP** (ThoughtLeaders Partner Program, a.k.a. "TL channels") — the smaller, exclusive ~169 channels TL manages directly. A channel is in the TPP group if the `channel.is_tl_channel` is True.
|
|
71
71
|
- **`demographics_updated_at`** (on channel detail) — ISO timestamp of when demographic screenshots were last uploaded and processed via OCR. If non-null, the channel has demographics screenshots on file. If null, no screenshots have been uploaded. Use this to check whether a channel has demographics data from screenshots.
|
|
72
72
|
- **`impression`** (on channels) — projected views per video on that channel. Forward-looking estimate. May be null when not yet computed.
|
|
73
73
|
- **`views`** (on sponsorships) — actual view count of the sold and published sponsored video, accessible when `article_id` is set.
|
|
74
74
|
- **`impressions_guarantee`** (on sponsorships) — projected/guaranteed impressions for the sponsorship. Numeric; rounded to int in list output.
|
|
75
75
|
- **Sponsorship detail fields** (returned by `tl sponsorships show <id> --json`) — in addition to the list-view columns, the detail payload includes `integration` (raw int), `publish_count`, `common_name`, `outreach_email`, nested `publisher` (`first_name`, `last_name`, `email`), nested `brand_contact` (`first_name`, `last_name`, `email`), and `brand.organization_name`. Use these when generating IOs, contracts, or outreach.
|
|
76
76
|
- **CPM** has two distinct meanings depending on level — pick the one the user actually wants:
|
|
77
|
-
- **Channel CPM** = `(adspot.price / channel.impression)
|
|
77
|
+
- **Channel CPM** = `(adspot.price / channel.impression) * 1000` — projected price per thousand projected views. Used for pricing decisions **before** a sponsorship is sold. Available for channels with active adspots via `tl channels show <channel_id>`.
|
|
78
78
|
- **Sponsorship CPM** = calculated in either of two ways: if `views` is present, then CPM is `(sponsorship.price / sponsorship.views) × 1000`, meaning realized cost per thousand actual views, computed post-publication. If `views` is null, Compute from the sponsorship's `price` and the channel's `impression` fields.
|
|
79
79
|
- **CPM does not have a range filter.** To find sponsorships in a CPM range (e.g. "around $15"), fetch the record set with other filters first, then apply the CPM range in post-processing (jq, Python, etc.) on the returned `cpm` field. Plan queries and pagination accordingly — the server cannot reduce the result count based on CPM.
|
|
80
80
|
- **Sponsorship dates** — each sponsorship has four distinct dates, useful for different queries:
|
|
@@ -87,23 +87,23 @@ Other key concepts:
|
|
|
87
87
|
Users see data scoped by their organization and plan:
|
|
88
88
|
- **Media buyers** see sponsorships where their org is the brand. They see `price` but never `cost`.
|
|
89
89
|
- **Media sellers** see sponsorships where their org is the publisher. They see `cost` but never `price`.
|
|
90
|
-
- **Intelligence plan** is required for
|
|
90
|
+
- **Intelligence plan** is required for accessing information not strictly related to the user's organisation.
|
|
91
91
|
|
|
92
92
|
When querying sponsorship bookings, query by `status:sold` and filter the the date range only by `purchase_date`. Otherwise, query for state:sold by `created_at`.
|
|
93
93
|
|
|
94
|
-
An obsolete name for "sponsorship" is an "adlink".
|
|
95
|
-
|
|
96
94
|
## Methodology
|
|
97
95
|
|
|
98
96
|
Where possible, if searching for a sponsorship match between channels and brands, first search for what do similar brands sponsor / which brands is the channel usually sponsored by. The similarity judgement should be preferably based on similar topics, similar upload frequency, similar channel sizes, and only after all that, on demographics.
|
|
99
97
|
|
|
98
|
+
Use the `tl channels similar` and `tl brands similar` commands to explore 1:1 similarity between known channels or brands. For category- or topic-driven discovery (e.g. "find me Cooking channels", "who scores high on USA share?"), use `tl recommender top-channels "<tag>"` (or `top-brands`/`top-profiles`) against the vector recommender — that's faster, ranked by category-strength. Run `tl recommender tags` to discover the valid tag names.
|
|
99
|
+
|
|
100
100
|
## Workflow
|
|
101
101
|
|
|
102
102
|
At the start of session, always run a `tl help` command to find out which commands are available, and the `tl whoami` command to find out what you have access to.
|
|
103
103
|
|
|
104
104
|
Unless the user specifically asks for running a specific report or showing the result of a specific report, find the data by using other, low-level commands.
|
|
105
105
|
|
|
106
|
-
1. **Discover first**: Run `tl describe show <resource> --json` to learn available fields, filters, and credit costs before querying
|
|
106
|
+
1. **Discover first**: Run `tl describe show <resource> --json` to learn available fields, filters, and credit costs before querying. Use `tl schema pg`, `tl schema es`, and `tl schema fb` to find information about the main database (pg), the articles / uploads database (es), and the channel metrics database (fb).
|
|
107
107
|
2. **Check saved reports**: Run `tl reports --json` to see if the user has a saved report that already answers their question
|
|
108
108
|
3. **Check credits**: Run `tl balance --json` before expensive queries. Warn the user if a query will cost many credits.
|
|
109
109
|
4. **Query with filters**: Use `key:value` filter syntax for structured queries
|
|
@@ -138,6 +138,13 @@ tl brands show <id-or-name> # Brand detail (1 credit)
|
|
|
138
138
|
tl brands history <id-or-name> # Sponsorship history (5 credits/result, linear)
|
|
139
139
|
tl brands history <query> --channel <id> # Brand mentions on specific channel
|
|
140
140
|
tl brands similar <id-or-name> # Find similar brands via profile vector KNN (50 credits flat)
|
|
141
|
+
tl recommender tags [query] # List vector tag names — categories, demographics, formats (free)
|
|
142
|
+
tl recommender top-channels "<tag>" # Top channels loaded on a vector tag (50 credits; Intelligence)
|
|
143
|
+
tl recommender top-profiles "<tag>" # Top brand profiles loaded on a vector tag (50 credits)
|
|
144
|
+
tl recommender top-brands "<tag>" # Top brands (deduped from profiles) loaded on a vector tag (50 credits)
|
|
145
|
+
tl recommender inspect-channel <ref> # Show a channel's feature-vector breakdown (50 credits; Intelligence)
|
|
146
|
+
tl recommender inspect-brand <ref> # Show a brand profile's ideal-vector breakdown (50 credits; Intelligence)
|
|
147
|
+
tl recommender similar-to-profile <id> # Channels closest to a brand profile's ideal vector (50 credits; Intelligence)
|
|
141
148
|
tl snapshots channel <id> # Channel metrics over time — list curve, mult 1.2 (Firebolt-backed)
|
|
142
149
|
tl snapshots video <id> --channel <id> # Video view curve — list curve, mult 1.2 (--channel required!)
|
|
143
150
|
tl reports # List saved reports — list curve, mult 1.3
|
|
@@ -278,10 +285,10 @@ See [references/business-glossary.md](references/business-glossary.md) for reven
|
|
|
278
285
|
|
|
279
286
|
| Capability | Status | Workaround |
|
|
280
287
|
|---|---|---|
|
|
281
|
-
| Arbitrary read-only `SELECT` on Postgres | **Available** via `tl db pg`. | SELECT-only, mandatory `LIMIT ≤ 500` + `OFFSET`,
|
|
282
|
-
| Cross-reference helpers ("channels proposed to brand X", "channels sponsored by MBN brands in last N days") | **
|
|
283
|
-
| **AdLink INSERT** with custom price/cost/owner/`weighted_price`/`created_where` | **Unavailable** — `tl sponsorships create` exists but only creates a free *proposal* between a channel and a brand.
|
|
284
|
-
| Pre-insert validation queries (joining `adspot ↔ channel ↔ profile ↔ org` to confirm MSN, integration=1, persona, plan) | **
|
|
288
|
+
| Arbitrary read-only `SELECT` on Postgres | **Available** via `tl db pg`. | SELECT-only, mandatory `LIMIT ≤ 500` + `OFFSET`, only certain SQL forms are allowed. See `references/postgres-schema.md`. |
|
|
289
|
+
| Cross-reference helpers ("channels proposed to brand X", "channels sponsored by MBN brands in last N days") | **Available** via `tl db pg`. | Write the join: `thoughtleaders_adlink` ↔ `adspot` ↔ `channel` ↔ `profile` ↔ `profile_brands` ↔ `brand`. Filter by `publish_status` for proposed/sold and by date range as needed. See `references/postgres-schema.md` for the exact column names. |
|
|
290
|
+
| **AdLink INSERT** with custom price/cost/owner/`weighted_price`/`created_where` | **Unavailable** — `tl sponsorships create` exists but only creates a free *proposal* between a channel and a brand. The `tl db pg` sanitizer accepts SELECT only — no INSERT/UPDATE. | Done in the app or by a human with DB access. |
|
|
291
|
+
| Pre-insert validation queries (joining `adspot ↔ channel ↔ profile ↔ org` to confirm MSN, integration=1, persona, plan) | **Available** via `tl db pg`. | One SELECT joining the four tables. Use `thoughtleaders_channel.media_selling_network_join_date IS NOT NULL` for MSN, `thoughtleaders_adspot.integration = 1` for mention adspots, `thoughtleaders_profile.persona` for the persona code (see persona constants in `references/postgres-schema.md`). |
|
|
285
292
|
| Firebolt cross-table or join queries; filtering on non-indexed columns in WHERE | **Unavailable** — not accepted. | Fetch a wider slice keyed on `channel_id` (and optionally `id`), filter the rest in `jq`/Python. |
|
|
286
293
|
| ES `query_string`, `regexp`, `wildcard`, `fuzzy`, `more_like_this`, parent/child joins; any `script_*`; multiple aggregations in one body | **Unavailable** — not accepted. | Rewrite using `term`/`terms`/`match`/`bool`/`nested`. For multi-agg dashboards, run multiple `tl db es` calls and combine client-side. For "similar"-style queries, try `tl channels similar` / `tl brands similar` (vector KNN, server-implemented). |
|
|
287
294
|
| ES deep pagination beyond `from+size = 10,000` | **Unavailable** via raw — `scroll` and `pit` aren't allowlisted; `search_after` is allowed but `from` is still capped. | Use `search_after` with `sort` to walk past 10k. For huge sweeps, narrow with `publication_date` ranges. |
|
|
@@ -309,43 +316,52 @@ tl changelog --md > CHANGELOG.md # Capture for a doc
|
|
|
309
316
|
`tl changelog` summaries are LLM-generated server-side from full commit messages and cached per version, so repeat calls are fast and don't re-bill the LLM. The release date and a 2–4 sentence prose summary come back per version.
|
|
310
317
|
|
|
311
318
|
### Filter syntax
|
|
312
|
-
|
|
319
|
+
Structured list commands accept `key:value` filters (use them for trivially simple lookups):
|
|
313
320
|
```bash
|
|
314
321
|
tl sponsorships list status:sold brand:"Nike" purchase-date:2026-01
|
|
315
322
|
tl uploads list channel:12345 type:longform
|
|
316
|
-
tl channels list category:cooking min-subs:100k language:en
|
|
317
|
-
tl channels list tpp:yes # list all TPP (TL-managed) channels
|
|
318
|
-
tl channels list tpp:no primary-device:mobile # mobile-first channels that aren't in TPP
|
|
319
|
-
tl channels list msn:yes category:tech # Media Selling Network channels in tech
|
|
320
|
-
tl channels list msn:no min-subs:500k # big non-MSN channels (not yet opted in)
|
|
321
323
|
```
|
|
322
324
|
|
|
323
325
|
Date filters accept keywords: `today`, `yesterday`, `tomorrow`.
|
|
324
326
|
|
|
325
|
-
#### Channel
|
|
327
|
+
#### Channel discovery — recommender first, raw SQL second
|
|
328
|
+
|
|
329
|
+
For category- or demographic-driven discovery, **use the vector recommender, not `content_category` SQL.** The recommender ranks channels by how strongly they load on a category/demographic tag (cosine-style scores), instead of forcing exact equality on a single integer code. It also returns the matching brand profiles alongside the channels — useful when the user actually wants to know "who buys this kind of inventory."
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# Discover the right tag name first (free)
|
|
333
|
+
tl recommender tags cooking
|
|
334
|
+
tl recommender tags "usa"
|
|
335
|
+
|
|
336
|
+
# Top channels & profiles loaded on a vector tag (50 credits; Intelligence)
|
|
337
|
+
tl recommender top-channels "Cooking" msn:yes --limit 50
|
|
338
|
+
tl recommender top-channels "Tech" --limit 30
|
|
339
|
+
tl recommender top-brands "USA share" mbn:yes --limit 50
|
|
340
|
+
```
|
|
326
341
|
|
|
327
|
-
|
|
342
|
+
Use `tl db pg` only for predicates the recommender can't express — pure attribute filters (`is_tl_channel`, `language`, `demographic_device_primary`), aggregations, and joins. Run `tl schema pg` once to confirm the live column set; the columns referenced below are stable.
|
|
328
343
|
|
|
329
344
|
```bash
|
|
330
|
-
#
|
|
331
|
-
tl
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
tl
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
# Combine with other filters
|
|
345
|
-
tl channels list category:tech primary-device:mobile min-us-share:50 min-subs:100k
|
|
346
|
-
tl sponsorships list status:sold primary-device:mobile min-us-share:60
|
|
345
|
+
# All TPP (TL-managed) channels — pure attribute filter, not a category query
|
|
346
|
+
tl db pg "SELECT id, channel_name, content_category, total_views
|
|
347
|
+
FROM thoughtleaders_channel
|
|
348
|
+
WHERE is_tl_channel = TRUE
|
|
349
|
+
ORDER BY total_views DESC
|
|
350
|
+
LIMIT 200 OFFSET 0"
|
|
351
|
+
|
|
352
|
+
# Mobile-first non-TPP channels — device share, not topic
|
|
353
|
+
tl db pg "SELECT id, channel_name, demographic_device_primary, total_views
|
|
354
|
+
FROM thoughtleaders_channel
|
|
355
|
+
WHERE is_tl_channel = FALSE
|
|
356
|
+
AND demographic_device_primary = 'mobile'
|
|
357
|
+
ORDER BY total_views DESC
|
|
358
|
+
LIMIT 100 OFFSET 0"
|
|
347
359
|
```
|
|
348
360
|
|
|
361
|
+
For per-country share beyond the recommender's "USA share" tag, use the `demographic_geo` jsonb in raw SQL: `(demographic_geo->>'gb')::int >= 25`. Same pattern with `demographic_device->>'mobile'` for non-primary device shares.
|
|
362
|
+
|
|
363
|
+
**MSN status (`media_selling_network_join_date`) is scrubbed from the advertiser sandbox view.** Raw SQL can't filter on it from an advertiser context; use the structured `tl channels list msn:yes|no|both` for MSN-specific lookups, then drop down to SQL on the resulting IDs (`WHERE id IN (...)`) for further analysis.
|
|
364
|
+
|
|
349
365
|
### Output flags
|
|
350
366
|
- `--json` — structured JSON (use this for parsing)
|
|
351
367
|
- `--csv` — CSV output
|
|
@@ -417,9 +433,19 @@ tl reports --json # Find the report ID first
|
|
|
417
433
|
tl reports run 42 --json
|
|
418
434
|
```
|
|
419
435
|
|
|
420
|
-
"Find
|
|
436
|
+
"Find Cooking channels with US-heavy mobile audiences":
|
|
421
437
|
```bash
|
|
422
|
-
|
|
438
|
+
# Use the vector recommender for the topic, then narrow with structured filters / SQL on the IDs.
|
|
439
|
+
tl recommender top-channels "Cooking" msn:yes --limit 100 --json \
|
|
440
|
+
| jq -r '.results[].channel_id' \
|
|
441
|
+
| paste -sd, - \
|
|
442
|
+
| xargs -I {} tl db pg "SELECT id, channel_name, total_views, demographic_usa_share
|
|
443
|
+
FROM thoughtleaders_channel
|
|
444
|
+
WHERE id IN ({})
|
|
445
|
+
AND demographic_device_primary = 'mobile'
|
|
446
|
+
AND demographic_usa_share >= 50
|
|
447
|
+
ORDER BY total_views DESC
|
|
448
|
+
LIMIT 50 OFFSET 0" --json
|
|
423
449
|
```
|
|
424
450
|
|
|
425
451
|
"Show sold sponsorships targeting mobile US audiences":
|
|
@@ -438,3 +464,17 @@ tl channels similar 29834 tpp:yes --limit 30 # TPP (TL-managed)
|
|
|
438
464
|
tl channels similar 29834 min-subs:1000000 exclude:477487 --limit 15 # client-side filters
|
|
439
465
|
```
|
|
440
466
|
**Both `tl channels show` and `tl channels similar` accept either a numeric channel ID or a channel name.** Name arguments are case-insensitive partial matches; if more than one active channel matches, the command prints a candidates table (channel_id, subscribers, name) and exits 1 so you can retry with a specific ID. The `msn` filter on `similar` is tri-state: `yes` (only MSN channels — the default), `no` (only non-MSN channels), `both` (no MSN filter). `tl channels look-alike` is a hidden alias for `similar` that matches the internal "look-alike channels" terminology.
|
|
467
|
+
|
|
468
|
+
"Browse the vector recommender" (categories, demographics, formats — `tl recommender tags` is free):
|
|
469
|
+
```bash
|
|
470
|
+
tl recommender tags # Full tag list (free)
|
|
471
|
+
tl recommender tags cooking # Search tag names by substring
|
|
472
|
+
tl recommender top-channels "Cooking" msn:yes --limit 50 # Top channels loaded on a tag (50 credits)
|
|
473
|
+
tl recommender top-profiles "Cooking" --limit 30 # Top brand profiles for the tag
|
|
474
|
+
tl recommender top-brands "USA share" mbn:yes --limit 30 # Top brands (deduped) — demographic tag, MBN only
|
|
475
|
+
tl recommender top-channels "Tech" exclude-for-profile:842 # Drop channels already proposed for profile 842
|
|
476
|
+
tl recommender inspect-channel 29834 # Per-tag breakdown of a channel's vector
|
|
477
|
+
tl recommender inspect-brand Nike # Per-tag breakdown of a brand's ideal vector
|
|
478
|
+
tl recommender similar-to-profile 842 --limit 30 # Channels closest to a brand profile's ideal vector
|
|
479
|
+
```
|
|
480
|
+
Use `tl recommender top` for category/topic discovery (it's ranked) and `tl channels similar` / `tl brands similar` for 1:1 lookalike searches.
|
{thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/skills/tl/references/firebolt-schema.md
RENAMED
|
@@ -49,7 +49,7 @@ Tracks YouTube video metrics over time. Each row = one scrape of one video on on
|
|
|
49
49
|
| Column | Type | Description |
|
|
50
50
|
|--------|------|-------------|
|
|
51
51
|
| `id` | TEXT | YouTube video ID (e.g., `'dQw4w9WgXcQ'`). **Bare YouTube ID** — NOT the compound `<channel_id>:<youtube_id>` form used in PG `adlink.article_id` and ES `_id`. |
|
|
52
|
-
| `channel_id` | INT | TL channel ID (matches
|
|
52
|
+
| `channel_id` | INT | TL channel ID (matches `thoughtleaders_channel.id` in Postgres) |
|
|
53
53
|
| `channel_format` | INT | Platform format (4 = YouTube) |
|
|
54
54
|
| `publication_date` | DATE | When the video was published |
|
|
55
55
|
| `scrape_date` | DATE | When this data point was captured |
|
|
@@ -85,8 +85,8 @@ Firebolt's **only advantage** is historical metric snapshots. For everything els
|
|
|
85
85
|
| Current view count on a video | **Elasticsearch** (`tl uploads show` or `tl db es`) |
|
|
86
86
|
| Current subscriber count | **Elasticsearch** (`tl channels show`) |
|
|
87
87
|
| Video metadata (title, tags, duration) | **Elasticsearch** |
|
|
88
|
-
| Find channels by criteria | **`tl
|
|
89
|
-
| Deal / pipeline / sponsorship data | **`tl
|
|
88
|
+
| Find channels by criteria | **`tl db pg`** against `thoughtleaders_channel` |
|
|
89
|
+
| Deal / pipeline / sponsorship data | **`tl db pg`** against `thoughtleaders_adlink` (joins to `adspot`/`channel`/`profile`/`brand`) |
|
|
90
90
|
| View curve over time (age 7→30→90→180) | **Firebolt** ✅ |
|
|
91
91
|
| Views at age 30 vs age 180 (evergreenness) | **Firebolt** ✅ |
|
|
92
92
|
| Channel subscriber growth trend | **Firebolt** ✅ |
|
|
@@ -116,8 +116,9 @@ Every Firebolt workflow has two steps:
|
|
|
116
116
|
**Step 1 — get `channel_id` and (optionally) video IDs from PG/ES.**
|
|
117
117
|
|
|
118
118
|
```bash
|
|
119
|
-
# Channels matching some
|
|
120
|
-
tl channels
|
|
119
|
+
# Channels matching some category (vector recommender — preferred over content_category equality)
|
|
120
|
+
tl recommender top-channels "Tech" msn:yes --limit 50 --json \
|
|
121
|
+
| jq '.results[].channel_id'
|
|
121
122
|
|
|
122
123
|
# Or videos for a specific brand's deals (Postgres side, via tl sponsorships)
|
|
123
124
|
tl deals list brand:"Nike" --json --limit 500 \
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""tl recommender — Vector-recommender introspection and discovery.
|
|
2
|
+
|
|
3
|
+
Surfaces the channel/profile feature-vector machinery that powers the
|
|
4
|
+
"Recommender Insights" web view: list the vector tags (categories,
|
|
5
|
+
demographics, formats, etc.), find the top channels and profiles loaded
|
|
6
|
+
on a given tag, inspect a single channel or brand vector, or fetch
|
|
7
|
+
channels similar to a brand profile's ideal vector.
|
|
8
|
+
|
|
9
|
+
For 1:1 similarity use `tl channels similar` and `tl brands similar`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import urllib.parse
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
from tl_cli.client.errors import ApiError, handle_api_error
|
|
18
|
+
from tl_cli.client.http import get_client
|
|
19
|
+
from tl_cli.filters import parse_filters
|
|
20
|
+
from tl_cli.output.formatter import detect_format, output, output_single
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(help="Vector recommender (tags, top-channels/profiles/brands, vector inspection, profile→channel similarity)")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
TOP_CHANNEL_COLUMNS = ["value", "channel_id", "channel_name", "slug"]
|
|
26
|
+
TOP_PROFILE_COLUMNS = ["value", "profile_id", "profile_email", "brand_name", "brand_slug"]
|
|
27
|
+
TOP_BRAND_COLUMNS = ["value", "brand_slug", "brand_name", "profile_id"]
|
|
28
|
+
TOP_COLUMN_CONFIG = {"value": {"justify": "right"}}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _handle_recommender_error(e: ApiError) -> None:
|
|
32
|
+
"""Show ambiguity candidates inline; otherwise default handler."""
|
|
33
|
+
if e.status_code == 400 and isinstance(e.raw, dict) and e.raw.get("candidates"):
|
|
34
|
+
err = Console(stderr=True)
|
|
35
|
+
err.print(f"[yellow]{e.detail}[/yellow]")
|
|
36
|
+
err.print()
|
|
37
|
+
err.print("[bold]Candidates:[/bold]")
|
|
38
|
+
for c in e.raw["candidates"]:
|
|
39
|
+
cid = c.get("channel_id") or c.get("brand_id") or "?"
|
|
40
|
+
name = c.get("name", "")
|
|
41
|
+
extra = c.get("website") or c.get("subscribers") or ""
|
|
42
|
+
err.print(f" {cid:>10} {name} [dim]{extra}[/dim]")
|
|
43
|
+
raise typer.Exit(1)
|
|
44
|
+
handle_api_error(e)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.callback(invoke_without_command=True)
|
|
48
|
+
def recommender(ctx: typer.Context) -> None:
|
|
49
|
+
"""Vector recommender."""
|
|
50
|
+
if ctx.invoked_subcommand is None:
|
|
51
|
+
typer.echo(ctx.get_help())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command("tags")
|
|
55
|
+
def tags_cmd(
|
|
56
|
+
args: list[str] = typer.Argument(None, help="Optional substring (matches tag or normalized name)"),
|
|
57
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
58
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
59
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
60
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""List vector tag names (free).
|
|
63
|
+
|
|
64
|
+
Use this to discover the tag names accepted by `tl recommender top`.
|
|
65
|
+
Each tag is one dimension of a channel/profile feature vector —
|
|
66
|
+
e.g. content categories like "Cooking", demographic buckets like
|
|
67
|
+
"Age 18-24", device shares, country shares.
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
tl recommender tags
|
|
71
|
+
tl recommender tags cooking
|
|
72
|
+
tl recommender tags "age 18"
|
|
73
|
+
"""
|
|
74
|
+
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
75
|
+
query = _strip_quotes(" ".join(args or []).strip())
|
|
76
|
+
params = {"q": query} if query else {}
|
|
77
|
+
client = get_client()
|
|
78
|
+
try:
|
|
79
|
+
data = client.get("/recommender/tags", params=params)
|
|
80
|
+
output(
|
|
81
|
+
data,
|
|
82
|
+
fmt,
|
|
83
|
+
columns=["group", "field_name"],
|
|
84
|
+
title="Recommender vector tags",
|
|
85
|
+
)
|
|
86
|
+
except ApiError as e:
|
|
87
|
+
handle_api_error(e)
|
|
88
|
+
finally:
|
|
89
|
+
client.close()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _strip_quotes(value: str) -> str:
|
|
93
|
+
"""Strip one matching pair of surrounding quotes if present.
|
|
94
|
+
|
|
95
|
+
Lets users paste an example like `tl recommender top-channels "cooking"`
|
|
96
|
+
where the shell already strips quotes, but also tolerates a layer of
|
|
97
|
+
extra quoting from agents or scripts that re-wrap the literal.
|
|
98
|
+
"""
|
|
99
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
|
|
100
|
+
return value[1:-1]
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _do_top(kind: str, tag: str, args: list[str], fmt: str, limit: int, columns: list[str], title: str) -> None:
|
|
105
|
+
tag = _strip_quotes(tag)
|
|
106
|
+
filters = parse_filters(args or [])
|
|
107
|
+
server_keys = {"msn", "mbn", "exclude-for-channel", "exclude-for-profile"}
|
|
108
|
+
params = {k: v for k, v in filters.items() if k in server_keys}
|
|
109
|
+
params["tag"] = tag
|
|
110
|
+
params["limit"] = str(limit)
|
|
111
|
+
|
|
112
|
+
client = get_client()
|
|
113
|
+
try:
|
|
114
|
+
data = client.get(f"/recommender/top/{kind}", params=params)
|
|
115
|
+
output(
|
|
116
|
+
data,
|
|
117
|
+
fmt,
|
|
118
|
+
columns=columns,
|
|
119
|
+
title=title,
|
|
120
|
+
column_config=TOP_COLUMN_CONFIG,
|
|
121
|
+
)
|
|
122
|
+
except ApiError as e:
|
|
123
|
+
handle_api_error(e)
|
|
124
|
+
finally:
|
|
125
|
+
client.close()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@app.command("top-channels")
|
|
129
|
+
def top_channels_cmd(
|
|
130
|
+
tag: str = typer.Argument(..., help='Vector tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
|
|
131
|
+
args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
|
|
132
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
133
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
134
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
135
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
136
|
+
limit: int = typer.Option(50, "--limit", "-l", help="Max results (1-100)"),
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Top channels loaded on a single vector tag.
|
|
139
|
+
|
|
140
|
+
Costs 50 credits per call. Intelligence plan required.
|
|
141
|
+
|
|
142
|
+
Filters:
|
|
143
|
+
msn:<yes|no|all> MSN membership (default: all)
|
|
144
|
+
exclude-for-profile:<id> Drop channels already proposed for this profile
|
|
145
|
+
|
|
146
|
+
Examples:
|
|
147
|
+
tl recommender top-channels "Cooking"
|
|
148
|
+
tl recommender top-channels "Tech" msn:yes --limit 30
|
|
149
|
+
tl recommender top-channels "Cooking" exclude-for-profile:842
|
|
150
|
+
"""
|
|
151
|
+
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
152
|
+
_do_top("channels", tag, args or [], fmt, limit, TOP_CHANNEL_COLUMNS, f"Top channels: {tag}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.command("top-profiles")
|
|
156
|
+
def top_profiles_cmd(
|
|
157
|
+
tag: str = typer.Argument(..., help='Vector tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
|
|
158
|
+
args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
|
|
159
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
160
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
161
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
162
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
163
|
+
limit: int = typer.Option(50, "--limit", "-l", help="Max results (1-100)"),
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Top brand profiles loaded on a single vector tag.
|
|
166
|
+
|
|
167
|
+
Costs 50 credits per call. Intelligence plan required. Profiles can
|
|
168
|
+
represent the same brand more than once (one brand → multiple
|
|
169
|
+
profiles); use `top-brands` for brand-deduplicated results.
|
|
170
|
+
|
|
171
|
+
Filters:
|
|
172
|
+
mbn:<yes|no|all> MBN membership (default: all)
|
|
173
|
+
exclude-for-channel:<id> Drop profiles already proposed for this channel
|
|
174
|
+
|
|
175
|
+
Examples:
|
|
176
|
+
tl recommender top-profiles "Cooking"
|
|
177
|
+
tl recommender top-profiles "USA share" mbn:yes --limit 30
|
|
178
|
+
"""
|
|
179
|
+
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
180
|
+
_do_top("profiles", tag, args or [], fmt, limit, TOP_PROFILE_COLUMNS, f"Top profiles: {tag}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@app.command("top-brands")
|
|
184
|
+
def top_brands_cmd(
|
|
185
|
+
tag: str = typer.Argument(..., help='Vector tag name (e.g. "Cooking", "Age 18-24"). Run `tl recommender tags` to discover valid names.'),
|
|
186
|
+
args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
|
|
187
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
188
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
189
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
190
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
191
|
+
limit: int = typer.Option(50, "--limit", "-l", help="Max results (1-100)"),
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Top brands loaded on a single vector tag (deduplicated from profiles).
|
|
194
|
+
|
|
195
|
+
Costs 50 credits per call. Intelligence plan required. Server-side
|
|
196
|
+
aggregates the underlying profile rows by brand, keeping the
|
|
197
|
+
highest-scoring profile per brand.
|
|
198
|
+
|
|
199
|
+
Filters:
|
|
200
|
+
mbn:<yes|no|all> MBN membership of the underlying profile (default: all)
|
|
201
|
+
exclude-for-channel:<id> Drop brands already proposed for this channel
|
|
202
|
+
|
|
203
|
+
Examples:
|
|
204
|
+
tl recommender top-brands "Cooking"
|
|
205
|
+
tl recommender top-brands "USA share" mbn:yes --limit 30
|
|
206
|
+
"""
|
|
207
|
+
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
208
|
+
_do_top("brands", tag, args or [], fmt, limit, TOP_BRAND_COLUMNS, f"Top brands: {tag}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@app.command("inspect-channel")
|
|
212
|
+
def inspect_channel_cmd(
|
|
213
|
+
channel_ref: str = typer.Argument(..., help="Channel ID (numeric) or name (partial match, must be unique)"),
|
|
214
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
215
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
216
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
217
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Show a channel's feature vector grouped by category.
|
|
220
|
+
|
|
221
|
+
Costs 50 credits per call. Intelligence plan required. Returns the
|
|
222
|
+
grouped sparse vector (active dimensions only) and the magnitude.
|
|
223
|
+
|
|
224
|
+
Examples:
|
|
225
|
+
tl recommender inspect-channel 12345
|
|
226
|
+
tl recommender inspect-channel "MrBeast"
|
|
227
|
+
"""
|
|
228
|
+
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
229
|
+
encoded = urllib.parse.quote(channel_ref, safe="")
|
|
230
|
+
client = get_client()
|
|
231
|
+
try:
|
|
232
|
+
data = client.get(f"/recommender/channels/{encoded}/inspect")
|
|
233
|
+
output_single(data, fmt)
|
|
234
|
+
except ApiError as e:
|
|
235
|
+
_handle_recommender_error(e)
|
|
236
|
+
finally:
|
|
237
|
+
client.close()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@app.command("inspect-brand")
|
|
241
|
+
def inspect_brand_cmd(
|
|
242
|
+
brand_ref: str = typer.Argument(..., help="Brand ID (numeric) or name (partial match, must be unique)"),
|
|
243
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
244
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
245
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
246
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Show a brand profile's ideal feature vector grouped by category.
|
|
249
|
+
|
|
250
|
+
Costs 50 credits per call. Intelligence plan required. Resolves the
|
|
251
|
+
brand to its (preferred MBN) profile and inspects that profile's
|
|
252
|
+
aggregated vector.
|
|
253
|
+
|
|
254
|
+
Examples:
|
|
255
|
+
tl recommender inspect-brand 287
|
|
256
|
+
tl recommender inspect-brand Nike
|
|
257
|
+
"""
|
|
258
|
+
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
259
|
+
encoded = urllib.parse.quote(brand_ref, safe="")
|
|
260
|
+
client = get_client()
|
|
261
|
+
try:
|
|
262
|
+
data = client.get(f"/recommender/brands/{encoded}/inspect")
|
|
263
|
+
output_single(data, fmt)
|
|
264
|
+
except ApiError as e:
|
|
265
|
+
_handle_recommender_error(e)
|
|
266
|
+
finally:
|
|
267
|
+
client.close()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@app.command("similar-to-profile")
|
|
271
|
+
def similar_to_profile_cmd(
|
|
272
|
+
profile_id: int = typer.Argument(..., help="Profile ID (numeric)"),
|
|
273
|
+
args: list[str] = typer.Argument(None, help="Filters (key:value pairs)."),
|
|
274
|
+
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
275
|
+
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
276
|
+
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
277
|
+
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
278
|
+
limit: int = typer.Option(20, "--limit", "-l", help="Max results (1-100)"),
|
|
279
|
+
) -> None:
|
|
280
|
+
"""Channels closest to a brand profile's ideal vector.
|
|
281
|
+
|
|
282
|
+
Costs 50 credits per call. Intelligence plan required. Channels the
|
|
283
|
+
brand has already worked with or been proposed are excluded.
|
|
284
|
+
|
|
285
|
+
Filters:
|
|
286
|
+
language:<iso> Content language (default: en)
|
|
287
|
+
msn:<yes|no> Restrict to MSN channels (default: no)
|
|
288
|
+
|
|
289
|
+
Examples:
|
|
290
|
+
tl recommender similar-to-profile 842
|
|
291
|
+
tl recommender similar-to-profile 842 msn:yes --limit 30
|
|
292
|
+
"""
|
|
293
|
+
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
294
|
+
filters = parse_filters(args or [])
|
|
295
|
+
params = {k: v for k, v in filters.items() if k in {"language", "msn"}}
|
|
296
|
+
params["limit"] = str(limit)
|
|
297
|
+
client = get_client()
|
|
298
|
+
try:
|
|
299
|
+
data = client.get(f"/recommender/profiles/{profile_id}/similar", params=params)
|
|
300
|
+
for r in data.get("results", []):
|
|
301
|
+
score = r.get("score")
|
|
302
|
+
if isinstance(score, (int, float)) and fmt in ("table", "md"):
|
|
303
|
+
r["score"] = f"{score * 100:.1f}%"
|
|
304
|
+
output(
|
|
305
|
+
data,
|
|
306
|
+
fmt,
|
|
307
|
+
columns=["score", "channel_id", "channel_name", "slug"],
|
|
308
|
+
title=f"Channels similar to profile {profile_id}",
|
|
309
|
+
column_config={"score": {"justify": "right"}},
|
|
310
|
+
)
|
|
311
|
+
except ApiError as e:
|
|
312
|
+
handle_api_error(e)
|
|
313
|
+
finally:
|
|
314
|
+
client.close()
|
|
@@ -27,6 +27,7 @@ from tl_cli.commands.db import app as db_app
|
|
|
27
27
|
from tl_cli.commands.deals import app as deals_app
|
|
28
28
|
from tl_cli.commands.matches import app as matches_app
|
|
29
29
|
from tl_cli.commands.proposals import app as proposals_app
|
|
30
|
+
from tl_cli.commands.recommender import app as recommender_app
|
|
30
31
|
from tl_cli.commands.sponsorships import app as sponsorships_app
|
|
31
32
|
from tl_cli.commands.describe import app as describe_app
|
|
32
33
|
from tl_cli.commands.schema import app as schema_app
|
|
@@ -95,6 +96,7 @@ app.add_typer(deals_app, name="deals")
|
|
|
95
96
|
app.add_typer(uploads_app, name="uploads")
|
|
96
97
|
app.add_typer(channels_app, name="channels")
|
|
97
98
|
app.add_typer(brands_app, name="brands")
|
|
99
|
+
app.add_typer(recommender_app, name="recommender")
|
|
98
100
|
app.add_typer(snapshots_app, name="snapshots")
|
|
99
101
|
app.add_typer(reports_app, name="reports")
|
|
100
102
|
app.add_typer(comments_app, name="comments")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/skills/tl/references/business-glossary.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.5.2 → thoughtleaders_cli-0.6.1}/skills/tl/references/postgres-schema.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|