Skip to content

About

No description, website, or topics provided.

Resources

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

Intent Labeler

Label the search intent behind a Google results page (or any set of web pages) and get a recommended content form: which intents the results serve, how strongly (by results and by traffic), in what form each is answered, how long the winning pages are, and when the query does not want an article at all.

Website and in-browser playground: https://romek-rozen.github.io/intent-labeler/ - bring your own OpenRouter key, and a DataForSEO login for live Google results, page reading, search volume and traffic in 16 markets. A full run costs about $0.03 in API fees (most of it DataForSEO Labs); the page shows the actual cost of every step, token counts and your remaining balances. The page has no server and stores no keys. Results marked "Public" (the default) are collected for the project through a small webhook - not published on the site until there is moderation.

n8n templates: the same method as a Google Sheets workflow and as a webhook API - see n8n-template/. The Google Sheets version is published in the n8n template library: Label search intent from Google SERPs with DataForSEO, OpenRouter and Sheets (more from the author: n8n creator page). No n8n yet? n8n (affiliate link).

Accounts you need: DataForSEO (affiliate link) for Google results, pages, volume and traffic, and an OpenRouter key for the model.

The method has one rule that makes it trustworthy: the language model only groups results; every number is computed by code. The model says "results r02, r06 and r08 serve the compare models intent". Shares, ranks, medians and the target length are counted from those IDs - the model never writes a percentage.

Share of results per intent

Which result serves which intent

Length of pages per intent

Charts from the bundled synthetic example (HTML report, Markdown).

Live examples in six markets (Google top 10, run on 2026-09-30). The main report uses openai/gpt-6-luna with reasoning off; models/ in each folder holds the same data labeled by seven low-cost models (see scripts/run_model_comparison.py). Each folder also has snapshot.json to re-run offline:

Query Market Volume / seasonality Dominant intent -> form Reference length Warnings
zagadki logiczne PL 8,100 / 1.66 Kupno książek z zagadkami -> księgarniane listingi i strony produktów 537 words traffic_disagrees, wide_length_band
jak zrobić zakwas na chleb PL 4,400 / 2.82 Zrobić domowy zakwas żytni -> Przepis krok po kroku z harmonogramem dokarmiania i wskazówkami 486 words -
kalkulator raty kredytu PL 1,900 / 1.7 Symulacja raty kredytu hipotecznego -> kalkulator hipoteczny połączony z informacją o ofertach lub konsultacją 2001 words wide_length_band, serp_does_not_want_an_article
senuto PL 6,600 / 1.52 Poznać platformę Senuto -> strona produktowa z opisem funkcji 1525 words -
standing desk US 135,000 / 1.23 Shop for a standing desk -> Retailer product-category listing 423 words wide_length_band
how to make sourdough starter US 74,000 / 2.66 Make a starter from scratch -> Day-by-day starter recipe with measurements and readiness cues n/a (insufficient_sample) -
best running shoes US 60,500 / 1.42 Compare top running shoes -> Editorial best-of roundup with category-based recommendations n/a (insufficient_sample) mixed_serp, traffic_disagrees
neuronwriter US 720 / 1.73 Review NeuronWriter's value -> hands-on review with pros, cons, and user experience n/a (insufficient_sample) consider_separate_pages
seo agency london UK 3,600 / 2.54 Hire a London SEO agency -> Local agency service pages with services, process, and quote or audit calls to action 803 words mixed_serp, traffic_disagrees
Sauerteig ansetzen DE 74,000 / 2.6 Sauerteigstarter selbst ansetzen -> Schritt-für-Schritt-Anleitung mit Tagesplan und kurzen Videos n/a (insufficient_sample) -
Wärmepumpe Kosten DE 40,500 / 1.54 Laufende Stromkosten abschätzen -> Rechenhilfe mit Verbrauchsbeispielen, Stromtarifen und Spartipps 861 words -
kindergeld DE 201,000 / 1.52 Anspruch und Bezugsdauer -> Anspruchsübersicht mit Alters- und Lebenssituationen n/a (spread_too_wide) consider_separate_pages, traffic_disagrees
come fare il lievito madre IT 3,600 / 4.43 Preparare il lievito madre in casa -> Ricetta guidata con dosi, passaggi e tempi di fermentazione 1289 words -
calcolo rata mutuo IT 49,500 / 1.96 Calcolare la rata del mutuo -> Calcolatore interattivo con stima della rata e piano di ammortamento n/a (insufficient_sample) -
recette pâte à crêpes FR 33,100 / 7.69 Préparer une pâte à crêpes classique -> Recette illustrée avec ingrédients et étapes 918 words mixed_serp, consider_separate_pages
meilleur aspirateur robot FR 6,600 / 2.67 Comparer les meilleurs modèles -> Comparatif de modèles testés avec critères et recommandations n/a (insufficient_sample) mixed_serp, traffic_disagrees

