29  πŸ—οΈ gander

Published

2025-05-06

WARNING

This chapter is being developed. Thank you for your patience.

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.

I’ve created the shinypak R package in an effort to make each section accessible and easy to follow. Install shinypak using pak (or remotes):

install.packages('pak')
pak::pak('mjfrigaard/shinypak')
library(shinypak)

List the apps in this chapter:

list_apps(regex = '^29')

Launch apps with launch()

launch(app = '29_llm-gander')

Download apps with get_app()

get_app(app = '29_llm-gander')

29.1 Configuration

First we’ll install the ellmer and gander packages:

install.packages('ellmer')
# or the dev version
pak::pak('tidyverse/ellmer')
install.packages('gander')
# or the dev version
pak::pak("simonpcouch/gander")

Configure the model in your .Renviron and .Rprofile files (an API key is required):

# configure ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.
usethis::edit_r_environ() 
# configure .gander_chat
usethis::edit_r_profile() 
options(
  .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.2 Navbar App

We’re going to continue the development of our navbar movie review from the previous branch.2 After loading, documenting, and installing sap, we’ll perform a β€˜test launch’:

Initial launch

Initial launch

In the previous chapter we used the chores package to include log messages, but we can control the verbosity in the Console by adjusting the threshold in R/launch_app.R. 3

logger::log_threshold(level = "INFO")

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:

Ctrl+Cmd+G or gander::gander_addin()

Ctrl+Cmd+G or gander::gander_addin()

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.

Suffix responses to preserve orginal code

Suffix responses to preserve orginal code
TIP: Suffix

I recommend using Suffix instead of the default Replace for package functions. It won’t replace the roxygen2 documentation, and it gives you a chance to review/compare the reponse to the original code.

After loading the new gt table function, we can test the code by launching our app:

gt table conversion error

gt table conversion error

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():

  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"
  ) 
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:

Updated Counts gt table

Updated Counts gt table

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:

Text size on Distribution tab

Text size on Distribution tab

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:

ggplot2::theme(
  strip.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)
        )
)

Updated text on Distribution tab

Updated text on Distribution tab

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


  1. gander is the third LLM R package we’ve covered by developer Simon Couch (we also covered ensure in Section 16.1 and chores 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.β†©οΈŽ

  2. This application uses the page_navbar layout from bslib. Review it’s structure in Section 28.1.β†©οΈŽ

  3. See the Section 28.4.1 and previous Section 13.3 sections.β†©οΈŽ

  4. Read more about this in the What is a prompt section? of the ellmer documentation.β†©οΈŽ