Skip to contents

Example with shinychat

library(shiny)
library(bslib)
library(shinychat)
library(openaiapi)

ui <- page_fluid(
  shinychat::chat_ui("chat")
)
server <- function(input, output, session) {
  chat <- oai_create_model_response(
    instructions = "You're a trickster who answers in riddles.",
    stream = TRUE,
    .async = TRUE,
    .perform_query = FALSE
  )
  observeEvent(input$chat_user_input, {
    chat$
      respond(input = input$chat_user_input)$
      async_generator()$
      then(function(stream) {
        shinychat::chat_append("chat", stream)
      })
  })
}

shinyApp(ui, server)

Low level example

This is an example of a Shiny app that uses openaiapi to stream a completion asynchonously. The completion is then rendered as markdown.

To the stream is non-blocking, the app displays a timer that updates every 100 milliseconds while the completion is being generated.

library(openaiapi)
library(shiny)
library(bslib)

my_prompt <- "This is a test. Can you produce a long text that takes at least 10 seconds to generate and uses markdown notation?"

ui <- fixedPage(
  input_task_button("prompt", "Prompt"),
  uiOutput("timer"),
  uiOutput("answer")
)

server <- function(input, output, session) {

  content <- reactiveVal()
  start_time <- NULL

  task <- ExtendedTask$new(function(prompt) {
    oai_create_chat_completion(
      prompt,
      stream = TRUE,
      .async = TRUE
    )$stream_async(
      callback = function(choices) {
        content(choices[[1]]$message$content)
      }
    )
  }) |>
    bind_task_button("prompt")

  observeEvent(input$prompt, {
    start_time <<- Sys.time()
    task$invoke(my_prompt)
  })

  output$timer <- renderUI({
      if (task$status() == "running") {
        invalidateLater(100)
      } else if (task$status() %in% c("initial", "error")) {
        req(FALSE)
      }
      time <-
        as.numeric(difftime(Sys.time(), start_time, units = "secs")) |>
        format(digits = 1, nsmall = 2)
      p(paste("Time elapsed:", time,"seconds"))
  })

  output$answer <- renderUI({
    tryCatch(
      markdown(content()),
      error = function(e) {
        req(FALSE, cancelOutput = TRUE)
      }
    )
  })
}
shinyApp(ui, server)

If using bslib, the default busy indicator when recalculating the output is a opacity fade, which does not work well with text streaming. You can turn it off with busyIndicatorOptions(fade_opacity = 1).

ui <- page_fixed(
  busyIndicatorOptions(fade_opacity = 1),
  input_task_button("prompt", "Prompt"),
  uiOutput("timer"),
  uiOutput("answer")
)
shinyApp(ui, server)

TODO: