feat(tiktok): surface comments, user_search, trending across all product surfaces

Brings the three newer verbs to parity with tiktok.scrape everywhere it was
wired: MCP tools (+ selfcheck manifest), the chat subagent prompt/description,
the playground catalog, the native docs page, and the SEO marketing page. Also
adds live e2e stages for comments, user search, and trending. Docs/FAQ now
state the real contract: profile metadata is reliable while its video list can
be withheld, and keyword video search is walled (use user search for accounts).
This commit is contained in:
CREDO23 2026-07-09 19:09:00 +02:00
parent 67b5472b9f
commit 6b15d610d9
8 changed files with 294 additions and 33 deletions

View file

@ -1,2 +1,2 @@
TikTok specialist: pulls structured public TikTok data — videos (caption/text, author, play/like/comment/share counts, music, hashtags, timestamps, web URL) from a hashtag feed, a search query, a creator profile, or a known video URL. Also compares fresh TikTok results against earlier findings in this chat.
Use whenever the task is to find what is trending or being said on TikTok about a topic, gather a creator's or hashtag's videos, or scrape a specific video URL. Triggers include "search TikTok for X", "trending TikTok videos about X", "videos with #X", and "scrape this TikTok video". Not for general web pages (use the web crawling specialist), Google results (use the Google Search specialist), Reddit (use the Reddit specialist), or YouTube (use the YouTube specialist).
TikTok specialist: pulls structured public TikTok data — videos (caption/text, author, play/like/comment/share counts, music, hashtags, timestamps, web URL) from a hashtag feed, a search query, a creator profile, or a known video URL; a video's public comment thread; accounts found by keyword; and the current Explore trending-video feed. Also compares fresh TikTok results against earlier findings in this chat.
Use whenever the task is to find what is trending or being said on TikTok about a topic, gather a creator's or hashtag's videos, read a video's comments, discover accounts by keyword, or scrape a specific video URL. Triggers include "search TikTok for X", "what's trending on TikTok", "videos with #X", "comments on this TikTok", "find TikTok accounts about X", and "scrape this TikTok video". Not for general web pages (use the web crawling specialist), Google results (use the Google Search specialist), Reddit (use the Reddit specialist), or YouTube (use the YouTube specialist).

View file

@ -6,17 +6,23 @@ Answer the delegated question from live TikTok data gathered with your verb, com
</goal>
<available_tools>
- `tiktok_scrape`
- `tiktok_scrape` — videos from urls / profiles / hashtags / search_queries
- `tiktok_comments` — a video's comment thread, from `video_urls`
- `tiktok_user_search` — find accounts by keyword, from `queries`
- `tiktok_trending` — the current Explore trending-video feed
- `read_run` / `search_run` (free readers for stored scrape output)
</available_tools>
<playbook>
- Finding videos on a topic: call `tiktok_scrape` with `hashtags` (no leading '#') and/or `search_queries`.
- Scraping a specific video, profile, hashtag, or search page: pass its TikTok URL in `urls`.
- Profiles: a creator's `profiles` feed can come back empty — TikTok restricts the profile video endpoint. Prefer `hashtags`, `search_queries`, or a direct video URL, and treat an empty profile result as a known limit, not a failure to retry endlessly.
- Controlling volume: use `max_items` for the total cap and `results_per_page` per target.
- Requested counts: `max_items` defaults to only 10 — when the task asks for N videos, set `max_items` and `results_per_page` above N. A call that caps below the target can never satisfy it.
- Batch multiple hashtags or search terms into one call rather than many single-term calls.
- Profiles: a creator's `profiles` feed returns the account's metadata (name, followers, bio, verification) reliably, but its video list is often withheld by TikTok — treat an empty video list as a known limit, not a failure to retry endlessly. Prefer `hashtags`, `search_queries`, or a direct video URL for videos.
- Comments on a video: call `tiktok_comments` with the video URL(s) in `video_urls`.
- Finding accounts (not videos): call `tiktok_user_search` with `queries` — this is the reliable path for account discovery (keyword *video* search is login-walled).
- "What's trending now": call `tiktok_trending` (no query needed); set `max_items` for how many.
- Controlling volume: use `max_items` for the total cap and `results_per_page` per target (per-verb equivalents: `comments_per_video`, `results_per_query`).
- Requested counts: `max_items` defaults low — when the task asks for N items, set `max_items` (and the per-target count) above N. A call that caps below the target can never satisfy it.
- Batch multiple hashtags, search terms, queries, or video URLs into one call rather than many single-item calls.
<include snippet="run_reader"/>
- Comparison requests: pull the current results, compare against prior values already in this conversation's earlier tool results, and report concrete deltas (added, removed, count changes).
</playbook>
@ -59,6 +65,6 @@ Return **only** one JSON object (no markdown/prose):
}
<include snippet="output_contract_base"/>
Route-specific rules:
- `evidence.findings`: one entry per distinct video or delta — a single sentence each; do not paste raw payloads. Max 10 entries, unless the delegated task asks for N items: then return up to N (each backed by a real scraped result, never padded).
- `evidence.findings`: one entry per distinct result (video, comment, or account) or delta — a single sentence each; do not paste raw payloads. Max 10 entries, unless the delegated task asks for N items: then return up to N (each backed by a real scraped result, never padded).
- `evidence.sources`: one TikTok URL per finding when applicable, same cap as findings. List each URL once.
</output_contract>

