Skip to content

Commit 0027819

Browse files
committed
More docs
1 parent d75f237 commit 0027819

File tree

4 files changed

+140
-24
lines changed

4 files changed

+140
-24
lines changed

DESCRIPTION

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
Package: shinychat
2-
Title: Chat UI Component for Shiny
2+
Title: Chat UI Component for 'Shiny'
33
Version: 0.0.0.9000
44
Authors@R: c(
55
person("Joe", "Cheng", , "[email protected]", role = c("aut", "cre")),
66
person("Carson", "Sievert", , "[email protected]", role = c("aut")),
77
person("Posit Software, PBC", role = c("cph", "fnd"))
88
)
9-
Description: Provides a chat UI component for Shiny apps.
9+
Description: A chat UI component for 'Shiny' apps, well suited for creating
10+
chatbot apps based on 'Large Language Models' (LLMs). Designed to work well
11+
with the 'elmer' R package for calling LLMs.
1012
License: MIT + file LICENSE
1113
URL: https://github.com/jcheng5/shinychat, https://jcheng5.github.io/shinychat/
1214
BugReports: https://github.com/jcheng5/shinychat/issues

R/chat.R

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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
179230
chat_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())
245301
utils:::globalVariables(c("generator_env", "exits", "yield"))
246302

247303
chat_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
}))

man/chat_append.Rd

Lines changed: 61 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/chat_append_message.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)