# install.packages('pak')
::pak('mjfrigaard/shinypak') pak
27 ellmer
The ellmer
package servers as the ‘backbone’ to using LLMs in R (we used it to set up the ensure
package for writing unit tests in Section 16.1). The package has been thoughtfully developed, is easy to use, and has excellent documentation and examples. This chapter will briefly cover using ellmer
’s chat tool in app-package development.
27.1 Installing ellmer
Install ellmer
like you would any other package:
install.packages('ellmer')
# or the dev version
::pak('tidyverse/ellmer') pak
27.1.1 LLM Provider
After installing the package, we need to set up an LLM model provider. ellmer
provides 10+ LLM provider options, but for this example, I’ll use the OpenAI model, which requires an API key (OPENAI_API_KEY
) which can be set in the .Renviron
file.1
27.1.2 Using the chat
To start a chat, we can call ellmer::chat_openai()
and specify the model
we want to use.2 We should also be sure to record any system_prompts
and the number of turns
we’ll be using.
library(ellmer)
<- chat_openai(model = "o1-mini") chat
Using model = "o1-mini".
The chat
object can be used directly in the console to submit prompts and get responses.
$chat("
chat What are the benefits of developing my Shiny application in an R package?
Answer in a markdown-formatted bulleted list.
")
- **Enhanced Code Organization**
- Structure your application with a standardized file hierarchy, making the codebase
easier to navigate and maintain.
- **Reusable Components**
- Develop modular functions and components that can be easily reused across different
projects or parts of the application.
- **Dependency Management**
- Clearly specify and manage package dependencies, ensuring that all necessary
libraries are installed and up to date.
- **Simplified Testing**
- Utilize built-in testing frameworks to implement unit tests and ensure the
reliability of your application.
- **Comprehensive Documentation**
- Provide detailed documentation for functions and features using tools like
Roxygen2, improving understandability for users and contributors.
- **Namespace Control**
- Avoid function name conflicts and manage the scope of your functions effectively
through namespaces.
- **Ease of Distribution**
- Share your Shiny application effortlessly by distributing it as an R package
through platforms like CRAN or GitHub.
- **Version Control Integration**
- Manage and track changes systematically with version control systems, facilitating
collaboration and rollback capabilities.
- **Streamlined Deployment**
- Leverage package tools for easier deployment to servers or Shiny hosting services,
ensuring a smoother launch process.
- **Community Contributions**
- Enable others to contribute to your application more easily by following standard package development practices, fostering community growth.
We can copy and paste the output into any Markdown document (vignettes, README, etc.).
27.1.3 Prompts
If we plan on having a conversation with an LLM, we should pick a location to store our prompts (and responses). The ellmer
authors recommend storing these in inst/prompts
, with a sensible naming convention.3
27.2 Movie explorer redux
We’re going to use ellmer
to help us convert an existing Shiny app into a Shiny app-package. The application comes from the Shiny Examples GitHub repo. The README.md
from the original app is available below:
Launch app with the shinypak
package:
launch('24_llm-ellmer')
The application files are also stored in the inst/51-movie-explorer
folder of this branch:
inst/51-movies-explorer/
├── DESCRIPTION
├── README.md
├── global.R
├── movies.db
├── server.R
└── ui.R
1 directory, 6 files
27.2.1 System Prompt
We’ll start by passing a system_prompt
to chat$chat()
to provide some additional information on how the model should respond:
I’ve modeled this system_prompt
off the example in the ellmer
documentation,4 but this can be used to specify expertise, coding style preferences (tidyverse/base R), etc.
27.2.2 Prompt 1: update
Prompt 1: The first prompt (stored in inst/prompts/prompt-01-refactor-mods-functions.md
) will focus on creating modules, UI and server functions, and a standalone app function. We’ll provide the ui.R
and server.R
code, and some instructions for module and function names. It’s recommended to provide prompts using Markdown,5 and I’d used escape characters for the R code chunks.
27.2.2.1 Response 1: update
The response is generated in the Console:
We can see the response contains the modules and functions in separate code chunks. I’ve organized them into the callout box below:
We’ll create an app folder in inst/db_app
, then structure the files like a ‘mini’ R package:
inst/db_app/
├── R
│ ├── db_app_server.R
│ ├── db_app_ui.R
│ ├── global.R
│ ├── mod_graph_inputs.R
│ └── mod_graph_output.R
├── app.R
└── movies.db
2 directories, 7 files
After organizing the files into the structure above, we can run the application using the launch_db_app()
function (stored in app.R
).
The application launches and we can see the plotly scatter plot renders successfully. The inputs in the sidebar are somewhat cramped and out of view, but we can adjust this with future prompts (or by manually editing the UI components).
27.2.3 Prompt 2: refactor
Prompt 2: The second prompt will focus on creating utility functions for the database connection and joining/wrangling the data. I’ve stored the instructions in inst/prompts/prompt-02-utility-functions.md
(we’ll use the same system_prompt
).
The goal with this prompt is to create testable utility functions and address the dplyr::src_sqlite()
warning we’re getting from our database connection:
Warning: `src_sqlite()` was deprecated in dplyr 1.0.0. ℹ Please use `tbl()` directly with a database connection
27.2.3.1 Response 2: refactor
The response includes two new functions: connect_database()
and get_all_movies()
connect_database()
still uses dplyr::src_sqlite()
, so we’ll rewrite this function to address the warning, move the movies.db
file into the inst/extdata
6 folder, and access the file with system.file()
:
<- function(pkg, db) {
connect_database # Create connection to movies database
<- DBI::dbConnect(RSQLite::SQLite(),
con system.file("extdata", db, package = pkg))
con }
We’ll also need to update the db_app_server()
function to use the new utility functions:
<- function(input, output, session) {
db_app_server
# Set up connection to database tables on app start
<- connect_database(pkg = "sap", db = "movies.db")
db
# Join tables, filter out those with <10 reviews, and select
<- get_all_movies(db = db)
all_movies
# Call module for inputs
<- mod_graph_inputs_server(
inputs_reactive id = "graph_inputs",
all_movies = all_movies
)
# Call module for outputs
mod_graph_output_server(
id = "graph_outputs",
inputs_list = inputs_reactive
) }
- 1
-
Replaces the
dplyr::src_sqlite()
anddplyr::tbl()
calls.
- 2
-
Replaces the
dplyr::inner_join()
,dplyr::filter()
, anddplyr::select()
functions.
To implement these changes, we’ll need to add the new function files to the inst/db_app/R
folder:
inst/db_app/
├── R
│ ├── connect_database.R
│ ├── db_app_server.R
│ ├── db_app_ui.R
│ ├── get_all_movies.R
│ ├── global.R
│ ├── mod_graph_inputs.R
│ └── mod_graph_output.R
└── app.R
2 directories, 8 files
This results in the following application:
We can see the dplyr::src_sqlite()
warning is gone and the data is correctly loaded in the application.
27.2.4 Prompt 3: overhaul
Prompt 3: We’ll get even more detailed in the third prompt (stored in inst/prompts/prompt-03-app-ast-design.md
). In these instructions, we’ll include an abstract syntax tree (courtesy of lobstr::ast()
) with proposed function names and descriptions:
█─launch_app
├─█─db_app_ui
│ ├─█─mod_select_input_ui
│ ├─█─mod_slider_input_ui
│ ├─█─mod_text_input_ui
│ ├─█─mod_points_plotly_ui
│ └─█─mod_table_ui
└─█─db_app_server
├─█─db_con
│ └─█─join_movies_tbls
├─█─mod_slider_input_server
├─█─mod_select_input_server
├─█─mod_text_input_server
├─█─mod_table_server
└─█─mod_points_plotly_server └─█─scatter_plotly
I’ve found this practice is helpful when writing prompts that request nested functions (like modules). We’ll also instruct the model to return the functions with roxygen2
documentation.
27.2.4.1 Response 3: overhaul
View the results in the callout box below (or in the Response section on GitHub).
We’ll create these files in the R/
folder and add a shinydb
folder to the inst/
directory. After making some minor adjustments to the module names, we can include this application to our launch_app()
function.
show/hide updated launch_app()
<- function(app = NULL, options = list(), run = "p", ...) {
launch_app if (interactive()) {
display_type(run = run)
}
if (is.null(app)) {
<- "movies"
app
}
logr_msg(glue::glue("Launching app: {app}"),
level = "INFO"
)
tryCatch(
{if (app == "bslib") {
shinyApp(
ui = movies_ui(bslib = TRUE),
server = movies_server,
options = options
)else if (app == "ggp2") {
} shinyAppDir(
appDir = system.file("tidy-movies", package = "sap"),
options = options
)else if (app == "quarto") {
} ::quarto_preview(
quartosystem.file("quarto", "index.qmd", package = "sap"),
render = "all"
)else if (app == "assist") {
} shinyAppDir(
appDir = system.file("shiny-assist/movies", package = "sap"),
options = options
)else if (app == "db") {
} shinyAppDir(
appDir = system.file("shinydb", package = "sap"),
options = options
)else {
} shinyApp(
ui = movies_ui(...),
server = movies_server,
options = options
)
}
},error = function(e) {
logr_msg(glue::glue("FATAL: Application failed to launch. Reason: {e$message}"),
level = "FATAL"
)
stop("Application launch failed. Check logs for details.")
}
) }
Our updated shinydb
application is below:
launch_app(app = 'db')
As we can see, the sidebar is less crowded, and with the information we provided, the chat was able to figure out where each utility function should be placed.
27.3 Finishing touches
From here, we can continue using ellmer
or ensure
(see Section 16.1) to write unit tests for these new functions. We should also make sure any new dependencies are properly managed (i.e., using attachment::att_amend_desc()
)
::att_amend_desc() attachment
Updating sap documentation
Writing NAMESPACE
ℹ Loading sap [+] 5 package(s) added: DBI, dplyr, plotly, reactable, RSQLite.
Recap
The ellmer
package simplifies using LLMs, making it easier to include them as a tool in regular app/package development. Storing prompts ensures your project is well documented (especially when converting back to previous versions).
Edit this file using the
usethis::edit_r_environ()
function.↩︎The
chat_openai()
documentation recommends “explicitly choosing a model for all but the most casual use.”↩︎“If you have multiple prompts, give them informative names like
prompt-extract-metadata.md
orprompt-summarize-text.md
. If you’re writing a package, put your prompt(s) ininst/prompts
” -ellmer
documentation↩︎Use
system_prompt
s to ‘provide additional instructions to the model, shaping its responses to your needs’ - What is a prompt?ellmer
documentation↩︎Markdown is “quite readable to LLMs (and humans), and it allows you to do things like use headers to divide up a prompt into sections and itemised lists to enumerate multiple options.” Best Practices,
ellmer
documentation↩︎The
inst/extdata
folder is covered in R Packages, 2nd ed.↩︎