What you get

Output Meaning
Intents searcher goals that emerge from the results - no fixed taxonomy, no fixed count - with an optional coarse tag for filtering
Form per intent how each intent is best answered, named freely ("shop category listing", "PDF set", "quiz")
Coverage fraction of results that address the intent (a page can address several)
Answer share how results divide between intents; a result in two intents counts half to each
Traffic share the same from estimated traffic (etv); unknown traffic is left out, never zero
Content form per page tables, lists, images, video, FAQ, forms, calculator inputs - counted from HTML, and their prevalence per intent
Page types what each result is (category listing, buying guide, brand page with FAQ...), named freely
Heading themes topics the pages cover, with how many results cover each - what a complete answer addresses
Search demand monthly volume, CPC, keyword difficulty and seasonality (peak and low month, index, year over year)
Brands, AI Overview brands present in the results; what the AI Overview (or its absence) says
Expected genre the content genre the SERP expects, and whether an article fits at all
Reference length p25/p50/p75 in words and characters of the dominant intent's pages - or null and a reason
Warnings mixed SERP, several major intents, traffic disagrees, wide length band, not an article
Elements presentation elements that do a job for these searchers, and forms to avoid
Reader questions from People Also Ask, related searches and headings

Install

git clone https://github.com/romek-rozen/intent-labeler.git
cd intent-labeler
python -m venv .venv && . .venv/bin/activate
pip install -e '.[api,extract]'  # core has zero dependencies; [api] = FastAPI, [extract] = trafilatura
cp .env.example .env             # then fill in the LLM endpoint and model

