@@ -82,21 +82,21 @@ chat_ui <- function(
8282
8383 attrs <- rlang :: list2(... )
8484 if (! all(nzchar(rlang :: names2(attrs )))) {
85- stop (" All arguments in ... must be named." )
85+ rlang :: abort (" All arguments in ... must be named." )
8686 }
8787
8888 message_tags <- lapply(messages , function (x ) {
8989 if (is.character(x )) {
9090 x <- list (content = x , role = " assistant" )
9191 } else if (is.list(x )) {
9292 if (! (" content" %in% names(x ))) {
93- stop (" Each message must have a 'content' key." )
93+ rlang :: abort (" Each message must have a 'content' key." )
9494 }
9595 if (! (" role" %in% names(x ))) {
96- stop (" Each message must have a 'role' key." )
96+ rlang :: abort (" Each message must have a 'role' key." )
9797 }
9898 } else {
99- stop (" Each message must be a string or a named list." )
99+ rlang :: abort (" Each message must be a string or a named list." )
100100 }
101101
102102 if (isTRUE(x [[" role" ]] == " user" )) {
@@ -129,14 +129,65 @@ chat_ui <- function(
129129 res
130130}
131131
132- # ' Append an assistant response to a chat control
132+ # ' Append an assistant response (or user message) to a chat control
133133# '
134+ # ' @description
135+ # ' The `chat_append` function appends a message to an existing chat control. The
136+ # ' `response` can be a string, string generator, string promise, or string
137+ # ' promise generator (as returned by the {elmer} package's `chat`, `stream`,
138+ # ' `chat_async`, and `stream_async` methods, respectively).
139+ # '
140+ # ' This function should be called from a Shiny app's server. It is generally
141+ # ' used to append the model's response to the chat, while user messages are
142+ # ' added to the chat UI automatically by the front-end. You'd only need to use
143+ # ' `chat_append(role="user")` if you are programmatically generating queries
144+ # ' from the server and sending them on behalf of the user, and want them to be
145+ # ' reflected in the UI.
146+ # '
147+ # ' # Error handling
148+ # '
149+ # ' If the `response` argument is a generator, promise, or promise generator, and
150+ # ' an error occurs while producing the message (e.g. an elmer chat object errors
151+ # ' during `stream_async`), the promise returned by `chat_append` will reject
152+ # ' with the error. If the `chat_append` call is the last expression in a Shiny
153+ # ' observer, Shiny will see that the observer failed, and end the user session.
154+ # ' If you prefer to handle the error gracefully, use [promises::catch()] on the
155+ # ' promise returned by `chat_append`.
156+ # '
134157# ' @param id The ID of the chat element
135158# ' @param response The message or message stream to append to the chat element
159+ # ' @param role The role of the message (either "assistant" or "user"). Defaults
160+ # ' to "assistant".
136161# ' @param session The Shiny session object
162+ # ' @returns Returns a promise. This promise resolves when the message has been
163+ # ' successfully sent to the client; note that it does not guarantee that the
164+ # ' message was actually received or rendered by the client. The promise
165+ # ' rejects if an error occurs while processing the response (see the "Error
166+ # ' handling" section).
167+ # '
168+ # ' @examplesIf interactive()
169+ # ' library(shiny)
170+ # ' library(bslib)
171+ # ' library(elmer)
172+ # ' library(shinychat)
173+ # '
174+ # ' ui <- page_fillable(
175+ # ' chat_ui("chat", fill = TRUE)
176+ # ' )
177+ # '
178+ # ' server <- function(input, output, session) {
179+ # ' chat <- chat_openai(model = "gpt-4o")
180+ # '
181+ # ' observeEvent(input$chat_user_input, {
182+ # ' response <- chat$stream_async(input$chat_user_input)
183+ # ' chat_append("chat", response)
184+ # ' })
185+ # ' }
137186# '
187+ # ' shinyApp(ui, server)
138188# ' @export
139- chat_append <- function (id , response , session = getDefaultReactiveDomain()) {
189+ chat_append <- function (id , response , role = c(" assistant" , " user" ), session = getDefaultReactiveDomain()) {
190+ role <- match.arg(role )
140191 if (is.character(response )) {
141192 # string => generator
142193 stream <- coro :: gen(yield(response ))
@@ -147,9 +198,9 @@ chat_append <- function(id, response, session = getDefaultReactiveDomain()) {
147198 # Already a generator (sync or async)
148199 stream <- response
149200 } else {
150- stop (" Unexpected message type; chat_append() expects a string, a string generator, a string promise, or a string promise generator" )
201+ rlang :: abort (" Unexpected message type; chat_append() expects a string, a string generator, a string promise, or a string promise generator" )
151202 }
152- chat_append_stream(id , stream , session = session )
203+ chat_append_stream(id , stream , role = role , session = session )
153204}
154205
155206# ' Low-level function to append a message to a chat control
@@ -172,14 +223,17 @@ chat_append <- function(id, response, session = getDefaultReactiveDomain()) {
172223# ' to the latest message. Default is `NULL`.
173224# ' @param session The Shiny session object
174225# '
175- # ' @returns Returns nothing of consequence .
226+ # ' @returns Returns nothing (\code{invisible(NULL)}) .
176227# '
177228# ' @importFrom shiny getDefaultReactiveDomain
178229# ' @export
179230chat_append_message <- function (id , msg , chunk = FALSE , operation = NULL , session = getDefaultReactiveDomain()) {
231+ if (! is.list(msg )) {
232+ rlang :: abort(" msg must be a named list with 'role' and 'content' fields" )
233+ }
180234 if (! isTRUE(msg [[" role" ]] %in% c(" user" , " assistant" ))) {
181235 warning(" Invalid role argument; must be 'user' or 'assistant'" )
182- return ()
236+ return (invisible ( NULL ) )
183237 }
184238
185239 if (! isFALSE(chunk )) {
@@ -191,7 +245,7 @@ chat_append_message <- function(id, msg, chunk = FALSE, operation = NULL, sessio
191245 } else if (isTRUE(chunk )) {
192246 chunk_type <- NULL
193247 } else {
194- stop (" Invalid chunk argument" )
248+ rlang :: abort (" Invalid chunk argument" )
195249 }
196250 } else {
197251 msg_type <- " shiny-chat-append-message"
@@ -217,16 +271,18 @@ chat_append_message <- function(id, msg, chunk = FALSE, operation = NULL, sessio
217271 handler = msg_type ,
218272 obj = msg
219273 ))
274+
275+ invisible (NULL )
220276}
221277
222- chat_append_stream <- function (id , stream , session = getDefaultReactiveDomain()) {
223- result <- chat_append_stream_impl(id , stream , session )
278+ chat_append_stream <- function (id , stream , role = " assistant " , session = getDefaultReactiveDomain()) {
279+ result <- chat_append_stream_impl(id , stream , role , session )
224280 # Handle erroneous result...
225281 promises :: catch(result , function (reason ) {
226282 chat_append_message(
227283 id ,
228284 list (
229- role = " assistant " ,
285+ role = role ,
230286 content = paste0(" \n\n **An error occurred:** " , conditionMessage(reason ))
231287 ),
232288 chunk = " end" ,
@@ -245,16 +301,16 @@ chat_append_stream <- function(id, stream, session = getDefaultReactiveDomain())
245301utils ::: globalVariables(c(" generator_env" , " exits" , " yield" ))
246302
247303chat_append_stream_impl <- NULL
248- rlang :: on_load(chat_append_stream_impl <- coro :: async(function (id , stream , session = shiny :: getDefaultReactiveDomain()) {
249- chat_append_message(id , list (role = " assistant " , content = " " ), chunk = " start" , session = session )
304+ rlang :: on_load(chat_append_stream_impl <- coro :: async(function (id , stream , role = " assistant " , session = shiny :: getDefaultReactiveDomain()) {
305+ chat_append_message(id , list (role = role , content = " " ), chunk = " start" , session = session )
250306 for (msg in stream ) {
251307 if (promises :: is.promising(msg )) {
252308 msg <- await(msg )
253309 }
254310 if (coro :: is_exhausted(msg )) {
255311 break
256312 }
257- chat_append_message(id , list (role = " assistant " , content = msg ), chunk = TRUE , operation = " append" , session = session )
313+ chat_append_message(id , list (role = role , content = msg ), chunk = TRUE , operation = " append" , session = session )
258314 }
259- chat_append_message(id , list (role = " assistant " , content = " " ), chunk = " end" , operation = " append" , session = session )
315+ chat_append_message(id , list (role = role , content = " " ), chunk = " end" , operation = " append" , session = session )
260316}))
0 commit comments