thoughtleaders-cli 0.7.13__tar.gz → 0.7.15__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.7.13 → thoughtleaders_cli-0.7.15}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/AGENTS.md +1 -1
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/PKG-INFO +1 -1
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl/SKILL.md +14 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl/references/elasticsearch-schema.md +14 -2
- thoughtleaders_cli-0.7.15/skills/tl-keyword-research/SKILL.md +309 -0
- thoughtleaders_cli-0.7.15/skills/tl-keyword-research/references/context-classifier.md +89 -0
- thoughtleaders_cli-0.7.15/skills/tl-keyword-research/scripts/fetch_context.py +184 -0
- thoughtleaders_cli-0.7.15/skills/tl-keyword-research/scripts/search_channels.py +268 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/db.py +7 -0
- thoughtleaders_cli-0.7.13/skills/tl-keyword-research/SKILL.md +0 -167
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/.github/dependabot.yml +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/.github/workflows/ci.yml +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/.github/workflows/cli-integration.yml +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/.gitignore +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/API.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/LICENSE +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/README.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/agents/youtube-comment-classifier.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/hooks/scripts/load-tl-skill.mjs +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl/references/business-glossary.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl/references/postgres-schema.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/.gitignore +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/references/peer-cohort.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/references/red-flags.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/references/scoring.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/scripts/analyze_channel.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/scripts/peer_cohort.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/scripts/report.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/scripts/resolve_channel.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/scripts/score.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/scripts/tl_cli.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-keyword-research/scripts/probe.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-save-report/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-save-report/references/columns_brands.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-save-report/references/columns_channels.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-save-report/references/columns_content.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-save-report/references/columns_sponsorships.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-save-report/references/intelligence_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-save-report/references/intelligence_widget_schema.json +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-save-report/references/report_glossary.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-save-report/references/sortable_columns.json +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-save-report/references/sponsorship_filterset_schema.json +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-save-report/references/sponsorship_widget_schema.json +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-save-report/references/widgets.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-top-partnerships/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-top-partnerships/scripts/top_partnerships.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-views-guarantee/SKILL.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl-views-guarantee/scripts/vg.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/_typer_utils.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/_comments_common.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/bulk_import.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/channels.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/credits.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/reports.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests/test_brands_winner_channels.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests/test_channels_lookalike.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests/test_describe.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests/test_http_auth.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests/test_reports.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests/test_setup.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests/test_sponsorships.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests_cli/AGENTS.md +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests_cli/conftest.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests_cli/test_balance.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests_cli/test_db_es.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests_cli/test_db_fb.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests_cli/test_db_pg.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests_cli/test_schema.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/tests_cli/test_whoami.py +0 -0
- {thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/uv.lock +0 -0
|
@@ -52,7 +52,7 @@ This repo is also a Claude Code plugin, and can directly be installed as one.
|
|
|
52
52
|
### Bundled skills — when to invoke
|
|
53
53
|
|
|
54
54
|
- **`tl`** — the main skill for querying ThoughtLeaders data. Default for any sponsorship / channel / brand / upload / report question.
|
|
55
|
-
- **`tl-keyword-research`** — invoke whenever the user wants to find videos or channels by **content keywords** (topics, concepts, niches) that aren't covered by a curated recommender tag, OR to validate that a candidate channel's content actually touches a given topic.
|
|
55
|
+
- **`tl-keyword-research`** — invoke whenever the user wants to find videos or channels by **content keywords** (topics, concepts, niches) that aren't covered by a curated recommender tag, OR to validate that a candidate channel's content actually touches a given topic. **Default output: a ranked, context-validated set of channels** (each flagged for sponsorability) — discovered via field-weighted ES relevance (`title` > `summary` > `transcript`), then sense-checked so channels that only use the keyword in an unrelated sense are dropped. The keyword-distribution shape (`{operator, keywords:[{keyword,count}]}`) is now an **opt-in mode**, produced only when the user explicitly asks for keyword counts / distribution. **Do not compose keyword sets by hand for `tl db es` content searches — delegate to this skill first.** See `skills/tl/SKILL.md` → *Channel & video discovery* for the four-path decision tree and when to use this vs the recommender / raw SQL.
|
|
56
56
|
- **`tl-save-report`**, **`adapt-tl-data`**, **`tl-views-guarantee`**, **`tl-top-partnerships`** — narrower workflows; the skill files document their own triggers. `tl-top-partnerships` is brand-user-facing: ranks a brand's sold sponsorships by live eCPM vs the sold-date projection and delivers a two-tab Google Sheet via `gws`.
|
|
57
57
|
|
|
58
58
|
### Skill content boundaries
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.15
|
|
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
|
|
@@ -238,8 +238,22 @@ tl sponsorships update 98765 '{"publish_status": 3}'
|
|
|
238
238
|
tl channels update 12345 '{"demographic_male_share": 62}'
|
|
239
239
|
tl channels update 12345 '{"demographic_geo": {"US": 60, "UK": 12, "CA": 8}}'
|
|
240
240
|
tl channels update 12345 '{"demographic_male_share": 55, "demographic_usa_share": 70}'
|
|
241
|
+
tl channels update 12345 '{"outreach_email": "press@creator.com"}'
|
|
242
|
+
tl channels update 12345 '{"all_emails": {"press@creator.com": {"timestamp": "2026-06-25T12:00:00+00:00", "source": "manual"}}}'
|
|
241
243
|
```
|
|
242
244
|
|
|
245
|
+
**Channel contact emails.** Besides demographics, `tl channels update` accepts two
|
|
246
|
+
contact fields:
|
|
247
|
+
|
|
248
|
+
- **`outreach_email`** — the channel's primary outreach address (a single email string, or `null`).
|
|
249
|
+
- **`all_emails`** — the channel's full email archive. It is **not** a free-form blob: it's
|
|
250
|
+
a JSON object keyed by email address, each value an object `{"timestamp": <ISO-8601 datetime>, "source": <string>}`.
|
|
251
|
+
`timestamp` must parse as ISO-8601 (e.g. `"2026-06-25T12:00:00+00:00"`); `source` records
|
|
252
|
+
where the address came from. Known `source` values: `outreach_email` (the address that was
|
|
253
|
+
the primary outreach email, archived when the channel was blacklisted), `google_search`,
|
|
254
|
+
`scheduled_rescrape`. Use a descriptive token such as `manual` for hand-added entries.
|
|
255
|
+
An edit **replaces** the whole `all_emails` object, so include every entry you want to keep.
|
|
256
|
+
|
|
243
257
|
### Creating and vetting sponsorships
|
|
244
258
|
|
|
245
259
|
This is the end-to-end workflow for proposing a sponsorship, then moving it through the funnel as the two sides respond. Three create commands plus `tl sponsorships update` cover every state transition the CLI exposes.
|
{thoughtleaders_cli-0.7.13 → thoughtleaders_cli-0.7.15}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
@@ -41,8 +41,8 @@ Filter with `{"term": {"doc_type": "article"}}`.
|
|
|
41
41
|
| `id` | keyword | Video/article ID. Compound form `<channel_id>:<youtube_id>` (matches PG `adlink.article_id` and ES `_id`). |
|
|
42
42
|
| `title` | text | Video title |
|
|
43
43
|
| `description` | text | Video description |
|
|
44
|
-
| `content` | text | Full content/transcript text |
|
|
45
|
-
| `transcript` | text | Raw transcript |
|
|
44
|
+
| `content` | text | Full content/transcript text (plain text; not always populated) |
|
|
45
|
+
| `transcript` | text | Raw transcript — stored as YouTube timed-text **XML**, not plain text (see note below) |
|
|
46
46
|
| `transcript_language` | keyword | Transcript language code |
|
|
47
47
|
| `summary` | text | AI-generated summary |
|
|
48
48
|
| `publication_date` | date | When video was published |
|
|
@@ -244,6 +244,18 @@ Repeat until a page comes back short (`next_search_after` is absent on an empty
|
|
|
244
244
|
|
|
245
245
|
`text` fields on article docs (`title`, `summary`, `transcript`) appear to use the `standard` analyzer (tokenize + lowercase, no stemmer, no English-possessive filter), so inflections, plurals, and possessives are each indexed as distinct terms. For example: `bitcoin` (4,466,300) vs `bitcoins` (489,262). For stemming-style recall, expand the query side with a `bool.should` over the variants.
|
|
246
246
|
|
|
247
|
+
## Transcript field format
|
|
248
|
+
|
|
249
|
+
The `transcript` field's `_source` is **YouTube timed-text caption XML**, not plain prose. Each caption cue is a `<text start="…" dur="…">` element wrapped in `<transcript>`, and the inner text is **double HTML-entity-encoded** (an apostrophe is `&#39;`, i.e. an escaped `'`):
|
|
250
|
+
|
|
251
|
+
```xml
|
|
252
|
+
<?xml version="1.0" encoding="utf-8" ?>
|
|
253
|
+
<transcript><text start="0.05" dur="4.33">I&#39;m going to</text>...</transcript>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
- **Searching is unaffected** — the field is analyzed as `text`, so `match` / `match_phrase` queries hit the words directly regardless of the markup. The XML only matters when you retrieve and read the raw `_source`.
|
|
257
|
+
- **For plain prose**, either use the `content` field (plain text, but not always populated) or strip the markup yourself, e.g. `jq -r '.results[0].transcript' | sed -E 's/<[^>]+>/ /g'` and then unescape entities twice (the encoding is doubled).
|
|
258
|
+
|
|
247
259
|
## Notes & gotchas
|
|
248
260
|
|
|
249
261
|
- **Composite IDs:** `tl-platform.id` and `_id` are `<channel_id>:<youtube_id>`. The `youtube_id` portion alone is what Firebolt's `article_metrics.id` stores.
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tl-keyword-research
|
|
3
|
+
tl-blurb: find & validate channels by topic
|
|
4
|
+
description: |
|
|
5
|
+
Find YouTube channels that *reliably* cover a topic — discovered by content keywords (topics, concepts, niches), then validated by the CONTEXT in which those keywords appear in the channel's videos. Invoke when the user wants channels for a topic ("find investing channels", "channels that cover tiktok shops"), not by channel ID/name. Default output: a ranked, context-validated set of channels with sponsorability flags (JSON), plus an offer to save as a TL report. The keyword-distribution mode (counts per keyword) is OPT-IN — only when the user explicitly asks for "keyword counts / distribution / how common is X". By default it runs ≥3 refinement rounds and checks in with you before finalizing — say "run autonomously" (or pass `autonomous` / `--auto`) to skip the pauses and let it finish on its own.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# tl-keyword-research — topic → validated channels
|
|
9
|
+
|
|
10
|
+
Given a topic (seed keyword(s) or an NL phrase), arrive at **a CNF (Conjunctive Normal
|
|
11
|
+
Form) keyword expression** that reliably selects channels covering it — *and* the ranked,
|
|
12
|
+
validated set of channels that expression selects. The flow: widen the topic into
|
|
13
|
+
keywords, find candidate channels by **field-weighted** relevance (`title` > `summary` >
|
|
14
|
+
`transcript`), spot-check the **context** of the keyword in each candidate with cheap
|
|
15
|
+
agents to drop channels that only use the word in an unrelated sense, flag each survivor
|
|
16
|
+
for sponsorability, and over **≥3 refinement rounds** compose/recompose the boolean query
|
|
17
|
+
— narrowing and expanding — based on what the context check reveals.
|
|
18
|
+
|
|
19
|
+
**The final result is the CNF expression** — an AND of OR-clauses with negated exclusions,
|
|
20
|
+
e.g. `(investing OR "stock market") AND (stocks OR etf) AND (NOT sermon) AND (NOT betting)`
|
|
21
|
+
— together with the validated channels it selects. The CNF is the distilled, reusable
|
|
22
|
+
artifact: it re-runs verbatim via `search_channels.py --any/--not` and saves with the report.
|
|
23
|
+
|
|
24
|
+
**Default output = the CNF + validated channels.** The old keyword-distribution output
|
|
25
|
+
(`{keyword, count}`) is now an **opt-in mode** — produce it only when the user
|
|
26
|
+
explicitly asks for keyword counts / distribution / "how common is X". See
|
|
27
|
+
[Opt-in: keyword distribution](#opt-in-keyword-distribution).
|
|
28
|
+
|
|
29
|
+
`<SKILL_DIR>` below is this skill's directory (the one holding `SKILL.md`).
|
|
30
|
+
|
|
31
|
+
## When to invoke / skip
|
|
32
|
+
|
|
33
|
+
Invoke when the user wants **channels for a topic** (concepts, niches), not a specific
|
|
34
|
+
channel/brand by ID or name.
|
|
35
|
+
|
|
36
|
+
Skip when:
|
|
37
|
+
- The user has explicit channel / brand IDs or names → `tl channels find` / `tl brands find`.
|
|
38
|
+
- The intent maps cleanly to a curated recommender tag (e.g. "Cooking channels") →
|
|
39
|
+
`tl recommender top-channels "<tag>"`. Don't re-discover curated tags by text match.
|
|
40
|
+
|
|
41
|
+
## The pipeline (you orchestrate; scripts + cheap agents do the work)
|
|
42
|
+
|
|
43
|
+
1. **Expand** — you widen the seeds into keywords and pick AND/OR.
|
|
44
|
+
2. **Search channels** — `search_channels.py` → field-weighted candidate channels (the default result shape).
|
|
45
|
+
3. **Fetch context** — `fetch_context.py` → keyword-in-context evidence per candidate.
|
|
46
|
+
4. **Validate** — spawn cheap (Haiku) agents to judge each channel's keyword sense and surface adjacent terms.
|
|
47
|
+
5. **Refine** — compose AND/OR/NOT to narrow & expand, probe fitness, backtrack; **≥3 rounds**, then checkpoint with the user (and interview them if they want more).
|
|
48
|
+
|
|
49
|
+
## Pacing & autonomy
|
|
50
|
+
|
|
51
|
+
**Default is interactive.** At the **start** of a run, tell the user in one line how this
|
|
52
|
+
will go — e.g. *"I'll refine over at least 3 rounds, then check in before finalizing. Say
|
|
53
|
+
'run autonomously' if you'd rather I not pause."* That lets them opt out **before** the
|
|
54
|
+
first interview, not just after round 3.
|
|
55
|
+
|
|
56
|
+
**Opt-out triggers — any of these → run autonomously** (full behavior in Stage 5): the
|
|
57
|
+
user says run autonomously / without pausing / "don't stop to ask", **or** invokes the
|
|
58
|
+
skill with an `autonomous` / `--auto` argument. The preference holds for the rest of the
|
|
59
|
+
session unless the user revokes it.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Stage 1 — Expand (you)
|
|
64
|
+
|
|
65
|
+
Broaden the seed(s) into **5–15** candidates: synonyms, sub-areas, specific multi-word
|
|
66
|
+
phrases, inflectional variants (ES text fields are **not stemmed** — `invest`/`investing`/`investments`
|
|
67
|
+
are distinct terms), and reasonable abbreviations. Hard rules:
|
|
68
|
+
|
|
69
|
+
- Generic topic/concept terms only. **No brand names** unless the seeds already contain
|
|
70
|
+
one (then adjacent brands in the same category are fine). **No channel names.**
|
|
71
|
+
- No random-letter padding.
|
|
72
|
+
|
|
73
|
+
**AND vs OR** (pass to the scripts via `--operator`):
|
|
74
|
+
- Default **OR** (union: "crypto channels" = crypto / bitcoin / Web3 / …).
|
|
75
|
+
- **AND** only for composite-noun phrases ("AI cooking", "Roman naval warfare") or
|
|
76
|
+
explicit "both X and Y". Under AND, keep candidates *inside the intersection* — don't
|
|
77
|
+
broaden each component independently.
|
|
78
|
+
|
|
79
|
+
## Stage 2 — Search channels (default output)
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Flat OR (broad first pass)
|
|
83
|
+
python3 <SKILL_DIR>/scripts/search_channels.py --operator OR --size 200 \
|
|
84
|
+
investing "index funds" "stock market" "personal finance"
|
|
85
|
+
|
|
86
|
+
# Composed boolean — the lever for narrowing/expanding (see Stage 5):
|
|
87
|
+
# (investing OR "stock market" OR "index funds") AND (stocks OR portfolio OR etf)
|
|
88
|
+
# AND NOT (sermon OR gospel OR "sports investing" OR betting)
|
|
89
|
+
python3 <SKILL_DIR>/scripts/search_channels.py --size 200 \
|
|
90
|
+
--any "investing,stock market,index funds" \
|
|
91
|
+
--any "stocks,portfolio,etf" \
|
|
92
|
+
--not "sermon,gospel,sports investing,betting"
|
|
93
|
+
# JSON array / newline list on stdin also accepted; --since/--until scope publication_date.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Runs ONE collapsed ES search (`collapse` on `channel.id`, sorted by `_score`) so each
|
|
97
|
+
channel surfaces its single best-matching video, then enriches with name +
|
|
98
|
+
sponsorability. **Field weighting is the priority you asked for:** title hits outscore
|
|
99
|
+
summary, which outscore transcript (default boosts `title^4,summary^2,transcript^1`,
|
|
100
|
+
tunable via `--fields`). **Boolean composition:** each `--any "a,b"` is one required
|
|
101
|
+
OR-group (a dimension); repeating `--any` ANDs the groups; `--not` excludes a sense.
|
|
102
|
+
So adding a group narrows, adding terms to a group widens, `--not` prunes — this is the
|
|
103
|
+
machinery Stage 5 drives. Output: `query` (the boolean it ran), `total_matching_videos`,
|
|
104
|
+
and per channel `channel_id, name, score, top_video_id, top_video_title, sponsorability{…}`.
|
|
105
|
+
|
|
106
|
+
## Stage 3 — Fetch context
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
python3 <SKILL_DIR>/scripts/fetch_context.py --channels 466311,2105,199308 \
|
|
110
|
+
--samples 4 --window 160 investing
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
For each candidate, pulls its top-scoring matching videos and extracts the text window
|
|
114
|
+
around each keyword occurrence. **`transcript` is YouTube caption XML** — the script
|
|
115
|
+
strips tags + unescapes entities + windows client-side (ES highlight can't be used: the
|
|
116
|
+
CLI API strips highlight, and transcript fragments would be full of `<text …>` tags
|
|
117
|
+
anyway). Returns per channel: `{channel_id, match_count, sampled, snippets:[{video_id,
|
|
118
|
+
title, field, keyword, text}]}`. `match_count` (how many of the channel's videos match)
|
|
119
|
+
is a useful breadth signal alongside the score.
|
|
120
|
+
|
|
121
|
+
## Stage 4 — Validate context (cheap / Haiku agents)
|
|
122
|
+
|
|
123
|
+
This is the core of "too weak → validated". Keyword matches are noisy: a search for
|
|
124
|
+
**investing** surfaces *"sports investing"* (betting) and church-sermon channels
|
|
125
|
+
("invest in your faith") right next to real finance channels. Spot-check the sense.
|
|
126
|
+
|
|
127
|
+
Spawn cheap agents (Agent tool, **`model: haiku`** or a similarly cheap one) using the prompt in
|
|
128
|
+
[`references/context-classifier.md`](references/context-classifier.md). Give each agent
|
|
129
|
+
a `TOPIC:` line (the intended sense) and usually a `NOT:` line (senses to exclude), plus
|
|
130
|
+
a batch of the Stage-3 evidence. Batch ≈50–100 channels per agent and run batches in
|
|
131
|
+
**parallel**. Each agent returns, per channel:
|
|
132
|
+
|
|
133
|
+
`{i, channel_id, verdict: on_topic|mixed|off_topic, confidence, evidence_quote, adjacent_terms[], notes}`
|
|
134
|
+
|
|
135
|
+
- **`adjacent_terms`** is the discovery signal — co-occurring topics/brands worth
|
|
136
|
+
considering for expansion (e.g. "Amazon"/"affiliate" under "tiktok shop").
|
|
137
|
+
- Surface the interesting verdicts and adjacent terms back to yourself for Stage 5.
|
|
138
|
+
|
|
139
|
+
**Steer for completeness, then verify it — cheap models silently drop the tail of a
|
|
140
|
+
long list.** This is an output-generation behavior, not an input-context limit (the
|
|
141
|
+
evidence fits easily), so two guards:
|
|
142
|
+
|
|
143
|
+
1. **Anchor the count in the prompt.** Index the evidence (`i: 0…N-1`) and open the
|
|
144
|
+
user message with the exact target, e.g. *"There are exactly 50 channels (indices
|
|
145
|
+
0–49). Return exactly 50 objects. The last channel_id is 778812."* Keep per-item
|
|
146
|
+
output terse (the classifier caps `evidence_quote` at ≤8 words) so the full list fits.
|
|
147
|
+
2. **Completeness check + re-send (the guarantee).** After each agent returns, diff the
|
|
148
|
+
returned `channel_id`s against the batch you sent. If any are missing (or the agent
|
|
149
|
+
emitted prose / invalid JSON), re-send just the missing channels to a fresh agent and
|
|
150
|
+
merge. Never assume a batch came back whole. Smaller batches make this fire less often.
|
|
151
|
+
|
|
152
|
+
## Stage 5 — Iterative compositional refinement (≥3 rounds, then ask the user)
|
|
153
|
+
|
|
154
|
+
This is the heart of the skill. You **research the topic by composing and recomposing a
|
|
155
|
+
boolean keyword query** — combining `--any` OR-groups (AND'd together) and `--not`
|
|
156
|
+
exclusions — to narrow and expand the channel set, examining the results each round,
|
|
157
|
+
probing their fitness, and **backtracking** when a move makes things worse. This is not
|
|
158
|
+
a single pass with a flat operator; it is a search through query-space. Each composed
|
|
159
|
+
query **is** a CNF expression — an AND of OR-clauses plus negated literals — so refinement
|
|
160
|
+
is the incremental construction of the final CNF, the skill's end-product.
|
|
161
|
+
|
|
162
|
+
**Run at least 3 rounds.** Three is the floor, not a cap — do not stop before three even
|
|
163
|
+
if round 1 looks good.
|
|
164
|
+
|
|
165
|
+
### Each round
|
|
166
|
+
|
|
167
|
+
1. **Compose / recompose** the boolean query. Round 1 is usually broad: one OR-group of
|
|
168
|
+
the Stage-1 expansion. Later rounds add structure (below).
|
|
169
|
+
2. **Search** — `search_channels.py` with the `--any` / `--not` you composed → candidates.
|
|
170
|
+
3. **Probe for fitness** — run Stages 3–4 (fetch context + Haiku validation) on the top
|
|
171
|
+
candidates. Read the verdicts and `adjacent_terms`.
|
|
172
|
+
4. **Score the round's fitness** from what you see, e.g.: the share of `on_topic` vs
|
|
173
|
+
`off_topic`/`mixed`; whether off-topic verdicts cluster on one confusable sense;
|
|
174
|
+
how many **sponsorable, high-reach** channels are `on_topic`; and how much the set
|
|
175
|
+
changed. Write this down.
|
|
176
|
+
5. **Decide the next move** and **record** (query, fitness, decision) so you can backtrack:
|
|
177
|
+
- **Narrow** when off-topic clusters on a sense, or the set is too broad/noisy: add a
|
|
178
|
+
required dimension (`--any "stocks,portfolio,etf"`), and/or exclude the bad sense
|
|
179
|
+
(`--not "sermon,sports investing"`).
|
|
180
|
+
- **Expand** when fitness is high but reach is thin, or `adjacent_terms` reveal a
|
|
181
|
+
productive direction: widen a group, or add a discovered term. Example: under
|
|
182
|
+
**"tiktok shop"** the context check repeatedly surfaces **"Amazon"** / **"affiliate"**
|
|
183
|
+
→ add them (`--any "tiktok shop,amazon,affiliate"`) as common affiliate providers.
|
|
184
|
+
- **Backtrack** when a move *reduced* fitness (fewer good channels, more noise, lost a
|
|
185
|
+
strong channel): discard it, return to the previous recorded query, and try a
|
|
186
|
+
**different** axis. Backtracking is expected, not failure.
|
|
187
|
+
6. Keep a **running validated set** across rounds (dedupe by `channel_id`); carry forward
|
|
188
|
+
confirmed `on_topic`/`mixed` channels even as the query shifts.
|
|
189
|
+
|
|
190
|
+
Each round, briefly report: the boolean query you ran, the fitness read, and the move
|
|
191
|
+
(narrow / expand / backtrack) with the reason.
|
|
192
|
+
|
|
193
|
+
### After round 3 (and every round thereafter) — checkpoint with the user
|
|
194
|
+
|
|
195
|
+
**Default (interactive):** do **not** finalize autonomously. Present the current validated
|
|
196
|
+
set with a short summary (the CNF expression, fitness, what changed across rounds), then
|
|
197
|
+
**ask the user to validate the results and choose**: accept as-is · run more refinement
|
|
198
|
+
rounds · adjust direction.
|
|
199
|
+
|
|
200
|
+
**Autonomy override — honor a stated preference.** If the user has told you to run
|
|
201
|
+
autonomously / without pausing / "don't stop to ask" — **or** invoked the skill with an
|
|
202
|
+
`autonomous` / `--auto` argument — (in this message or earlier in the session),
|
|
203
|
+
**skip this checkpoint and the intent interview**: keep refining on your own
|
|
204
|
+
judgement, still run **≥3 rounds**, stop when fitness stops improving (or at a sane cap,
|
|
205
|
+
~6 rounds), and return the CNF + channels directly. Honor that preference for the rest of
|
|
206
|
+
the session unless the user revokes it, and note in the result that you ran autonomously.
|
|
207
|
+
|
|
208
|
+
**If the user chooses more rounds, interview them about the *intent* behind the keywords
|
|
209
|
+
first** — so the next rounds are steered, not guessed. Ask what would sharpen the
|
|
210
|
+
composition, e.g.: which sense of the topic they actually mean (and which to exclude);
|
|
211
|
+
the audience / intent / format they care about (beginner vs advanced, reviews vs news);
|
|
212
|
+
must-have vs nice-to-have sub-topics; brands/products that should count or must not; and
|
|
213
|
+
any reach / language / recency constraints. Fold the answers into the `--any` groups,
|
|
214
|
+
`--not` exclusions, and the validator's `TOPIC:` / `NOT:` lines, then run the next rounds
|
|
215
|
+
and checkpoint again.
|
|
216
|
+
|
|
217
|
+
## Sponsorability — rank all, flag (don't filter)
|
|
218
|
+
|
|
219
|
+
Return **all** topically-validated channels ranked by relevance; do not drop a channel
|
|
220
|
+
for being unbookable. Annotate each with the `sponsorability` block from Stage 2:
|
|
221
|
+
`is_active`, `is_tpp` (TPP partner), `is_msn` (Media Selling Network member — has a join
|
|
222
|
+
date), `has_outreach_email`, `sponsorship_price`, `reach` (subscribers). The user
|
|
223
|
+
decides what to do with non-sponsorable matches.
|
|
224
|
+
|
|
225
|
+
## Mixed-context channels — label all, drop only off-topic
|
|
226
|
+
|
|
227
|
+
Keep `on_topic` **and** `mixed` channels in the output, each carrying its verdict +
|
|
228
|
+
confidence. Exclude only channels the agents judged **clearly `off_topic`** (wrong
|
|
229
|
+
sense). Surface the count of dropped channels so the exclusion is visible.
|
|
230
|
+
|
|
231
|
+
## Output (default)
|
|
232
|
+
|
|
233
|
+
A single JSON object. The **CNF expression is the headline result**; the channels are the
|
|
234
|
+
set it selects. `cnf` comes straight from the final `search_channels.py` run.
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"topic": "financial investing",
|
|
239
|
+
"cnf": {
|
|
240
|
+
"expression": "(investing OR \"stock market\" OR \"index funds\") AND (stocks OR portfolio OR etf) AND (NOT sermon) AND (NOT \"sports investing\")",
|
|
241
|
+
"clauses": [["investing", "stock market", "index funds"], ["stocks", "portfolio", "etf"], ["NOT sermon"], ["NOT sports investing"]]
|
|
242
|
+
},
|
|
243
|
+
"rounds": 3,
|
|
244
|
+
"channels": [
|
|
245
|
+
{
|
|
246
|
+
"channel_id": 2105, "name": "Financial Education",
|
|
247
|
+
"score": 55.37, "match_count": 312,
|
|
248
|
+
"verdict": "on_topic", "confidence": "high",
|
|
249
|
+
"evidence_quote": "how to build a stock portfolio",
|
|
250
|
+
"adjacent_terms": ["index funds", "roth ira"],
|
|
251
|
+
"sponsorability": {"is_active": true, "is_tpp": false, "is_msn": false,
|
|
252
|
+
"has_outreach_email": true, "sponsorship_price": 4710.0, "reach": 938000}
|
|
253
|
+
}
|
|
254
|
+
],
|
|
255
|
+
"excluded_off_topic": [{"channel_id": 199308, "name": "Cornerstone Church Sermons", "evidence_quote": "invest in your relationship with God"}]
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Then **offer to save** the validated set: `tl-import` / `tl-save-report` (channels report);
|
|
260
|
+
record the CNF `expression` alongside so the report is reproducible.
|
|
261
|
+
|
|
262
|
+
## Opt-in: keyword distribution
|
|
263
|
+
|
|
264
|
+
Only when the user explicitly asks for keyword counts / distribution, run the ranking
|
|
265
|
+
probe instead of (or before) the channel search:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
# Channel-topic counts — default fields (title, summary, transcript)
|
|
269
|
+
python3 <SKILL_DIR>/scripts/probe.py crypto bitcoin DeFi Web3 "smart contract"
|
|
270
|
+
# Video-level counts — pass --fields title,summary (drop transcript noise)
|
|
271
|
+
python3 <SKILL_DIR>/scripts/probe.py --fields title,summary "budget meal prep" "cheap meal prep"
|
|
272
|
+
# Composite noun — --operator AND keeps candidates inside the intersection
|
|
273
|
+
python3 <SKILL_DIR>/scripts/probe.py --operator AND "3d printing" "resin printing minis"
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Emits `{"operator", "keywords":[{"keyword","count"}, …]}` sorted descending. This is the
|
|
277
|
+
old default; it now serves the explicit "how common is X" question.
|
|
278
|
+
|
|
279
|
+
## Cost
|
|
280
|
+
|
|
281
|
+
- `search_channels.py`: 2 ES calls (collapse search + enrichment), ~`size` billed rows
|
|
282
|
+
each, no expensive fields → cheap.
|
|
283
|
+
- `fetch_context.py`: 1 ES call per channel; it returns `transcript`/`summary` (priced
|
|
284
|
+
fields), so cost ≈ channels × `--samples` × field-rate. Keep `--samples` small (default 4)
|
|
285
|
+
and validate the top candidates, not the whole tail.
|
|
286
|
+
- Haiku validation: cheap by design; batch + parallelize.
|
|
287
|
+
- Use `tl db es … --pricing` to preview; `tl describe show db` for live rates.
|
|
288
|
+
|
|
289
|
+
## Self-check before emitting
|
|
290
|
+
|
|
291
|
+
1. Default output is the validated-channel JSON object (not keyword counts) unless the
|
|
292
|
+
user explicitly asked for distribution.
|
|
293
|
+
2. Field weighting applied: title > summary > transcript.
|
|
294
|
+
3. Every channel sent for validation has a verdict — batch returns were diffed by
|
|
295
|
+
`channel_id` and any missing were re-sent, not silently dropped; only clearly
|
|
296
|
+
`off_topic` channels were excluded, and the dropped set is surfaced.
|
|
297
|
+
4. Refinement composed `--any` / `--not` (not just a flat operator) to narrow & expand,
|
|
298
|
+
probed fitness each round, and backtracked when a move hurt fitness.
|
|
299
|
+
5. **At least 3 rounds** were run; each round reported its query, fitness, and move.
|
|
300
|
+
6. The final result includes the **CNF expression** (AND of OR-clauses + negated literals)
|
|
301
|
+
as the headline artifact, alongside the channels it selects.
|
|
302
|
+
7. Checkpoint honored: by default, after round 3 you **asked the user to validate and
|
|
303
|
+
choose**, and **interviewed them about keyword intent** before any further rounds —
|
|
304
|
+
**unless** the user asked to run autonomously, in which case you ran ≥3 rounds and
|
|
305
|
+
returned directly, noting the skipped checkpoint. Nothing was saved without confirmation
|
|
306
|
+
(or per the user's autonomy preference).
|
|
307
|
+
8. Each channel carries its `sponsorability` flags (all ranked, none filtered out for being unbookable).
|
|
308
|
+
9. Offered to save the result (CNF + channels) as a TL report (did not save unprompted).
|
|
309
|
+
10. If the user requested a chart, render it as an SVG.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Keyword Context Classifier (cheap / Haiku)
|
|
2
|
+
|
|
3
|
+
This is the prompt for the per-channel context check. The orchestrator spawns one
|
|
4
|
+
cheap agent (e.g. **model `haiku`**) per batch of channels with the System role set
|
|
5
|
+
to everything below, then puts the `TOPIC` line + the indexed evidence JSON in the
|
|
6
|
+
user message, and runs batches in parallel.
|
|
7
|
+
|
|
8
|
+
**The orchestrator MUST anchor completeness in the user message** — state the exact
|
|
9
|
+
count and the last id, e.g.:
|
|
10
|
+
`There are exactly 50 channels (indices 0–49). Return exactly 50 objects. The last channel_id is 778812.`
|
|
11
|
+
and **after** collecting results, verify every `channel_id` came back and re-send any
|
|
12
|
+
missing ones. Cheap models drop the tail of long lists; steering reduces it, the
|
|
13
|
+
re-send guarantees it.
|
|
14
|
+
|
|
15
|
+
On promotion to `tl-cli` this can be registered as a subagent
|
|
16
|
+
(`agents/keyword-context-classifier.md`, `model: haiku`, `tools: Read`).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
You decide whether a YouTube channel genuinely covers a topic, or only mentions the
|
|
21
|
+
topic's keyword(s) in an **unrelated sense**. ThoughtLeaders uses your verdict to
|
|
22
|
+
pick channels for paid sponsorships, so a false "on_topic" wastes real money — be
|
|
23
|
+
skeptical. Judge from the snippets only; do not invent context.
|
|
24
|
+
|
|
25
|
+
## Input
|
|
26
|
+
|
|
27
|
+
The user message contains:
|
|
28
|
+
1. A count line stating exactly how many channels there are and the last `channel_id`.
|
|
29
|
+
2. A `TOPIC:` line describing the intended sense, and usually a `NOT:` line listing
|
|
30
|
+
senses to exclude. Example:
|
|
31
|
+
`TOPIC: financial investing — stocks, funds, assets, retirement, portfolios.`
|
|
32
|
+
`NOT: sports betting ("sports investing"), religious ("invest in your faith"), investing time/effort in people.`
|
|
33
|
+
3. A JSON array of channels, each indexed and with sampled keyword-in-context snippets:
|
|
34
|
+
`[{"i": 0, "channel_id": 466311, "snippets": [{"field","keyword","text"}, ...]}, ...]`
|
|
35
|
+
`field` is one of `title` / `summary` / `transcript`. A title hit is a stronger
|
|
36
|
+
topic signal than a lone transcript mention.
|
|
37
|
+
|
|
38
|
+
## Completeness — NON-NEGOTIABLE
|
|
39
|
+
|
|
40
|
+
You MUST return exactly one object for **every** input channel — same `i`, same
|
|
41
|
+
`channel_id`, same order, from index 0 through the last one.
|
|
42
|
+
|
|
43
|
+
- Do **not** stop early, summarize, abbreviate, collapse duplicates, or write `...`
|
|
44
|
+
/ "and so on". Keep going until you have emitted the last `channel_id` named in the
|
|
45
|
+
count line.
|
|
46
|
+
- A long input is not a reason to shorten the output. Process the whole list.
|
|
47
|
+
- Keep each object terse (see limits below) so the full set fits — brevity per item
|
|
48
|
+
is how you finish the list, not dropping items.
|
|
49
|
+
- Before you finish: count your objects. If the count is less than the stated total,
|
|
50
|
+
continue from where you stopped until it matches.
|
|
51
|
+
|
|
52
|
+
## Verdict (choose exactly one per channel)
|
|
53
|
+
|
|
54
|
+
- **on_topic** — snippets show the keyword used in the intended sense across the
|
|
55
|
+
channel's content. Clear, repeated, in-sense usage.
|
|
56
|
+
- **off_topic** — the keyword is used only in an excluded / unrelated sense (the
|
|
57
|
+
`NOT` cases, or anything clearly outside `TOPIC`). This is the exclusion signal.
|
|
58
|
+
- **mixed** — both in-sense and unrelated usage, or too thin/ambiguous to call
|
|
59
|
+
on_topic with confidence. (Mixed channels are KEPT downstream and labelled —
|
|
60
|
+
reserve **off_topic** for channels whose keyword use is clearly the wrong sense.)
|
|
61
|
+
|
|
62
|
+
When torn between on_topic and mixed, prefer **mixed**. Only use **off_topic** when
|
|
63
|
+
the evidence clearly shows the wrong sense.
|
|
64
|
+
|
|
65
|
+
## Also surface (the discovery signal)
|
|
66
|
+
|
|
67
|
+
- **adjacent_terms** — notable topics, products, or brand names that co-occur in the
|
|
68
|
+
snippets and could sharpen the search (e.g. under "tiktok shop": "amazon",
|
|
69
|
+
"affiliate", "temu"). Lower-case, deduped, ≤6 items. `[]` if none.
|
|
70
|
+
- **evidence_quote** — one verbatim phrase from a snippet, **≤8 words**, that best
|
|
71
|
+
justifies the verdict.
|
|
72
|
+
|
|
73
|
+
## Output — STRICT
|
|
74
|
+
|
|
75
|
+
Return ONLY a JSON array, no prose, no markdown fence. One object per input channel,
|
|
76
|
+
same `i` and `channel_id`, same order, **same length as the input**:
|
|
77
|
+
|
|
78
|
+
`[{"i": 0, "channel_id": 466311, "verdict": "on_topic", "confidence": "high", "evidence_quote": "Stock Market Investing", "adjacent_terms": ["stocks","index funds"], "notes": ""}]`
|
|
79
|
+
|
|
80
|
+
- `verdict`: `on_topic` | `mixed` | `off_topic`
|
|
81
|
+
- `confidence`: `high` | `medium` | `low`
|
|
82
|
+
- `evidence_quote`: ≤8 words, or `""`.
|
|
83
|
+
- `notes`: ≤1 short sentence, or `""`.
|
|
84
|
+
|
|
85
|
+
If a channel has no snippets: `{"i": <i>, "channel_id": <id>, "verdict": "mixed", "confidence": "low", "evidence_quote": "", "adjacent_terms": [], "notes": "no evidence"}`.
|
|
86
|
+
If the input array is empty, return `[]`.
|
|
87
|
+
|
|
88
|
+
**Final check before returning: your array length must equal the count stated in the
|
|
89
|
+
user message, with every `channel_id` present.**
|