Skip to contents

Compiling a module, as in Tutorials 4 and 5, produces demos and instructions that you do not want to recompute in every R session. By the end of this tutorial you can save a compiled module’s configuration to a pins board, restore it as a working module, and export the traces the module records as it runs.

library(dsprrr)
library(ellmer)
library(pins)

chat <- chat_openai(model = "gpt-6-luna")

The answers on this page were recorded with gpt-4.1 and are replayed when the site is built, so gpt-6-luna may word them differently.

Compile a module

This is the sentiment classifier from Tutorial 5, compiled with LabeledFewShot on nine short reviews:

sig <- signature(
  "review -> sentiment: enum('positive', 'negative', 'neutral')",
  instructions = "Classify the sentiment of this product review."
)

trainset <- tibble::tribble(
  ~review,            ~sentiment,
  "Love it!",         "positive",
  "Hate it!",         "negative",
  "It's okay.",       "neutral",
  "Amazing!",         "positive",
  "Terrible!",        "negative",
  "Meh.",             "neutral",
  "Best ever!",       "positive",
  "Worst purchase!",  "negative",
  "Fine I guess.",    "neutral"
)

classifier <- module(sig) |> compile(LabeledFewShot(k = 3L), trainset)

run(classifier, review = "This product is fantastic!", .llm = chat)
#> $sentiment
#> [1] "positive"

Save the configuration

pin_module_config() writes the module’s signature, demos, settings and optimization results to a pins board. The board here is a temporary folder, which R deletes when the session ends:

board <- board_temp()

pin_module_config(board, "sentiment-classifier", classifier)
#> Creating new version '20260930T174635Z-56386'
#> Writing to pin 'sentiment-classifier'
#> ✔ Pinned program artifact: "sentiment-classifier"
#> ℹ Root module: <PredictModule>
#> ℹ Graph nodes: 1
#> ℹ Compiled: TRUE

For real use, pick a board that outlives the session:

board <- board_local() # a folder in your user data directory
board <- board_folder("pins") # or a folder in your project

Shared boards such as board_connect(), board_s3() and board_gcs() work the same way and let other people load your module. Each call to pin_module_config() with the same name saves a new version: pin_versions(board, "sentiment-classifier") lists them, and pin_read(board, "sentiment-classifier", version = ) reads an older one.

Restore it

In a new session, connect to the same board, read the pin and rebuild the module:

config <- pin_read(board, "sentiment-classifier")
restored <- restore_module_config(config)
#> ✔ Restored program artifact
#> ℹ Root module: <PredictModule>
#> ℹ Artifact version: 6

identical(restored$demos, classifier$demos)
#> [1] TRUE

The chat is not part of the saved configuration, so pass .llm when you run the restored module:

run(restored, review = "Worst product ever!", .llm = chat)

restore_module_config() reads only the artifact format of the installed dsprrr (version 6 in the message above), so keep the code that compiled the module: if a dsprrr upgrade changes the format, compile and pin again. A module that holds R functions or tools also needs a registry when you save and restore it; see pin_module_config(). To use a saved module in a targets pipeline or a Quarto report, see Run dsprrr in pipelines.

Keep a reviewed version

A pin keeps every version you save. To also record which version a person approved for production, and what it was compiled from, keep the program in graft. Save the whole program with save_program(), store it with its training examples as evidence, and accept it for a purpose:

store <- graft::graft_store("programs", create = TRUE)
id <- program_artifact_id(classifier)
path <- save_program(classifier, tempfile(fileext = ".rds"))

examples <- graft::graft_save(
  store,
  as.character(jsonlite::toJSON(trainset)),
  id = "sentiment:trainset",
  media_type = "application/json"
)
program <- graft::graft_save_file(
  store,
  path,
  id = "sentiment:program",
  dependencies = examples
)
graft::graft_accept(
  store,
  program,
  stream = "sentiment-classifier",
  expected = NULL,
  key = id,
  actor = "reviewer",
  reason = "Checked its evaluation on held-out reviews",
  purpose = "production"
)

In production, load whichever program is accepted now:

recall <- graft::graft_recall(
  store,
  stream = "sentiment-classifier",
  purpose = "production",
  eligible = TRUE
)
path <- tempfile(fileext = ".rds")
writeBin(recall@roots[[1]]@bytes, path)
production <- load_program(path)

identical(program_artifact_id(production), id)

The ID from program_artifact_id() is also recorded in every call’s metadata and trace, so a trace points to the exact program that was reviewed. graft keeps each acceptance, and any later withdrawal, in the stream’s history.

Save and inspect traces

A module records a trace for each call it makes, with token counts, latency, cost and model. Run the module a few times, then pin its traces next to the configuration:

run(classifier, review = "Great product!", .llm = chat)
#> $sentiment
#> [1] "positive"
run(classifier, review = "Not worth the money.", .llm = chat)
#> $sentiment
#> [1] "negative"
run(classifier, review = "Does the job.", .llm = chat)
#> $sentiment
#> [1] "neutral"

pin_trace(board, "sentiment-traces", classifier)
#> Creating new version '20260930T174637Z-0c98a'
#> Writing to pin 'sentiment-traces'
#> ✔ Pinned 4 traces: "sentiment-traces"
#> ℹ Total tokens: 941

export_traces() returns the same data as a tibble with one row per call, including the call made right after compiling:

export_traces(classifier)
#> # A tibble: 4 × 11
#>     timestamp latency_ms input_tokens cached_input_tokens output_tokens
#>         <dbl>      <dbl>        <int>               <int>         <int>
#> 1 1790790395.       875.          101                   0             7
#> 2 1790790397.       427.          200                   0             7
#> 3 1790790397.       381.          271                   0             7
#> 4 1790790398.       396.          341                   0             7
#> # ℹ 6 more variables: total_tokens <int>, cost <dbl>, model <chr>,
#> #   prompt_length <int>, program_artifact_id <chr>, trace_context <list>

The tibble leaves out prompts and responses, so you can share it without exposing the text your module handled. Add them with include_prompts = TRUE and include_outputs = TRUE; pin_trace() takes the same two arguments. summarize_traces(classifier) totals the tokens, cost and latency.

That completes the tutorials; Compile and optimize and Choose an optimizer take optimization further.