install.packages('ellmer')
# or the dev version
::pak('tidyverse/ellmer') pak
29 ποΈ gander
gander
goes beyond chores by extending the use of ellmer
models to,
βtalk[ing] to the objects in your R environment.β1
The previous LLM package weβve covered required background information and conditions to be provided via prompts. gander
is able to βpeek aroundβ in our current R environment to provide framing and context.
29.1 Configuration
First weβll install the ellmer
and gander
packages:
install.packages('gander')
# or the dev version
::pak("simonpcouch/gander") pak
Configure the model in your .Renviron
and .Rprofile
files (an API key is required):
# configure ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.
::edit_r_environ() usethis
# configure .gander_chat
::edit_r_profile()
usethisoptions(
.gander_chat = ellmer::chat_anthropic()
)
29.1.1 Keyboard shortcut
Configure the keyboard shortcut following the instructions on the package website (RStudio or Positron
).
I use the recommended Ctrl+Cmd+G (or Ctrl+Alt+G on Windows).
29.3 Developing with gander
To develop with gander
interactively, highlight the code youβd like to send to the model and use the keyboard shortcut:
The gander
addin includes a text box and a select input with Prefix, Replace, or Suffix.
29.3.1 Editing package functions
Lets assume weβve made the decision to change the reactable
tables to use the gt
package. Weβll start by converting a single reactable
table output (in R/mod_counts_tbl.R
) to use a gt
table.
After loading the new gt
table function, we can test the code by launching our app:
We can see the initial response throws an error, but weβll use this as a opportunity to explore how gander
works.
29.4 Context
We can view the background/context passed to the model using gander::peek()
. After giving us the model, turns, tokens and cost, the results are separated into system
, user
, and assistant
.
<Chat Anthropic/claude-3-7-sonnet-latest turns=3 tokens=1178/336 $0.01>
29.4.1 System
The system
portion tells us how each response is framed and provides, βadditional instructions to the model, shaping its responses to your needs.β4
ββ system [0] ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
You are a helpful but terse R data scientist. Respond only with valid R code:
no exposition, no backticks. Always provide a minimal solution and refrain
from unnecessary additions. Use tidyverse style and, when relevant, tidyverse
packages. For example, when asked to plot something, use ggplot2, or when
asked to transform data, using dplyr and/or tidyr unless explicitly instructed otherwise.
Think of this as the assistantβs βpersonality,β because it affects every response.
29.4.2 User
The user prompt section gives the model information on the R environment, file contents, etc. In this example we can see it provided the entire module (UI and server functions) and roxygen2
documentation.
Note the βUp to this point, the contents of my r file reads: β and βNow, convert the reactable output to a gt table: β delineation.
ββ user [1178] βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Up to this point, the contents of my r file reads:
\`\`\`r
#' UI for counts table module
#'
#' Creates a reactive table displaying count data. This function is designed
#' to work together with [mod_counts_tbl_server()].
#'
#' @param id A character string used to identify the namespace for the module.
#'
#' @return A `tagList` containing UI elements:
#' * A reactive table output that displays the count data
#'
#' @seealso [mod_counts_tbl_server()] for the server-side logic
#'
#' @examples
#' # UI implementation
#' ui <- fluidPage(
#' mod_counts_tbl_ui("counts1")
#' )
#'
#' # Server implementation
#' server <- function(input, output, session) {
#' mod_counts_tbl_server("counts1", counts_data = reactive(data))
#' }
#'
mod_counts_tbl_ui <- function(id) {
ns <- NS(id)
tagList(
reactable::reactableOutput(
outputId = ns("counts_table")
)
)
}
#' Server function for the count table module
#'
#' Creates a reactive table showing movies based on selected filters. This
#' function is designed to work together with a corresponding UI function.
#'
#' @param id A character string used to identify the namespace for the module.
#' @param vals A reactive expression that returns a list containing at least:
#' * `start_year`: numeric value for the earliest year to include
#' * `end_year`: numeric value for the latest year to include
#' * `chr_var`: symbol representing the variable to display alongside title
#'
#' @return Creates the following reactive elements within the module's namespace:
#' * `counts_table`: A reactive Reactable table with three columns:
#' - Title: The movie title
#' - The selected character variable from `vals()$chr_var`
#' - Thtr Rel Year: The theatrical release year
#'
#' The table includes styling with a dark background, white text, and features
#' such as highlighting, striping, and compact display.
#'
#' @details
#' The function filters the global `movies` dataset based on the year range
#' provided in `vals()`. Column names are normalized using the `name_case()`
#' function before displaying.
#'
#' @seealso [mod_counts_tbl_()] The corresponding UI function for this module
#'
#' @examples
#' # Server implementation
#' server <- function(input, output, session) {
#' # Create a reactive values list
#' selected_vals <- reactive({
#' list(
#' start_year = 2000,
#' end_year = 2010,
#' chr_var = sym("Critics Rating")
#' )
#' })
#'
#' # Call the module server
#' mod_counts_tbl_server("movie_table", selected_vals)
#' }
#'
\`\`\`
Now, convert the reactable output to a gt table:
\`\`\`r
mod_counts_tbl_server <- function(id, vals) {
moduleServer(id, function(input, output, session) {
output$counts_table <- reactable::renderReactable({
req(vals())
# subset
count_data <- subset(
movies,
thtr_rel_year >= vals()$start_year &
thtr_rel_year <= vals()$end_year
)
# Convert selected variable to snake_case
group_var <- name_case(as.character(vals()$chr_var), case = "lower")
# Count per group using tidy evaluation
tbl_data <- count_data |>
dplyr::group_by(.data[[group_var]]) |>
dplyr::summarise(n = dplyr::n(), .groups = "drop") |>
dplyr::arrange(dplyr::desc(n))
# Normalize column names in the data
names(tbl_data) <- name_case(names(tbl_data))
# reactable with dynamic row styling
reactable::reactable(tbl_data,
defaultPageSize = 25,
borderless = TRUE,
highlight = TRUE,
striped = TRUE,
compact = TRUE,
style = list(
backgroundColor = "#121212",
color = "#ffffff"
)
)
})
})
} \`\`\`
29.4.3 Assistant
Finally, the assistant
is the response from the model (given the system
and user
prompts).
ββ assistant [336] βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
mod_counts_tbl_server <- function(id, vals) {
moduleServer(id, function(input, output, session) {
output$counts_table <- gt::render_gt({
req(vals())
# subset
count_data <- subset(
movies,
thtr_rel_year >= vals()$start_year &
thtr_rel_year <= vals()$end_year
)
# Convert selected variable to snake_case
group_var <- name_case(as.character(vals()$chr_var), case = "lower")
# Count per group using tidy evaluation
tbl_data <- count_data |>
dplyr::group_by(.data[[group_var]]) |>
dplyr::summarise(n = dplyr::n(), .groups = "drop") |>
dplyr::arrange(dplyr::desc(n))
# Normalize column names in the data
names(tbl_data) <- name_case(names(tbl_data))
# gt table with dark theme styling
gt::gt(tbl_data) %>%
gt::tab_options(
table.background.color = "#121212",
column.labels.background.color = "#1e1e1e",
table.font.color = "#ffffff",
column.labels.font.color = "#ffffff",
table.border.top.style = "hidden",
table.border.bottom.style = "hidden"
) %>%
gt::opt_row_striping()
})
}) }
29.4.4 In practice
The context gander
provides is enough information to 1) ensure the response is aware of the corresponding UI function, and 2) notice weβre developing an R package (i.e., use explicit namespacing with pkg::fun()
).
The errors weβre encountering are due to incorrect arguments passed to gt::tab_options()
:
::tab_options(
gttable.background.color = "#121212",
column.labels.background.color = "#1e1e1e",
table.font.color = "#ffffff",
column.labels.font.color = "#ffffff",
table.border.top.style = "hidden",
table.border.bottom.style = "hidden"
)
- 1
-
Should be
column_labels.background.color
- 2
-
Doesnβt exist (uses
table.font.color
)
After making the changes to the server-side gt
table code, we need to remember to update the corresponding UI code with the gt
rendering function (recall that we didnβt specify any changes to this function in the prompt).
After updating the UI module function and loading the changes, we can see the Counts table below:
29.5 Graph aesthetics
gander
is incredibly helpful for making small adjustments to ggplot2
graphs. I noticed the text size was too small in the graphs on the Distribution tabs:
I asked the chatbot to make the font size in the graph adjust with the display.
Prompt: βI want the font in the ggplot2 graph to adjust with the screen sizeβ
gander
only made a few changes to the ggplot2
code in the modules, but these changes definitely reduced a lot of time because the ggplot2::theme()
arguments can be difficult to remember, and usually take multiple iterations to get correct:
::theme(
ggplot2strip.text = ggplot2::element_text(
color = "#ffffff",
size = ggplot2::rel(1.05)
),axis.text = ggplot2::element_text(
color = "#ffffff",
size = ggplot2::rel(1.075)
),axis.title = ggplot2::element_text(
color = "#ffffff",
size = ggplot2::rel(1.15)
) )
Small changes like these can take up significant time to read through documentation/examples, although a foundational understanding of ggplot2
makes it easier to debug any errors the generated code produces.
29.6 External resources
To full test the functionality of gander, I asked it
gander
is the third LLM R package weβve covered by developer Simon Couch (we also coveredensure
in Section 16.1 andchores
in Chapter 28). If youβre interested in staying up on LLMs and their use in R package development, I highly recommend regularly checking his blog.β©οΈThis application uses the
page_navbar
layout frombslib
. Review itβs structure in Section 28.1.β©οΈSee the Section 28.4.1 and previous Section 13.3 sections.β©οΈ
Read more about this in the What is a prompt section? of the
ellmer
documentation.β©οΈ