Skip to content

feat: Add chat_enable_bookmarking(id, client, ..., update_on_input = TRUE, update_on_response = TRUE) #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from

Conversation

schloerke
Copy link

@schloerke schloerke commented Feb 20, 2025

Fixes #27

Reprex app:

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

ui <- function(...) {
  page_fillable(
    chat_ui("chat", fill = TRUE)
  )
}

server <- function(input, output, session) {
  chat_client <- ellmer::chat_ollama(
    system_prompt = "Important: Always respond in a limerick",
    model = "qwen2.5-coder:1.5b",
    echo = TRUE
  )
  # Let the UI know about the client
  chat_enable_bookmarking("chat", chat_client)

  observeEvent(input$chat_user_input, {
    stream <- chat_client$stream_async(input$chat_user_input)
    chat_append("chat", stream)
  })
}

# Enable bookmarking!
shinyApp(ui, server, enableBookmarking = "url")

@schloerke schloerke marked this pull request as ready for review February 24, 2025 18:11
@schloerke schloerke requested a review from cpsievert February 24, 2025 18:11
@schloerke schloerke requested a review from cpsievert February 26, 2025 15:36
@schloerke schloerke marked this pull request as draft February 26, 2025 20:14
@schloerke
Copy link
Author

schloerke commented Feb 26, 2025

Talking with @icarusz and @wch , I am in agreement that we should change the API:

This change below (set_chat_client(id, client, ..., bookmark = "auto") -> chat_bookmark(id, client, ..., update_on_input = TRUE, update_on_response = TRUE) will only really change the interface. The main logic will remain.

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

ui <- function(...) {
  page_fillable(
    chat_ui("chat", fill = TRUE)
  )
}

server <- function(input, output, session) {
  chat_client <- ellmer::chat_ollama(
    system_prompt = "Important: Always respond in a limerick",
    model = "qwen2.5-coder:1.5b",
    echo = TRUE
  )

  # Let the UI know that shinychat should bookmark the chat_client
  shinychat::chat_bookmark("chat", chat_client) # update_on_input = TRUE, update_on_response = TRUE

  observeEvent(input$chat_user_input, {
    stream_promise <- chat_client$stream_async(input$chat_user_input)
    shinychat::chat_append("chat", stream_promise)
  })

}

# Enable bookmarking!
shinyApp(ui, server, enableBookmarking = "server")

If other features were to be made, they should have their own shinychat::chat_FEATURE() method.

@schloerke schloerke marked this pull request as ready for review February 26, 2025 21:20
@schloerke schloerke changed the title feat: Add set_chat_model(id, model, ..., bookmark = TRUE) feat: Add chat_bookmark(id, client, ..., update_on_input = TRUE, update_on_response = TRUE) Feb 26, 2025
@schloerke schloerke changed the title feat: Add chat_bookmark(id, client, ..., update_on_input = TRUE, update_on_response = TRUE) feat: Add chat_bookmark(id, client) Feb 26, 2025
@schloerke schloerke changed the title feat: Add chat_bookmark(id, client) feat: Add chat_bookmark(id, client, ..., update_on_input = TRUE, update_on_response = TRUE) Feb 26, 2025
Comment on lines 73 to 83
# 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\")`"
)
)
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remaining question

@schloerke schloerke requested a review from wch February 26, 2025 21:27
@schloerke
Copy link
Author

@wch Looking for a review on the API: shinychat::chat_bookmark(ID, CLIENT) with default parameters of update_on_input = TRUE and update_on_response = TRUE.

These default parameters hide away the multiple calls to session$doBookmark().

Ex equivalent server code:

  shinychat::chat_bookmark("chat", chat_client, update_on_input = FALSE, update_on_response = FALSE)
  
  observeEvent(input$chat_user_input, {
    session$doBookmark()
    stream_promise <- chat_client$stream_async(input$chat_user_input)
	stream_promise$then(function(stream_value) { session$doBookmark() })
    shinychat::chat_append("chat", stream_promise)
  })

Final server code:

  shinychat::chat_bookmark("chat", chat_client)
  
  observeEvent(input$chat_user_input, {
    stream_promise <- chat_client$stream_async(input$chat_user_input)
    shinychat::chat_append("chat", stream_promise)
  })

@wch
Copy link

wch commented Feb 27, 2025

The new API does look better. A few questions:

  • The name chat_bookmark makes it sound like running that function will do the bookmarking. Maybe chat_enable_bookmarking?
  • Another thing we should consider: We want to use this mechanism to (A) do bookmarking in the usual way that Shiny does it (B) allow the user to save/restore multiple chats, similar to the ChatGPT web interface. Will this API make sense for the second case as well?
  • Suppose an app uses update_on_input=TRUE and there's a disconnection before the response is completed. When the chat is restored, is there be a way in the UI re-send the message to the server and get a response? If not, then restoring that conversation could leave the app in an unusable state, I think. (We should have the ability to re-send a message anyway, but that's outside the scope of this PR.)

@schloerke
Copy link
Author

chat_enable_bookmarking()

@schloerke
Copy link
Author

Will this API make sense for the second case as well?

If the chat is done within a single chat object, no. The data storage required is more complex than what a single ellmer object wants.

If the chat is done within a module where given the sidebar's chat selected id, we could have a dynamic chat module that loads a chat given the chat ID. I believe this is a plausible approach

@schloerke schloerke changed the title feat: Add chat_bookmark(id, client, ..., update_on_input = TRUE, update_on_response = TRUE) feat: Add chat_enable_bookmarking(id, client, ..., update_on_input = TRUE, update_on_response = TRUE) Mar 5, 2025
@schloerke schloerke marked this pull request as draft March 5, 2025 21:30
})
state$values[[id]] <- turns_list
}
})
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to save the UI here

Comment on lines 185 to 195
# Set the UI
# 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
ellmer::contents_markdown(turn),
# turn_info$contents,
role = turn@role
)
})
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to independently restore the UI here (not directly from ellmer)

Comment on lines 190 to 191
# Use `contents_markdown()` as it handles image serialization
ellmer::contents_markdown(turn),
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should ideally be a {s7} JSON "pack" method, similar to jsonlite:::pack()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: Add set_chat_client()
3 participants