Ask — LLM commentary
<Ask /> sends a query's result to an LLM and renders its natural-language
commentary inline — the explained-insight layer on top of a chart or table. The
model's markdown answer is rendered with raw HTML disabled, and answers are
cached so repeat page loads don't re-bill. A muted footer attributes each
answer to the model that wrote it (e.g. Generated by claude-haiku-4-5).
<Ask data={downloads_by_month} ask="Summarize the download trend in two sentences." />
| Attribute | Purpose |
|---|---|
data |
Required. The query whose result the model sees. |
ask |
Required. The instruction / prompt. |
max_rows |
Cap on rows sent to the model (default in llm.py). |
cache_ttl |
Seconds to cache the answer. |
label |
Optional heading above the answer. |
Needs an `llm:` block
<Ask> requires an llm: block in dashdown.yaml — so there's
no live example on this docs site. Install the provider extra and add the config
below.
Works on semantic-layer data too #
<Ask> accepts a semantic metric reference as an alternative to
data={query} — the same grammar a chart uses:
<Ask metric={sales.revenue} by={sales.region} ask="Which region stands out, and why?" />
It binds to the same synthetic query a chart with those attrs would build, so the
answer rides the same result cache and filter path. A plain queries/*.py
Python query works as a source too.
Configuration #
Set llm.provider to one of mistral, anthropic (Claude), openai, or
openrouter (OpenRouter's OpenAI-compatible gateway). Each provider's SDK is an
optional extra — pip install 'dashdown-md[mistral|anthropic|openai|openrouter]'.
# dashdown.yaml
llm:
provider: anthropic
api_key: ${ANTHROPIC_API_KEY} # ${VAR} reads from the environment
model: claude-haiku-4-5 # optional (this is the anthropic default)
model is optional for every provider except openrouter, which routes to many
upstream models — name one explicitly (e.g. model: anthropic/claude-3.5-sonnet).
The defaults are fast/cheap models; since each uncached request is billed, pin a
more capable one (e.g. claude-opus-4-8) via model when quality matters more.
The block is provider-only (provider / api_key / model) — per-answer knobs
like max_rows and cache_ttl are <Ask> attributes, not config. See
Configuration → llm.
Caching & cost #
Each answer is cached by a deterministic id — a hash of (connector, query,
prompt, max_rows) plus the filter params the SQL actually substitutes — so repeat
page loads and shared filter states reuse one answer instead of billing each view.
cache_ttl controls expiry; it isn't part of the id (so changing it doesn't bust
the cache). A reader's ↻ refresh affordance forces a fresh answer past the cache.
Safety #
The prompt is registered server-side and addressed by that opaque id, so the
GET /_dashdown/api/ask/{id} endpoint can never be fed an arbitrary prompt. The
data payload is capped to max_rows rows plus column types, and the model's answer
is rendered as markdown with raw HTML disabled.
Data leaves your server
<Ask> sends the (capped) query result to your chosen LLM provider. Treat it like
any third-party data processor — don't point it at columns you can't share, or run
an OpenAI-compatible endpoint yourself and target it with the openai / openrouter
provider.
Static builds #
dashdown build bakes one answer JSON per <Ask> def into the export, so the
commentary ships in a static site with no server or API key at view time — the
answer is computed once at build. The model attribution is baked in too, but the ↻
refresh affordance is omitted — a baked answer is fixed, with no live endpoint to
regenerate against.
Try it #
Add an llm: block to your dashdown.yaml, drop an
<Ask data={your_query} ask="…" /> tag onto a page, and dashdown serve it —
the commentary renders inline beneath the data.