Any OpenAI-compatible endpoint works: OpenAI, OpenRouter, LiteLLM, vLLM, Ollama (http://localhost:11434/v1). A cheap model is enough, with reasoning switched off: openai/gpt-6-luna on OpenRouter with INTENT_LLM_TEMPERATURE=none and INTENT_LLM_EXTRA_BODY={"reasoning":{"enabled":false}} (the defaults in .env.example). Seven low-cost models were benchmarked on the same data; see the model comparison on every example page and scripts/run_model_comparison.py.

Use

# 1. Live Google top 10 + traffic + search volume (needs DataForSEO; about $0.03 in API costs). Location 2616 = Poland, 2840 = US.
intent-labeler --keyword "zagadki logiczne" --language pl --location 2616 --save-snapshot

# 2. Any set of pages - a competitor list, your own site section, 20 URLs from a client
intent-labeler --urls my-pages.txt --language en

# 3. A saved snapshot: offline and reproducible (no SERP call, cached LLM answer)
intent-labeler --snapshot examples/sample_snapshot.json --no-fetch --traffic off

Each run writes out/analysis.json, out/report.html (self-contained, light and dark mode) and out/report.md. Add --brief "a buying guide for home office workers" to give the model context about the page you plan; it never changes which intents are reported.

HTTP API

uvicorn intent_labeler.api.app:app --port 8000     # or: docker build -t intent-labeler . && docker run -p 8000:8000 --env-file .env intent-labeler
Endpoint Input
POST /analyze/keyword {"keyword": "...", "language": "en", "location_code": 2840, "traffic": true}
POST /analyze/urls {"urls": ["https://...", ...]} (max 50)
POST /analyze/html-files multipart upload of saved .html files (max 50)
POST /analyze/snapshot {"snapshot": {...}}

Add ?format=html or ?format=md to get a report instead of JSON. Interactive docs at /docs. Details: docs/API.md.

Python

from intent_labeler import analyze
from intent_labeler.core import llm
from intent_labeler.core.config import LlmConfig
from intent_labeler.features import page_source

snapshot = page_source.snapshot_from_urls(["https://a.example/guide", "https://b.example/shop"])
result = analyze(snapshot, chat=llm.openai_chat(LlmConfig.from_env()))
print(result["form"]["dominant_intent_title"], result["form"]["length_words"])

chat is any (system, user) -> str function, so plugging in another SDK takes three lines.

How it works

flowchart TD
    K[keyword] --> S[serp_source<br/>Google top 10 via DataForSEO]
    U[URL list / HTML files] --> P0[page_source<br/>page set without ranks]
    S --> SN[(Snapshot)]
    P0 --> SN
    SN --> F[page_source.enrich<br/>fetch pages, trafilatura text]
    F --> D[digest 400 chars<br/>8 headings + 3 paragraphs]
    F --> E[element inventory<br/>tables, lists, images, video, FAQ, forms]
    F --> W[words and characters<br/>thin pages flagged]
    SN --> T[traffic<br/>etv per URL, unknown stays unknown]

    D --> L{{intent_labeling - the only LLM call<br/>sees titles, snippets, digests - NO numbers<br/>GROUPS results into emergent intents<br/>names form, genre, page types, themes}}
    L --> V[contract validation<br/>errors go back to the model<br/>unplaced results -> unassigned]

    V --> M[metrics - code only<br/>coverage, answer share, traffic share<br/>length distributions, element prevalence]
    E --> M
    W --> M
    T --> M

    M --> FD[form_decision - code only<br/>dominant intent, reference length<br/>genre guard, warnings]
    FD --> R[report<br/>HTML with SVG charts, Markdown, JSON]

    style L fill:#4a3aa7,color:#fff
    style M fill:#1baf7a,color:#fff
    style FD fill:#1baf7a,color:#fff
Loading

The purple box is the only place a language model works; the green boxes are plain arithmetic.

  1. Source - Google top 10 (DataForSEO) or a page list becomes a Snapshot of Results.
  2. Fetch - pages are downloaded; text is extracted with trafilatura (if installed, else a stdlib parser); a 400-char digest (8 headings + 3 paragraphs), words, characters and a structural inventory (tables, lists, images, video, FAQ, forms) are recorded. Pages under 150 words are flagged thin and fall back to title + snippet.
  3. Traffic and demand - DataForSEO estimates etv for every URL (unknown stays unknown) and returns the keyword's search volume with 8 years of monthly history; code derives seasonality.
  4. Label - one LLM call groups results into emergent intents and names each form and the genre. Invalid JSON goes back to the model with the exact error. Unplaced results land in unassigned.
  5. Measure - code computes coverage, answer and traffic shares, length distributions, element prevalence, and coverage of every page type, heading theme and reader question.
  6. Decide - code picks the dominant intent, the reference length and the warnings.
  7. Report - HTML with charts, Markdown, JSON.

The full method, with the reasons behind each rule, is in docs/METHOD.md. Code layout: docs/ARCHITECTURE.md.

Limitations

  • Page fetching uses plain HTTP. JavaScript-rendered pages come back thin; bot-protected sites come back error. Both stay in the intent analysis (title and snippet are enough to label them) but not in length statistics. When most results are video or social (see the sourdough example), there is honestly no reference length.
  • The labeling is as good as the model; see METHOD.md on model choice.
  • Traffic share needs DataForSEO (automatic with --keyword) or your own etv per result.

Development

pip install -e '.[dev,api,extract]'
pytest                          # offline, no API keys, ~1 s
python scripts/build_examples.py   # rebuild docs/example-report.* and docs/images/*

Contributors and coding agents: read AGENTS.md first. Every feature has its own README - start from src/intent_labeler/features/README.md.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages