Skip to contents

Programming—not prompting—LLMs in R

dsprrr brings the power of DSPy to R. Instead of wrestling with prompt strings, declare what you want, compose modules into pipelines, and use optimization to improve prompts, examples, or the program that executes them.

# Install
pak::pak("JamesHWade/dsprrr")

# Configure credentials for an ellmer-supported provider, then:
library(dsprrr)
dsp("question -> answer", question = "What is the capital of France?")
#> "Paris"

Compose programs with reusable primitives

Every dsprrr program is built from the same three pieces. Learn these and the rest of the package falls into place.

Signatures

Declare your task. Define typed inputs and outputs instead of wrestling with prompt strings. Portable, maintainable, and easy to iterate on.

Learn about signatures →

# Route a support ticket
sig <- signature(
  "ticket -> urgency: enum('low', 'high'), team: string"
)

Modules

Same interface, different strategy. Modules control how a signature executes—reason step by step, use tools, or run ensembles—without rewriting the task.

Explore modules →

sig <- signature(
  "ticket -> urgency: enum('low', 'high'), team: string"
)

# Direct completion
classify <- module(sig, type = "predict")

# Add step-by-step reasoning
classify <- module(sig, type = "chain_of_thought")

# Add a tool-use loop
lookup_tool <- ellmer::tool(
  function(query) paste("Found:", query),
  description = "Look up support policy details",
  arguments = list(query = ellmer::type_string())
)
classify <- module(sig, type = "react", tools = list(lookup_tool))

Optimizers

Compile your program against a metric. Give dsprrr examples and a scoring function; it selects the best candidate observed within the configured budget.

Try optimizers →

route_sig <- signature("ticket -> urgency: enum('low', 'high')")
router <- module(route_sig, type = "predict")
trainset <- dsp_trainset(
  ticket  = c("Package lost", "Need a receipt"),
  urgency = c("high", "low")
)

tp <- GEPA(metric = metric_exact_match(field = "urgency"))
optimized <- compile(tp, router, trainset)

board <- pins::board_temp()
pin_module_config(board, "ticket-router-v2", optimized)

Find the release cohort that broke

A release-level average can reveal a regression without explaining it. If the useful grouping and calculation are not known in advance, an RLM keeps the source object in an R environment, lets the model inspect it with R code, and returns only bounded observations between steps.

The deterministic tutorial asks an RLM to explore 40,000 checkout sessions and 200 change records. The answer is fixed and independently validated:

release:      2.4.0
cohort:       platform=mobile / plan=pro
before_rate:  0.92
after_rate:   0.61
drop_pp:      31
change_id:    CHG-1842
investigator <- rlm_module(
  paste(
    "sessions, changes, question ->",
    "release: string, cohort: string, before_rate: number,",
    "after_rate: number, drop_pp: number, change_id: string, evidence: string"
  ),
  interpreter_factory = function() {
    r_code_runner(timeout = 30, persistent = TRUE)
  },
  max_iters = 8,
  max_llm_calls = 0L
)

This persistent callr runner stages rich R objects once, but it is explicitly trusted-input-only. For compact JSON-compatible context, the one-call rlm() helper creates a fresh managed mcp-repl OS sandbox by default. It disables network access but allows writes inside the workspace. Install mcptools and the external mcp-repl executable first. The final JSON-RPC request has a 7 KB wire bound, with gzip/base64 attempted when the raw request is too large, and RLM control frames have a 3,000-byte encoded bound.

Investigate the deterministic regression →  ·  Understand the RLM execution contract →

ProgramOfThought, RLM, or Flex?

If the task requires… Use…
A calculation whose steps are already known program_of_thought()
A new exploration path for this input at inference time rlm_module()
A reusable implementation discovered from labeled examples flex() with GEPA

RLM is experimental inference-time exploration; one invocation does not learn an exploration program. Supported optimizers can still tune its action and fallback instructions. Flex searches during compilation for an implementation to reuse.

Optimize the program, not only the prompt

Most optimizers improve instructions inside a workflow you designed. flex() lets GEPA change the workflow itself: which predictors run, where deterministic R is enough, and when to call a tool you supplied.

The worked example starts with a support router that asks a model about every ticket. In a deterministic GEPA replay, the selected program checks known incident codes directly and reserves the model for ambiguous prose. It keeps all six held-out routes correct while cutting predictor calls from six to three.

Flex did not find a better prompt. It found that half the tickets did not need one.

See the complete router demo and decide when Flex fits →

Getting Started: Configure Your LLM

library(dsprrr)
library(ellmer)

chat <- chat_openai(model = "gpt-4o-mini")
chat |> dsp("question -> answer", question = "What is 2+2?")
#> "4"
library(dsprrr)
library(ellmer)

chat <- chat_claude(model = "claude-sonnet-4-20250514")
chat |> dsp("question -> answer", question = "What is 2+2?")
#> "4"
library(dsprrr)
library(ellmer)

chat <- chat_google_gemini(model = "gemini-2.0-flash")
chat |> dsp("question -> answer", question = "What is 2+2?")
#> "4"
library(dsprrr)
library(ellmer)

chat <- chat_ollama(model = "llama3.2")
chat |> dsp("question -> answer", question = "What is 2+2?")
#> "4"
# dsprrr auto-detects from environment variables
library(dsprrr)

# Uses OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_API_KEY
dsp("question -> answer", question = "What is 2+2?")
#> "4"

Define a task. Grow it into a system.

Start with a single signature and grow it into a multi-step program—the same building blocks scale from a one-line extractor to a full pipeline.

Automatic Optimization

dsprrr can automatically optimize your prompts using your data.

# Add examples automatically
trainset <- dsp_trainset(
  text = c("Great product!", "Awful experience", "It works"),
  sentiment = c("positive", "negative", "neutral")
)

optimized <- compile(
  LabeledFewShot(k = 3),
  classifier,
  trainset
)

# Now includes 3 examples in every prompt
optimized$predict(text = "Amazing service!")
#> "positive"

Result: Few-shot examples improve accuracy on edge cases.

# Search over configurations
classifier$optimize_grid(
  devset = validation_data,
  metric = metric_exact_match(),
  parameters = list(
    temperature = c(0.1, 0.5, 1.0),
    prompt_style = c("concise", "detailed")
  )
)

# View results
module_trials(classifier)
#> # A tibble: 6 × 4
#>   temperature prompt_style score    n
#>         <dbl> <chr>        <dbl> <int>
#> 1         0.1 concise      0.92    100
#> 2         0.1 detailed     0.88    100
#> ...

Result: Find the best configuration for your task.

# Keep agent-proposed analysis inside an OS sandbox
runner <- mcp_repl_runner()

harness <- MetaHarness(
  metric = metric_exact_match(field = "sentiment"),
  max_iterations = 6L,
  max_candidates_per_iteration = 3L
)

optimized <- compile(
  harness,
  classifier,
  trainset,
  valset = validation_data,
  .llm = task_chat,
  .agent_llm = proposer_chat,
  runner = runner
)

Result: Search coordinated instructions and templates across an entire module graph while dsprrr retains control of evaluation, budgets, lineage, and accepted state.

Run agentic optimization →

# Rigorous evaluation with metrics
results <- evaluate(
  classifier,
  test_data,
  metric = metric_exact_match()
)

results$mean_score
#> 0.94

# Integrate with vitals for advanced evaluation
library(vitals)
solver <- as_vitals_solver(classifier)

Result: Measure and track performance systematically.

Why dsprrr?

Declarative

Define what you want, not how to prompt. Signatures like “text -> sentiment” describe your task clearly.

Composable

Build complex pipelines from simple modules. Each module is testable, optimizable, and reusable.

Optimizable

Automatically improve prompts with your data. Few-shot learning, grid search, and advanced teleprompters.

Integrated

Built on ellmer for LLM access and vitals for evaluation. Works with tidyverse.

Observable

Inspect bounded module traces, debug failures, and track usage when providers report it.

Deployment building blocks

Persistence with pins, orchestration with targets, deployment with vetiver.

Learn More

Tutorials

Reference

Ecosystem

dsprrr integrates with much of Posit’s LLM ecosystem:

Package Purpose
ellmer Chat with LLMs from R
vitals LLM evaluation framework
shinychat Chat UIs for Shiny
mcptools Connect R programs to MCP tools
mcp-repl Persistent OS-sandboxed R execution

Inspired by DSPy from Stanford NLP.