AI#
Configuring an AI provider and asking natural-language questions about the experiment.
Configure the AI provider#
Point knit at an OpenAI-compatible AI provider with bootstrap –ai- — storing only env-var names, never the key.*
APIs: bootstrap
The ai commands talk to an OpenAI-compatible chat API. Before using them you
tell knit which env vars hold your provider’s credentials and settings. You
configure this at bootstrap with the --ai-* options, which write the
ai.* metadata:
$ ./exp.sh bootstrap --ai-api-key-env OPENAI_API_KEY --ai-model gpt-4o
Only env-var names and non-secret defaults are stored — the API key itself never reaches the database. At call time knit reads the value of the env var you named, so the secret lives only in your shell:
$ export OPENAI_API_KEY=sk-...
$ ./exp.sh ai ask --question "how many jobs completed?"
--ai-api-key-env is the only required setting. The rest tune where the
request goes and which model answers, each with an env-var form (read at call
time) and a literal fallback (baked into metadata):
--ai-base-url-env/--ai-base-url— the endpoint. The env var wins if set, then the literal, thenhttps://api.openai.com/v1. Point these at any OpenAI-compatible gateway (Azure, a local server, a proxy).--ai-model-env/--ai-model— the default model. A per-call--modelonai ask/ai queryoverrides both.
Re-running bootstrap updates only the ai.* options you type, so one field
(say the model) can change without clearing the rest:
$ ./exp.sh bootstrap --ai-model gpt-4o-mini
If the named key env var is empty or unset when you run an ai command, knit
fails with a clear message rather than calling out with no credentials. Like the
rest of the ai group, configuring the provider needs a bootstrapped
experiment. Once configured, ask a question (Ask a natural-language question)
or generate a query (Answer with generated SQL).
Ask a natural-language question#
Ask ai ask a plain-English question; the model answers by calling knit’s read-only introspection tools — it can inspect, never mutate.
APIs: ai:ask
Once a provider is configured (Configure the AI provider), ai ask answers a
plain-English question about this experiment. The model runs an agentic loop,
answering by calling a fixed set of read-only knit commands exposed to it as
tools — it can describe the command tree, read a command’s --help, run a
read-only SQL query over the recorded run/job tables, read a job’s captured output
(job show), and show metadata (metadata show):
$ export OPENAI_API_KEY=sk-...
$ ./exp.sh ai ask --question "which job used the most nodes, and did it finish?"
The tools are real knit commands — a recordable one records exactly as if you
had run it — but the set is deliberately limited to read-only commands:
there is no arbitrary-command tool, so ai ask can inspect the experiment but
cannot run a command that mutates state. Its answer is grounded in what those
tools return, not invented — the system prompt seeds it with a summary of your
commands and tells it to call a tool when unsure.
A few options tune the call:
--modeloverrides the configured default model for this one question.--max-iterationscaps the agentic tool-call rounds (default8).--systemreplaces the built-in system prompt wholesale — use it to change the assistant’s persona or house rules.--verbosestreams each tool call and result to stderr, so you can watch the model’s reasoning;--rawprints the raw final-message JSON instead of just the answer text.
ai ask reasons over several tools and replies in prose. When you want tabular
data back from a single query instead, use Answer with generated SQL.
Answer with a generated query#
Turn a question into one read-only SQL or Cypher query with ai query — the model writes it, knit runs it read-only and self-corrects on error.
APIs: ai:query
ai query answers a question by translating it into exactly one read-only
query, running it against the experiment, and printing the result. It works over
any experiment that records runs — a command with an output and a table:
@command "montecarlo" "Estimate pi and record the run."
@with_required "samples:integer" "How many samples to draw."
@with_optional "seed:integer" "1" "Random seed."
@with_output "pi:real" "0" "The estimate produced by this run."
@with_table
_montecarlo() {
local samples seed estimate
samples="$(knit_get_parameter "samples" "$@")"
seed="$(knit_get_parameter "seed" "$@")"
# A deterministic stand-in for a real estimate: enough to populate the
# `montecarlo` table (samples, seed, pi) that `ai query` writes SQL against.
# seed 1 gives exactly 3.14159; other seeds nudge it so runs differ.
estimate="$(awk -v d="${seed}" \
'BEGIN { printf "%.5f", 3.14159 + (d - 1) / 100000 }')"
knit_output "pi" "${estimate}"
printf 'samples=%s pi=%s\n' "${samples}" "${estimate}"
}
@done
knit_register "montecarlo" _montecarlo "Estimate pi and record the run."
knit_with_required "samples:integer" "How many samples to draw."
knit_with_optional "seed:integer" "1" "Random seed."
knit_with_output "pi:real" "0" "The estimate produced by this run."
knit_with_table
_montecarlo() {
local samples seed estimate
samples="$(knit_get_parameter "samples" "$@")"
seed="$(knit_get_parameter "seed" "$@")"
# A deterministic stand-in for a real estimate: enough to populate the
# `montecarlo` table (samples, seed, pi) that `ai query` writes SQL against.
# seed 1 gives exactly 3.14159; other seeds nudge it so runs differ.
estimate="$(awk -v d="${seed}" \
'BEGIN { printf "%.5f", 3.14159 + (d - 1) / 100000 }')"
knit_output "pi" "${estimate}"
printf 'samples=%s pi=%s\n' "${samples}" "${estimate}"
}
knit_done
The model is seeded with the live schema and the provenance name map (see Discover the schema), so it writes against your actual tables, columns, and edges:
$ export OPENAI_API_KEY=sk-...
$ ./exp.sh ai query --question "average pi per seed"
SQL or Cypher — the model chooses. By default (--lang auto) the model
picks the language that fits the question: SQL for filtering, aggregation, and
sorting within a table; Cypher (transpiled to SQL by the bundled knit-cypher-to-sql) for
relationships across commands — which command called which, or which setup a
job used:
$ ./exp.sh ai query --question "which setup did the montecarlo job use"
Pin the language with --lang sql or --lang cypher when you already know
which you want; that also narrows the system prompt to a single language so the
model is not tempted by the other. Whichever language is used, the generated
statement passes through the same read-only guard as query sql / query
graph — it must start with a read clause (SELECT/WITH/EXPLAIN/
PRAGMA for SQL; MATCH/OPTIONAL MATCH for Cypher) and contain no write
keyword. If a statement is rejected or the backend reports an error, knit feeds
the error back to the model and it tries again, up to --max-iterations rounds
(default 3).
To review the query before trusting it, ask for it without running it —
--query-only prints the generated statement and its detected language:
$ ./exp.sh ai query --query-only --question "average pi per seed"
SELECT seed, avg(pi) FROM montecarlo GROUP BY seed
The output is shaped by the shared query options:
--formatpicks the mode from thequery_formatset:box(the default here),column,csv,json,line,list,markdown,table,html,ascii,tabs— and it applies identically whether the answer came back as SQL or Cypher.--no-headeromits the header row (headers are on by default here, unlikequery sql);--separatorsets the column separator forcsv/list.--modeloverrides the model for this call and--verbosestreams the chosen language, each generated statement, and any backend error to stderr.
Use ai query when you want data back and would rather not hand-write the
query; reach for Run raw SQL or Query the provenance graph when you already
know it, and Ask a natural-language question when you want a reasoned prose
answer over several read-only tools.