Copyable snippets for everyday dsprrr work, grouped by task. Chunks that need no model ran when this page was built, so their output is real. Chunks that call a model show the code only. When a call fails, look up the message in Troubleshooting.
| Task | Functions | Returns |
|---|---|---|
| Describe the task | signature() |
a Signature
|
| Build a program |
module(), chain_of_thought(),
react(), … |
a module |
| Run it |
run(), run_dataset(),
run_async(), run_stream()
|
a named list, a tibble, a promise, the final output |
| Score it |
evaluate() with a metric |
a list with mean_score, scores,
n_errors
|
| Improve it |
compile() with a teleprompter |
a new module (the input is unchanged) |
optimize_grid() |
the same module, changed in place | |
| Look inside |
get_last_prompt(), inspect_history(),
summarize_traces(), session_cost()
|
a prompt record, a tibble, summary lists |
| Keep it |
save_program(), load_program(),
pin_module_config()
|
a file or pin, a module |
Configure a model
qa <- module(signature("question -> answer"))
# Pass a chat to one call
chat <- ellmer::chat_openai(model = "gpt-6-luna")
run(qa, question = "What is the capital of France?", .llm = chat)
# Or set a default for the session
dsp_configure(provider = "openai", model = "gpt-6-luna") # or "anthropic", "google"
set_default_chat(ellmer::chat_anthropic(model = "claude-sonnet-4-5")) # any ellmer chat
run(qa, question = "What is the capital of France?")
# Use a chat only inside a block or a function
with_lm(chat, run(qa, question = "What is the capital of France?"))
ask <- function(question) {
local_lm(chat) # reverts when ask() returns
run(qa, question = question)
}
dsprrr_sitrep() # default chat, API keys, cache statusCalled without arguments, dsp_configure() uses the first
key it finds among OPENAI_API_KEY,
ANTHROPIC_API_KEY and GOOGLE_API_KEY. When
several chats are set, dsprrr uses the first of: the .llm
argument, the module’s own chat (module(sig, chat = chat)),
with_lm() or local_lm(), and the default. Models, providers and streaming
covers providers in detail.
Sampling parameters can be set for the default chat, on a chat you
build, or for one module. gpt-6-luna reasons by default and accepts
temperature only with
reasoning_effort = "none".
dsp_configure(
provider = "openai",
model = "gpt-6-luna",
temperature = 0,
params = ellmer::params(reasoning_effort = "none")
)
chat <- ellmer::chat_openai(
model = "gpt-6-luna",
params = ellmer::params(reasoning_effort = "none", temperature = 0)
)
qa_quick <- module(
signature("question -> answer"),
config = list(reasoning_effort = "low")
)Define signatures
sig <- signature(
"review: str, stars: int -> sentiment: enum('positive', 'negative', 'neutral'), confidence: float",
instructions = "Classify the sentiment of a product review."
)
sig
#>
#> ── Signature ──
#>
#> ── Inputs
#> • review: "string" - Input: review
#> • stars: "integer" - Input: stars
#>
#> ── Output
#> Type: "object(sentiment: enum(positive, negative, neutral), confidence:
#> number)"
#>
#> ── Instructions
#> Classify the sentiment of a product review.| Write | Meaning |
|---|---|
a, b -> x, y |
inputs a and b, outputs x and
y
|
answer |
a string (fields without a type are strings) |
name: str or string
|
string |
n: int or integer
|
integer |
score: float or number
|
number |
flag: bool or boolean
|
TRUE or FALSE
|
label: enum('a', 'b') or
Literal['a', 'b']
|
one of the listed values |
note: Optional[str] |
a field that may be left out |
tags: list[str], array(str) or
str[]
|
a vector of values of the inner type |
counts: dict[str, int] |
an object with free-form keys (value types are not enforced) |
Bounds such as number[0, 100] parse as a plain number:
the bounds are dropped, so check ranges in R. signature()
also ignores arguments it does not know, so a misspelled
instructions disappears without a warning.
For descriptions or nested outputs, build the signature from
input() and ellmer types:
review_sig <- signature(
inputs = list(input("review", description = "Customer review text")),
output_type = ellmer::type_object(
sentiment = ellmer::type_enum(c("positive", "negative", "neutral")),
aspects = ellmer::type_array(
ellmer::type_string(),
description = "Product aspects the review mentions"
)
),
instructions = "Classify the review and list the aspects it mentions."
)Build modules
| Constructor | Use it when |
|---|---|
module(sig) |
one structured prediction per call |
chain_of_thought(sig) |
the model should reason first; adds a reasoning
output |
multi_chain_comparison(sig, M = 3L) |
several reasoning attempts are compared and merged |
react(sig, tools = list(...)) |
the model calls R functions as tools |
program_of_thought(sig, interpreter_factory = r_code_runner) |
the model writes R code whose result answers the question |
code_act(sig, tools = list(...), interpreter_factory = r_code_runner) |
tools and R code in one agent |
rlm_module(sig, interpreter_factory = ...) |
the model explores large inputs in an R session |
flex(sig) |
experimental: an optimizer may rewrite the whole program |
module_fn(sig, forward) |
plain R code with the module interface |
best_of_n(mod, N = 5L, reward_fn = f) |
sample several answers and keep the one with the highest reward |
refine(mod, N = 3L, reward_fn = f) |
retry with feedback until the reward passes |
ensemble(list(mod_a, mod_b)) |
vote across modules |
with_assertions(mod, list(...)) |
retry when checks on the output fail |
pipeline(), %>>%,
step()
|
chain modules |
module() takes a Signature object; the
other constructors also accept a signature string. Choose a module type compares them.
qa <- module(signature("question -> answer"))
cot <- chain_of_thought("question -> answer")
mcc <- multi_chain_comparison("question -> answer", M = 3L)
get_weather <- function(city) paste("Sunny in", city)
weather_tool <- ellmer::tool(
get_weather,
description = "Get today's weather for a city.",
arguments = list(city = ellmer::type_string("City name"))
)
agent <- react("question -> answer", tools = list(weather_tool))
# r_code_runner() (needs callr) runs code in a separate R process.
# It is not a sandbox: use it only with trusted input.
pot <- program_of_thought("question -> answer", interpreter_factory = r_code_runner)
actor <- code_act(
"question -> answer",
tools = list(weather_tool),
interpreter_factory = r_code_runner
)
analyst <- rlm_module(
"document, question -> answer",
interpreter_factory = function() r_code_runner(persistent = TRUE)
)
word_count <- module_fn(
"text -> n_words: int",
function(text) list(n_words = lengths(strsplit(text, "\\s+")))
)
run(word_count, text = "Signatures describe the task")
#> $n_words
#> [1] 4
under_100 <- function(prediction, inputs) as.numeric(nchar(prediction$answer) <= 100)
best <- best_of_n(qa, N = 5L, reward_fn = under_100)
refined <- refine(qa, N = 3L, reward_fn = under_100)
voted <- ensemble(list(qa, cot), reduce_fn = reduce_majority(field = "answer"))
checked <- with_assertions(
qa,
list(assert_output(~ nchar(.x$answer) <= 100, "Keep the answer under 100 characters."))
)
program <- flex("question -> answer")
#> Warning: `flex()` is experimental and its module source schema may change
#> ℹ The default source is declarative JSON; executable R source requires an
#> explicit interpreter factory.Pipelines pass outputs forward by name. The first step receives the
pipeline inputs; every later step receives only the previous step’s
outputs plus the fixed inputs given to step(). In
map, the names are upstream fields and the values are this
step’s inputs. select keeps only the named output fields of
a step. See Chain modules into
pipelines.
summarize <- module(signature("article -> summary"))
headline <- module(signature("summary -> headline"))
news <- summarize %>>% headline # `summary` flows into the second step
explain <- module(signature("text, audience -> explanation"))
for_kids <- pipeline(
summarize,
step(explain, map = c(summary = "text"), audience = "children")
)
run(for_kids, article = "The council approved a bike lane on Main Street.", .llm = chat)Run
reviews <- tibble::tibble(
text = c("I love it", "Arrived broken", "Great value", "Not great, returned it"),
sentiment = c("positive", "negative", "positive", "negative")
)
classifier <- module(
signature("text -> sentiment: enum('positive', 'negative', 'neutral')")
)
# One call returns a named list
result <- run(classifier, text = "I love it", .llm = chat)
result$sentiment
# Vector inputs run as a batch: one named list per element
run(classifier, text = c("I love it", "Arrived broken"), .llm = chat)
# A data frame, row by row: the input columns plus a `result` list-column
run_dataset(classifier, reviews, .llm = chat)
run_dataset(classifier, reviews, .llm = chat, .return_format = "structured") # adds .error, .metadata, .chat
# Per-call options
run(classifier, text = "I love it", .llm = chat, .cache = FALSE) # skip the cache
run(classifier, text = "I love it", .llm = chat, .return_format = "structured") # output, chat, metadata
run_dataset(classifier, reviews, .llm = chat, .concurrency = concurrency_control(max_active = 4L))
# Asynchronous: a promise that resolves to the named list
run_async(classifier, text = "I love it", .llm = chat) |>
promises::then(\(result) print(result$sentiment))
# Streaming: callbacks receive text as it arrives
run_stream(
qa,
question = "Explain recursion in two sentences.",
.llm = chat,
listeners = stream_listener("answer", \(chunk) cat(chunk))
)Token streaming needs the coro package and a module with a single string output; other modules run normally and the listener fires once with the full value.
Evaluate
A metric is called as metric(prediction, expected). Most
built-in metrics compare one field, and field names both
the output field and the data column:
prediction <- list(sentiment = "positive", answer = "Paris is the capital of France.")
expected <- list(sentiment = "Positive", answer = "Paris, France")
metric_exact_match(field = "sentiment")(prediction, expected)
#> [1] FALSE
metric_exact_match(field = "sentiment", ignore_case = TRUE)(prediction, expected)
#> [1] TRUE
metric_f1(field = "answer")(prediction, expected)
#> [1] 0.5
metric_threshold(metric_f1(field = "answer"), threshold = 0.8)(prediction, expected)
#> [1] FALSE
metric_contains("Paris", field = "answer")(prediction, expected) # ignores `expected`
#> [1] TRUEInside evaluate(), prediction is the run
result and expected is the data row, so a custom metric
reads fields from both:
within_one_star <- function(prediction, expected) {
abs(prediction$stars - expected$stars) <= 1
}
within_one_star(list(stars = 4L), tibble::tibble(stars = 5L))
#> [1] TRUEA module_fn() baseline scores without a model:
keyword_baseline <- module_fn("text -> sentiment", function(text) {
positive <- grepl("love|great", text, ignore.case = TRUE)
list(sentiment = if (positive) "positive" else "negative")
})
baseline <- evaluate(
keyword_baseline,
reviews,
metric = metric_exact_match(field = "sentiment")
)
baseline$mean_score
#> [1] 0.75
baseline$scores
#> [1] 1 1 1 0
result <- evaluate(
classifier,
reviews,
metric = metric_exact_match(field = "sentiment"),
.llm = chat
)
result$mean_score
result$n_errors # rows where the call or the metric failed; they score 0The full list of metrics, including
metric_field_match(), metric_custom() and
metric_with_feedback(), is in Metrics and evaluation. To
score with vitals, see Evaluate with
vitals.
Optimize
trainset below is a data frame with the same columns as
reviews, usually a few dozen rows or more.
# compile() returns a new module; `classifier` is unchanged
few_shot <- classifier |> compile(LabeledFewShot(k = 4L), trainset)
bootstrapped <- classifier |>
compile(
BootstrapFewShot(
metric = metric_exact_match(field = "sentiment"),
max_bootstrapped_demos = 4L
),
trainset,
.llm = chat
)
# optimize_grid() changes the module in place
optimize_grid(
classifier,
data = trainset,
metric = metric_exact_match(field = "sentiment"),
parameters = list(
reasoning_effort = c("none", "low"),
instructions_suffix = c("", "Reply with the label only.")
),
.llm = chat
)
module_trials(classifier)Integer settings need an L suffix: k = 4L
works, k = 4 is an error. compile() does not
accept module_fn() programs.
| Teleprompter | What it changes | Example |
|---|---|---|
LabeledFewShot |
demos: k training rows drawn at random (seeded); no
model calls |
LabeledFewShot(k = 4L) |
BootstrapFewShot |
demos: runs the program on training rows and keeps those that pass the metric; handles pipelines jointly | BootstrapFewShot(metric = m, max_bootstrapped_demos = 4L) |
BootstrapFewShotWithRandomSearch |
demos: several bootstrapped candidates, keeps the best | BootstrapFewShotWithRandomSearch(metric = m, num_candidate_programs = 8L) |
KNNFewShot |
demos picked per input by embedding similarity | KNNFewShot(k = 3L, vectorizer = embed) |
GridSearchTeleprompter |
tries instruction or template variants (instructions are appended) | GridSearchTeleprompter(variants = variants, metric = m) |
COPRO |
instructions, by coordinate ascent over model-written candidates | COPRO(metric = m, breadth = 5L, depth = 2L) |
MIPROv2 |
instructions and demos, chosen with a UCB1 bandit | MIPROv2(metric = m, auto = "light") |
SIMBA |
rules and demos learned from hard examples (simplified) | SIMBA(metric = m, max_steps = 4L) |
GEPA |
instructions or Flex source, by reflecting on failures; reads
metric_with_feedback() feedback |
GEPA(metric = m, generations = 5L) |
BetterTogether |
runs several teleprompters in sequence | BetterTogether(metric = m, p = BootstrapFewShot(metric = m), g = GEPA(metric = m), default_strategy = "p -> g") |
Omni |
experimental: explores several teleprompters and continues from the best | Omni(metric = m, explorers = list(...), continuation = GEPA(metric = m)) |
ReAnchor |
experimental: fits the thresholds, cuts and weights of
with_decisions() outputs |
ReAnchor(metric = m) |
Choose an optimizer explains
when to use each. The experimental AutoResearch() and
MetaHarness() are covered in Agentic optimization
harnesses.
Inspect
get_last_prompt() # the last prompt and response, with model, tokens and cost
inspect_history(n = 5) # a tibble of the last five calls
summarize_traces(classifier) # calls, tokens, cost and latency for one module
export_traces(classifier, include_prompts = TRUE, include_outputs = TRUE)
session_cost() # tokens and estimated cost for the whole sessionFor a debugging routine and the meaning of common errors, see Troubleshooting.
Cache
Identical requests (same model, parameters, prompt and output type) are answered from a memory and disk cache. The first cache hit in a session prints “Using cached LLM responses”.
configure_cache(enable_disk = FALSE) # memory only, for this session
cache_stats()
#>
#> ── dsprrr Cache Statistics
#> • Hit rate: 0%
#> • Hits: 0
#> • Misses: 0
clear_cache()
run(qa, question = "Suggest a name for a cat.", .llm = chat, .cache = FALSE) # one call
configure_cache(enable = FALSE) # the rest of the sessionIn CI, set the environment variable
DSPRRR_CACHE_ENABLED=false. The disk cache holds prompts
and responses, so dsprrr uses it only when the directory is private to
you; see Troubleshooting if it is
rejected.
Save and reload
save_program(few_shot, "sentiment-classifier.rds")
classifier <- load_program("sentiment-classifier.rds")
board <- pins::board_folder("pins")
pin_module_config(board, "sentiment-classifier", few_shot)
classifier <- restore_module_config(pins::pin_read(board, "sentiment-classifier"))
use_dsprrr_template("targets", path = "my_project") # a targets pipeline to start fromSaved programs keep the signature, demos and configuration. Chats are
never saved, so pass .llm or set a default after loading;
tools and other R functions need a registry (see
?save_program). Tutorial 6: Save and reuse a
module walks through the full workflow.