diff --git a/pkg-r/DESCRIPTION b/pkg-r/DESCRIPTION
index 85ae1c7e..0cc0e5a3 100644
--- a/pkg-r/DESCRIPTION
+++ b/pkg-r/DESCRIPTION
@@ -27,6 +27,7 @@ Imports:
fastmap,
htmltools,
jsonlite,
+ lifecycle,
promises (>= 1.3.2),
rlang,
S7,
diff --git a/pkg-r/NAMESPACE b/pkg-r/NAMESPACE
index 5bb459a5..b707be29 100644
--- a/pkg-r/NAMESPACE
+++ b/pkg-r/NAMESPACE
@@ -4,9 +4,9 @@ export(chat_app)
export(chat_append)
export(chat_append_message)
export(chat_clear)
-export(chat_enable_bookmarking)
export(chat_mod_server)
export(chat_mod_ui)
+export(chat_restore)
export(chat_ui)
export(markdown_stream)
export(output_markdown_stream)
@@ -18,4 +18,5 @@ importFrom(coro,async)
importFrom(htmltools,HTML)
importFrom(htmltools,css)
importFrom(htmltools,tag)
+importFrom(lifecycle,deprecated)
importFrom(shiny,getDefaultReactiveDomain)
diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md
index 7b9d9d60..5a12d946 100644
--- a/pkg-r/NEWS.md
+++ b/pkg-r/NEWS.md
@@ -2,7 +2,8 @@
## New features
-* Added `chat_enable_bookmarking()` which adds Shiny bookmarking hooks to save and restore the `{ellmer}` chat client. (#28)
+* Added `chat_restore()` which adds Shiny bookmarking hooks to save and restore the `{ellmer}` chat client. (#28, #82)
+
* Added `update_chat_user_input()` for programmatically updating the user input of a chat UI element. (#78)
## Improvements
diff --git a/pkg-r/R/chat_app.R b/pkg-r/R/chat_app.R
index 08cc9957..4d03f796 100644
--- a/pkg-r/R/chat_app.R
+++ b/pkg-r/R/chat_app.R
@@ -58,10 +58,17 @@
#' }
#'
#' @param client A chat object created by \pkg{ellmer}, e.g.
-#' [ellmer::chat_openai()] and friends.
+#' [ellmer::chat_openai()] and friends. This argument is deprecated in
+#' `chat_mod_ui()` because the client state is now managed by
+#' `chat_mod_server()`.
#' @param ... In `chat_app()`, additional arguments are passed to
#' [shiny::shinyApp()]. In `chat_mod_ui()`, additional arguments are passed to
#' [chat_ui()].
+#' @param bookmark_store The bookmarking store to use for the app. Passed to
+#' `enable_bookmarking` in [shiny::shinyApp()]. Defaults to `"url"`, which
+#' uses the URL to store the chat state. URL-based bookmarking is limited in
+#' size; use `"server"` to store the state on the server side without size
+#' limitations; or disable bookmarking by setting this to `"disable"`.
#'
#' @returns
#' * `chat_app()` returns a [shiny::shinyApp()] object.
@@ -73,12 +80,12 @@
#' app is suitable for interactive use by a single user; do not use
#' `chat_app()` in a multi-user Shiny app context.
#' @export
-chat_app <- function(client, ...) {
+chat_app <- function(client, ..., bookmark_store = "url") {
check_ellmer_chat(client)
ui <- function(req) {
bslib::page_fillable(
- chat_mod_ui("chat", client = client, height = "100%"),
+ chat_mod_ui("chat", height = "100%"),
shiny::actionButton(
"close_btn",
label = "",
@@ -96,7 +103,7 @@ chat_app <- function(client, ...) {
})
}
- shiny::shinyApp(ui, server, ...)
+ shiny::shinyApp(ui, server, ..., enableBookmarking = bookmark_store)
}
check_ellmer_chat <- function(client) {
@@ -107,34 +114,25 @@ check_ellmer_chat <- function(client) {
#' @describeIn chat_app A simple chat app module UI.
#' @param id The chat module ID.
-#' @param messages Initial messages shown in the chat, used when `client` is not
-#' provided or when the chat `client` doesn't already contain turns. Passed to
-#' `messages` in [chat_ui()].
+#' @param messages Initial messages shown in the chat, used only when `client`
+#' (in `chat_mod_ui()`) doesn't already contain turns. Passed to `messages`
+#' in [chat_ui()].
#' @export
-chat_mod_ui <- function(id, ..., client = NULL, messages = NULL) {
- if (!is.null(client)) {
- check_ellmer_chat(client)
-
- client_msgs <- map(client$get_turns(), function(turn) {
- content <- ellmer::contents_markdown(turn)
- if (is.null(content) || identical(content, "")) {
- return(NULL)
- }
- list(role = turn@role, content = content)
- })
- client_msgs <- compact(client_msgs)
-
- if (length(client_msgs)) {
- if (!is.null(messages)) {
- warn(
- "`client` was provided and has initial messages, `messages` will be ignored."
- )
- }
- messages <- client_msgs
- }
+chat_mod_ui <- function(
+ id,
+ ...,
+ client = deprecated(),
+ messages = NULL
+) {
+ if (lifecycle::is_present(client)) {
+ lifecycle::deprecate_warn(
+ "0.3.0",
+ "chat_mod_ui(client = )",
+ "chat_mod_server(client = )"
+ )
}
- shinychat::chat_ui(
+ chat_ui(
shiny::NS(id, "chat"),
messages = messages,
...
@@ -142,8 +140,14 @@ chat_mod_ui <- function(id, ..., client = NULL, messages = NULL) {
}
#' @describeIn chat_app A simple chat app module server.
+#' @inheritParams chat_restore
#' @export
-chat_mod_server <- function(id, client) {
+chat_mod_server <- function(
+ id,
+ client,
+ bookmark_on_input = TRUE,
+ bookmark_on_response = TRUE
+) {
check_ellmer_chat(client)
append_stream_task <- shiny::ExtendedTask$new(
@@ -158,6 +162,14 @@ chat_mod_server <- function(id, client) {
)
shiny::moduleServer(id, function(input, output, session) {
+ chat_restore(
+ "chat",
+ client,
+ session = session,
+ bookmark_on_input = bookmark_on_input,
+ bookmark_on_response = bookmark_on_response
+ )
+
shiny::observeEvent(input$chat_user_input, {
append_stream_task$invoke(
client,
diff --git a/pkg-r/R/chat_enable_bookmarking.R b/pkg-r/R/chat_restore.R
similarity index 84%
rename from pkg-r/R/chat_enable_bookmarking.R
rename to pkg-r/R/chat_restore.R
index 81163182..8f9b77fb 100644
--- a/pkg-r/R/chat_enable_bookmarking.R
+++ b/pkg-r/R/chat_restore.R
@@ -1,7 +1,8 @@
#' Add Shiny bookmarking for shinychat
#'
#' @description
-#' Adds Shiny bookmarking hooks to save and restore the \pkg{ellmer} chat `client`.
+#' Adds Shiny bookmarking hooks to save and restore the \pkg{ellmer} chat
+#' `client`. Also restores chat messages from the history in the `client`.
#'
#' If either `bookmark_on_input` or `bookmark_on_response` is `TRUE`, the Shiny
#' App's bookmark will be automatically updated without showing a modal to the
@@ -13,6 +14,10 @@
#' then you may need to implement your own `session$onRestore()` (and possibly
#' `session$onBookmark`) handler to restore any additional state.
#'
+#' To avoid restoring chat history from the `client`, you can ensure that the
+#' history is empty by calling `client$set_turns(list())` before passing the
+#' client to `chat_restore()`.
+#'
#' @param id The ID of the chat element
#' @param client The \pkg{ellmer} LLM chat client.
#' @param ... Used for future parameter expansion.
@@ -39,7 +44,7 @@
#' echo = TRUE
#' )
#' # Update bookmark to chat on user submission and completed response
-#' chat_enable_bookmarking("chat", chat_client)
+#' chat_restore("chat", chat_client)
#'
#' observeEvent(input$chat_user_input, {
#' stream <- chat_client$stream_async(input$chat_user_input)
@@ -50,7 +55,7 @@
#' # Enable bookmarking!
#' shinyApp(ui, server, enableBookmarking = "server")
#' @export
-chat_enable_bookmarking <- function(
+chat_restore <- function(
id,
client,
...,
@@ -72,23 +77,12 @@ chat_enable_bookmarking <- function(
if (is.null(session)) {
rlang::abort(
- "A `session` must be provided. Be sure to call `chat_enable_bookmarking()` where a session context is available."
+ "A `session` must be provided. Be sure to call `chat_restore()` where a session context is available."
)
}
# Verify bookmark store is not disabled. Bookmark options: "disable", "url", "server"
bookmark_store <- shiny::getShinyOption("bookmarkStore", "disable")
- # TODO: Q - I feel this should be removed. Since we are only adding hooks, it doesn't matter if it's enabled or not. If the user diables chat, it would be very annoying to receive error messages for code they may not own.
- if (bookmark_store == "disable") {
- rlang::abort(
- paste0(
- "Error: Shiny bookmarking is not enabled. ",
- "Please enable bookmarking in your Shiny app either by calling ",
- "`shiny::enableBookmarking(\"server\")` or by setting the parameter in ",
- "`shiny::shinyApp(enableBookmarking = \"server\")`"
- )
- )
- }
# Exclude works with bookmark names
excluded_names <- session$getBookmarkExclude()
@@ -115,6 +109,11 @@ chat_enable_bookmarking <- function(
state$values[[id]] <- client_state
})
+ cancel_set_ui <- shiny::observe({
+ client_set_ui(client, id = id)
+ cancel_set_ui$destroy()
+ })
+
# Restore
cancel_on_restore_client <-
session$onRestore(function(state) {
@@ -123,11 +122,14 @@ chat_enable_bookmarking <- function(
return()
}
+ cancel_set_ui$destroy()
client_set_state(client, client_state)
# Set the UI
- chat_clear(id)
- client_set_ui(client, id = id)
+ shiny::withReactiveDomain(session, {
+ chat_clear(id)
+ client_set_ui(client, id = id)
+ })
})
# Update URL
@@ -151,13 +153,15 @@ chat_enable_bookmarking <- function(
cancel_update_bookmark <- NULL
if (bookmark_on_input || bookmark_on_response) {
cancel_update_bookmark <-
- # Update the query string when bookmarked
- shiny::onBookmarked(function(url) {
- shiny::updateQueryString(url)
+ shiny::withReactiveDomain(session$rootScope(), {
+ # Update the query string when bookmarked
+ shiny::onBookmarked(function(url) {
+ shiny::updateQueryString(url)
+ })
})
}
- # Set callbacks to cancel if `chat_enable_bookmarking(id, client)` is called again with the same id
+ # Set callbacks to cancel if `chat_restore(id, client)` is called again with the same id
# Only allow for bookmarks for each chat once. Last bookmark method would win if all values were to be computed.
# Remove previous `on*()` methods under same hash (.. odd author behavior)
previous_info <- get_session_chat_bookmark_info(session, id)
@@ -169,7 +173,7 @@ chat_enable_bookmarking <- function(
}
}
- # Store callbacks to cancel in case a new call to `chat_enable_bookmarking(id, client)` is called with the same id
+ # Store callbacks to cancel in case a new call to `chat_restore(id, client)` is called with the same id
set_session_chat_bookmark_info(
session,
id,
@@ -205,7 +209,7 @@ chat_update_bookmark <- function(
prom <-
promises::then(stream_promise, function(stream) {
# Force a bookmark update when the stream ends!
- session$doBookmark()
+ shiny::isolate(session$doBookmark())
})
return(prom)
diff --git a/pkg-r/R/client_state.R b/pkg-r/R/client_state.R
index a146e0bd..ba89f97d 100644
--- a/pkg-r/R/client_state.R
+++ b/pkg-r/R/client_state.R
@@ -73,7 +73,8 @@ method(client_set_state, S7::new_S3_class(c("Chat", "R6"))) <-
replayed_turns <- lapply(
recorded_turns,
- ellmer::contents_replay
+ ellmer::contents_replay,
+ tools = client$get_tools()
)
client$set_turns(replayed_turns)
@@ -84,15 +85,10 @@ method(client_set_ui, S7::new_S3_class(c("Chat", "R6"))) <-
function(client, ..., id) {
# TODO-future: Disable bookmarking when restoring. Leverage `tryCatch(finally={})`
# TODO-barret-future; In shinychat, make this a single/internal custom message call to send all the messages at once (and then scroll)
- lapply(client$get_turns(), function(turn) {
- chat_append(
- id,
- # Use `contents_markdown()` as it handles image serialization
- # TODO: Use `contents_shinychat()` from posit-dev/shinychat#52
- ellmer::contents_markdown(turn),
- # turn_info$contents,
- role = turn@role
- )
+
+ msgs <- contents_shinychat(client)
+ lapply(msgs, function(x) {
+ chat_append(id, x$content, role = x$role)
})
}
diff --git a/pkg-r/R/contents_shinychat.R b/pkg-r/R/contents_shinychat.R
new file mode 100644
index 00000000..16fa7758
--- /dev/null
+++ b/pkg-r/R/contents_shinychat.R
@@ -0,0 +1,14 @@
+contents_shinychat <- function(client) {
+ # TODO(garrick): placeholder, will be replaces with S7 generic
+ check_ellmer_chat(client)
+
+ client_msgs <- map(client$get_turns(), function(turn) {
+ content <- ellmer::contents_markdown(turn)
+ if (is.null(content) || identical(content, "")) {
+ return(NULL)
+ }
+ list(role = turn@role, content = content)
+ })
+
+ compact(client_msgs)
+}
diff --git a/pkg-r/R/shinychat-package.R b/pkg-r/R/shinychat-package.R
index 554033cf..617fd5dd 100644
--- a/pkg-r/R/shinychat-package.R
+++ b/pkg-r/R/shinychat-package.R
@@ -2,10 +2,11 @@
"_PACKAGE"
## usethis namespace: start
-#' @importFrom coro async
-#' @importFrom htmltools tag css HTML
#' @import rlang
#' @import S7
+#' @importFrom coro async
+#' @importFrom htmltools tag css HTML
+#' @importFrom lifecycle deprecated
## usethis namespace: end
NULL
diff --git a/pkg-r/man/chat_app.Rd b/pkg-r/man/chat_app.Rd
index 844e77eb..f1646cc5 100644
--- a/pkg-r/man/chat_app.Rd
+++ b/pkg-r/man/chat_app.Rd
@@ -6,25 +6,42 @@
\alias{chat_mod_server}
\title{Open a live chat application in the browser}
\usage{
-chat_app(client, ...)
+chat_app(client, ..., bookmark_store = "url")
-chat_mod_ui(id, ..., client = NULL, messages = NULL)
+chat_mod_ui(id, ..., client = deprecated(), messages = NULL)
-chat_mod_server(id, client)
+chat_mod_server(
+ id,
+ client,
+ bookmark_on_input = TRUE,
+ bookmark_on_response = TRUE
+)
}
\arguments{
\item{client}{A chat object created by \pkg{ellmer}, e.g.
-\code{\link[ellmer:chat_openai]{ellmer::chat_openai()}} and friends.}
+\code{\link[ellmer:chat_openai]{ellmer::chat_openai()}} and friends. This argument is deprecated in
+\code{chat_mod_ui()} because the client state is now managed by
+\code{chat_mod_server()}.}
\item{...}{In \code{chat_app()}, additional arguments are passed to
\code{\link[shiny:shinyApp]{shiny::shinyApp()}}. In \code{chat_mod_ui()}, additional arguments are passed to
\code{\link[=chat_ui]{chat_ui()}}.}
+\item{bookmark_store}{The bookmarking store to use for the app. Passed to
+\code{enable_bookmarking} in \code{\link[shiny:shinyApp]{shiny::shinyApp()}}. Defaults to \code{"url"}, which
+uses the URL to store the chat state. URL-based bookmarking is limited in
+size; use \code{"server"} to store the state on the server side without size
+limitations; or disable bookmarking by setting this to \code{"disable"}.}
+
\item{id}{The chat module ID.}
-\item{messages}{Initial messages shown in the chat, used when \code{client} is not
-provided or when the chat \code{client} doesn't already contain turns. Passed to
-\code{messages} in \code{\link[=chat_ui]{chat_ui()}}.}
+\item{messages}{Initial messages shown in the chat, used only when \code{client}
+(in \code{chat_mod_ui()}) doesn't already contain turns. Passed to \code{messages}
+in \code{\link[=chat_ui]{chat_ui()}}.}
+
+\item{bookmark_on_input}{A logical value determines if the bookmark should be updated when the user submits a message. Default is \code{TRUE}.}
+
+\item{bookmark_on_response}{A logical value determines if the bookmark should be updated when the response stream completes. Default is \code{TRUE}.}
}
\value{
\itemize{
diff --git a/pkg-r/man/chat_enable_bookmarking.Rd b/pkg-r/man/chat_restore.Rd
similarity index 83%
rename from pkg-r/man/chat_enable_bookmarking.Rd
rename to pkg-r/man/chat_restore.Rd
index af25d651..cde25716 100644
--- a/pkg-r/man/chat_enable_bookmarking.Rd
+++ b/pkg-r/man/chat_restore.Rd
@@ -1,10 +1,10 @@
% Generated by roxygen2: do not edit by hand
-% Please edit documentation in R/chat_enable_bookmarking.R
-\name{chat_enable_bookmarking}
-\alias{chat_enable_bookmarking}
+% Please edit documentation in R/chat_restore.R
+\name{chat_restore}
+\alias{chat_restore}
\title{Add Shiny bookmarking for shinychat}
\usage{
-chat_enable_bookmarking(
+chat_restore(
id,
client,
...,
@@ -30,7 +30,8 @@ chat_enable_bookmarking(
Returns nothing (\code{invisible(NULL)}).
}
\description{
-Adds Shiny bookmarking hooks to save and restore the \pkg{ellmer} chat \code{client}.
+Adds Shiny bookmarking hooks to save and restore the \pkg{ellmer} chat
+\code{client}. Also restores chat messages from the history in the \code{client}.
If either \code{bookmark_on_input} or \code{bookmark_on_response} is \code{TRUE}, the Shiny
App's bookmark will be automatically updated without showing a modal to the
@@ -41,6 +42,10 @@ the \code{client}'s state doesn't properly capture the chat's UI (i.e., a
transformation is applied in-between receiving and displaying the message),
then you may need to implement your own \code{session$onRestore()} (and possibly
\code{session$onBookmark}) handler to restore any additional state.
+
+To avoid restoring chat history from the \code{client}, you can ensure that the
+history is empty by calling \code{client$set_turns(list())} before passing the
+client to \code{chat_restore()}.
}
\examples{
\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf}
@@ -61,7 +66,7 @@ server <- function(input, output, session) {
echo = TRUE
)
# Update bookmark to chat on user submission and completed response
- chat_enable_bookmarking("chat", chat_client)
+ chat_restore("chat", chat_client)
observeEvent(input$chat_user_input, {
stream <- chat_client$stream_async(input$chat_user_input)
diff --git a/pkg-r/man/figures/lifecycle-deprecated.svg b/pkg-r/man/figures/lifecycle-deprecated.svg
new file mode 100644
index 00000000..b61c57c3
--- /dev/null
+++ b/pkg-r/man/figures/lifecycle-deprecated.svg
@@ -0,0 +1,21 @@
+
diff --git a/pkg-r/man/figures/lifecycle-experimental.svg b/pkg-r/man/figures/lifecycle-experimental.svg
new file mode 100644
index 00000000..5d88fc2c
--- /dev/null
+++ b/pkg-r/man/figures/lifecycle-experimental.svg
@@ -0,0 +1,21 @@
+
diff --git a/pkg-r/man/figures/lifecycle-stable.svg b/pkg-r/man/figures/lifecycle-stable.svg
new file mode 100644
index 00000000..9bf21e76
--- /dev/null
+++ b/pkg-r/man/figures/lifecycle-stable.svg
@@ -0,0 +1,29 @@
+
diff --git a/pkg-r/man/figures/lifecycle-superseded.svg b/pkg-r/man/figures/lifecycle-superseded.svg
new file mode 100644
index 00000000..db8d757f
--- /dev/null
+++ b/pkg-r/man/figures/lifecycle-superseded.svg
@@ -0,0 +1,21 @@
+
diff --git a/pkg-r/pkgdown/_pkgdown.yml b/pkg-r/pkgdown/_pkgdown.yml
index a1bef232..ca6b3487 100644
--- a/pkg-r/pkgdown/_pkgdown.yml
+++ b/pkg-r/pkgdown/_pkgdown.yml
@@ -50,4 +50,4 @@ reference:
- title: Bookmark chat history
contents:
- - chat_enable_bookmarking
+ - chat_restore