View file

@ -15,6 +15,12 @@ What it exercises (everything REAL — live network, live proxy, live browser):
Stage 5 full scrape_tiktok() pipeline on a hashtag.
Stage 6 search via the full pipeline: same graceful-degrade contract as
profile (results feed doesn't load for anonymous sessions).
Stage 7 comments on a real video URL (served anonymously once the panel
opens): real comments OR a single honest ErrorItem.
Stage 8 user search: the account-discovery XHR that DOES serve anonymous
headless sessions asserts real account records come back.
Stage 9 trending: the Explore feed of trending videos asserts real,
normalized video items come back.
On success it writes raw itemStructs under tests/fixtures/tiktok/ so the parser
suites can pin against real-shaped data without network.
@ -199,6 +205,61 @@ async def stage_search_listing() -> tuple[bool, list[dict[str, Any]]]:
return ok, items
async def stage_comments(video_url: str) -> tuple[bool, list[dict[str, Any]]]:
_hr("STAGE 7 — comments graceful-degrade")
print(f" target: {video_url}")
from app.proprietary.platforms.tiktok import scrape_tiktok_comments
# Comments load over a signed /api/comment/list XHR that TikTok serves to
# anonymous sessions once the panel opens. Pass if real comments come back
# OR a graceful ErrorItem (video has none / disabled / withheld).
items = await scrape_tiktok_comments(
[video_url], per_video=_COUNT, limit=_COUNT
)
has_comment = any(it.get("id") and not it.get("errorCode") for it in items)
has_error = any(it.get("errorCode") == "no_comments" for it in items)
ok = _check(
"comments yield records or a graceful ErrorItem (never silent empty)",
has_comment or has_error,
f"{len(items)} item(s); comment={has_comment} error={has_error}",
)
return ok, items
async def stage_user_search() -> tuple[bool, list[dict[str, Any]]]:
_hr(f"STAGE 8 — user search (browser): {_PROFILE!r}")
from app.proprietary.platforms.tiktok import search_tiktok_users
# Unlike keyword *video* search, the account-search XHR serves anonymous
# headless sessions — so this asserts real records, not just degradation.
items = await search_tiktok_users([_PROFILE], per_query=_COUNT, limit=_COUNT)
real = [it for it in items if not it.get("errorCode")]
ok = _check(
"user search returns account records",
bool(real) and bool(real[0].get("uniqueId") or real[0].get("name")),
f"{len(items)} item(s); accounts={len(real)}",
)
if real:
print(f" sample: @{real[0].get('uniqueId') or real[0].get('name')}")
return ok, items
async def stage_trending() -> tuple[bool, list[dict[str, Any]]]:
_hr("STAGE 9 — trending (browser): Explore feed")
from app.proprietary.platforms.tiktok import scrape_tiktok_trending
items = await scrape_tiktok_trending(count=_COUNT)
real = [it for it in items if not it.get("errorCode")]
ok = _check(
"trending returns normalized video items",
bool(real) and bool(real[0].get("id")) and bool(real[0].get("webVideoUrl")),
f"{len(items)} item(s); videos={len(real)}",
)
if real:
print(f" sample: {real[0].get('webVideoUrl')}{real[0].get('text', '')[:60]!r}")
return ok, items
async def main() -> int:
print("TikTok scraper functional e2e — live network + proxy + browser")
results: dict[str, bool] = {}
@ -214,8 +275,10 @@ async def main() -> int:
if video_url:
ok_video, _ = await stage_blob_video(video_url)
results["Stage 3 blob video"] = ok_video
ok_comments, _ = await stage_comments(video_url)
results["Stage 7 comments"] = ok_comments
else:
print("\n [SKIP] Stage 3 — no captured struct to build a video URL")
print("\n [SKIP] Stage 3/7 — no captured struct to build a video URL")
ok_search, _ = await stage_search_listing()
results["Stage 6 search listing"] = ok_search
@ -224,6 +287,11 @@ async def main() -> int:
results["Stage 2 profile listing"] = ok_profile
results["Stage 5 pipeline"] = await stage_pipeline()
ok_users, _ = await stage_user_search()
results["Stage 8 user search"] = ok_users
ok_trending, _ = await stage_trending()
results["Stage 9 trending"] = ok_trending
_hr("SUMMARY")
for name, ok in results.items():
print(f" {'PASS' if ok else 'FAIL/SKIP'}{name}")