diff --git a/pkg-py/src/querychat/_querychat.py b/pkg-py/src/querychat/_querychat.py
index 52136587..b5655a9f 100644
--- a/pkg-py/src/querychat/_querychat.py
+++ b/pkg-py/src/querychat/_querychat.py
@@ -137,7 +137,7 @@ def __init__(
)
# Fork and empty chat now so the per-session forks are fast
- client = normalize_client(client)
+ client = as_querychat_client(client)
self._client = copy.deepcopy(client)
self._client.set_turns([])
self._client.system_prompt = prompt
@@ -636,7 +636,7 @@ def normalize_data_source(
return DataFrameSource(data_source, table_name)
-def normalize_client(client: str | chatlas.Chat | None) -> chatlas.Chat:
+def as_querychat_client(client: str | chatlas.Chat | None) -> chatlas.Chat:
if client is None:
client = os.getenv("QUERYCHAT_CLIENT", None)
diff --git a/pkg-r/DESCRIPTION b/pkg-r/DESCRIPTION
index f11321b5..e8bd4104 100644
--- a/pkg-r/DESCRIPTION
+++ b/pkg-r/DESCRIPTION
@@ -1,6 +1,6 @@
Package: querychat
Title: Filter and Query Data Frames in 'shiny' Using an LLM Chat Interface
-Version: 0.0.1.9000
+Version: 0.1.0.9000
Authors@R: c(
person("Joe", "Cheng", , "joe@posit.co", role = c("aut", "cre")),
person("Posit Software, PBC", role = c("cph", "fnd"))
@@ -23,17 +23,16 @@ Imports:
lifecycle,
promises,
purrr,
+ R6,
rlang,
shiny,
shinychat (>= 0.2.0.9000),
utils,
- whisker,
- xtable
+ whisker
Suggests:
bsicons,
DT,
palmerpenguins,
- R6,
RSQLite,
shinytest2,
testthat (>= 3.0.0),
diff --git a/pkg-r/NAMESPACE b/pkg-r/NAMESPACE
index 6db28d67..fa321d5b 100644
--- a/pkg-r/NAMESPACE
+++ b/pkg-r/NAMESPACE
@@ -1,5 +1,7 @@
# Generated by roxygen2: do not edit by hand
+S3method(as_querychat_data_source,DBIConnection)
+S3method(as_querychat_data_source,data.frame)
S3method(cleanup_source,dbi_source)
S3method(create_system_prompt,querychat_data_source)
S3method(execute_query,dbi_source)
@@ -7,14 +9,15 @@ S3method(get_db_type,data_frame_source)
S3method(get_db_type,dbi_source)
S3method(get_db_type,default)
S3method(get_schema,dbi_source)
-S3method(querychat_data_source,DBIConnection)
-S3method(querychat_data_source,data.frame)
S3method(test_query,dbi_source)
+export(QueryChat)
+export(as_querychat_data_source)
export(cleanup_source)
export(create_system_prompt)
export(execute_query)
export(get_db_type)
export(get_schema)
+export(querychat)
export(querychat_app)
export(querychat_data_source)
export(querychat_greeting)
@@ -23,4 +26,7 @@ export(querychat_server)
export(querychat_sidebar)
export(querychat_ui)
export(test_query)
+importFrom(R6,R6Class)
+importFrom(bslib,sidebar)
importFrom(lifecycle,deprecated)
+importFrom(rlang,"%||%")
diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md
index 74aced17..5a91e2ef 100644
--- a/pkg-r/NEWS.md
+++ b/pkg-r/NEWS.md
@@ -1,5 +1,8 @@
# querychat (development version)
+* Nearly the entire functional API (i.e., `querychat_init()`, `querychat_sidebar()`, `querychat_server()`, etc) has been hard deprecated in favor of a simpler OOP-based API. Namely, the new `QueryChat$new()` class is now the main entry point (instead of `querychat_init()`) and has methods to replace old functions (e.g., `$sidebar()`, `$server()`, etc). (#109)
+ * In addition, `querychat_data_source()` was renamed to `as_querychat_data_source()`, and remains exported for a developer extension point, but users no longer have to explicitly create a data source. (#109)
+
* Added `prompt_template` support for `querychat_system_prompt()`. (Thank you, @oacar! #37, #45)
* `querychat_init()` now accepts a `client`, replacing the previous `create_chat_func` argument. (#60)
diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R
new file mode 100644
index 00000000..3c214b2d
--- /dev/null
+++ b/pkg-r/R/QueryChat.R
@@ -0,0 +1,681 @@
+#' QueryChat: Interactive Data Querying with Natural Language
+#'
+#' @description
+#' `QueryChat` is an R6 class built on Shiny, shinychat, and ellmer to enable
+#' interactive querying of data using natural language. It leverages large
+#' language models (LLMs) to translate user questions into SQL queries, execute
+#' them against a data source (data frame or database), and various ways of
+#' accessing/displaying the results.
+#'
+#'
+#' @details
+#' The `QueryChat` class takes your data (a data frame or database connection)
+#' as input and provides methods to:
+#' - Generate a chat UI for natural language queries (e.g., `$app()`, `$sidebar()`)
+#' - Initialize server logic that returns session-specific reactive values (via `$server()`)
+#' - Access reactive data, SQL queries, and titles through the returned server values
+#'
+#' @section Usage:
+#' ```r
+#' library(querychat)
+#'
+#' # Create a QueryChat object
+#' qc <- QueryChat$new(mtcars, "mtcars")
+#'
+#' # Quick start: run a complete app
+#' qc$app()
+#'
+#' # Or build a custom Shiny app
+#' ui <- page_sidebar(
+#' qc$sidebar(),
+#' verbatimTextOutput("sql"),
+#' dataTableOutput("data")
+#' )
+#'
+#' server <- function(input, output, session) {
+#' qc_vals <- qc$server()
+#'
+#' output$sql <- renderText(qc_vals$sql())
+#' output$data <- renderDataTable(qc_vals$df())
+#' }
+#'
+#' shinyApp(ui, server)
+#' ```
+#'
+#' @export
+#' @examples
+#' \dontrun{
+#' # Basic usage with a data frame
+#' qc <- QueryChat$new(mtcars, "mtcars")
+#' app <- qc$app()
+#'
+#' # With a custom greeting
+#' greeting <- "Welcome! Ask me about the mtcars dataset."
+#' qc <- QueryChat$new(mtcars, "mtcars", greeting = greeting)
+#'
+#' # With a specific LLM provider
+#' qc <- QueryChat$new(mtcars, "mtcars", client = "anthropic/claude-sonnet-4-5")
+#'
+#' # Generate a greeting for reuse
+#' qc <- QueryChat$new(mtcars, "mtcars")
+#' greeting <- qc$generate_greeting(echo = "text")
+#' # Save greeting for next time
+#' writeLines(greeting, "mtcars_greeting.md")
+#' }
+QueryChat <- R6::R6Class(
+ "QueryChat",
+ private = list(
+ server_values = NULL,
+ .data_source = NULL,
+ .client = NULL
+ ),
+ public = list(
+ #' @field greeting The greeting message displayed to users.
+ greeting = NULL,
+ #' @field id The module ID for namespacing.
+ id = NULL,
+
+ #' @description
+ #' Create a new QueryChat object.
+ #'
+ #' @param data_source Either a data.frame or a database connection (e.g., DBI
+ #' connection).
+ #' @param table_name A string specifying the table name to use in SQL queries.
+ #' If `data_source` is a data.frame, this is the name to refer to it by in
+ #' queries (typically the variable name). If `data_source` is a database
+ #' connection, this is the name of the table in the database.
+ #' @param ... Additional arguments (currently unused).
+ #' @param id Optional module ID for the QueryChat instance. If not provided,
+ #' will be auto-generated from `table_name`. The ID is used to namespace
+ #' the Shiny module.
+ #' @param greeting Optional initial message to display to users. Can be a
+ #' character string (in Markdown format) or a file path. If not provided,
+ #' a greeting will be generated at the start of each conversation using the
+ #' LLM, which adds latency and cost. Use `$generate_greeting()` to create
+ #' a greeting to save and reuse.
+ #' @param client Optional chat client. Can be:
+ #' - An [ellmer::Chat] object
+ #' - A string to pass to [ellmer::chat()] (e.g., `"openai/gpt-4o"`)
+ #' - `NULL` (default): Uses the `querychat.client` option, the
+ #' `QUERYCHAT_CLIENT` environment variable, or defaults to
+ #' [ellmer::chat_openai()]
+ #' @param data_description Optional description of the data in plain text or
+ #' Markdown. Can be a string or a file path. This provides context to the
+ #' LLM about what the data represents.
+ #' @param categorical_threshold For text columns, the maximum number of unique
+ #' values to consider as a categorical variable. Default is 20.
+ #' @param extra_instructions Optional additional instructions for the chat
+ #' model in plain text or Markdown. Can be a string or a file path.
+ #' @param prompt_template Optional path to or string of a custom prompt
+ #' template file. If not provided, the default querychat template will be
+ #' used. See the package prompts directory for the default template format.
+ #' @param cleanup Whether or not to automatically run `$cleanup()` when the
+ #' Shiny session/app stops. By default, cleanup only occurs if `QueryChat`
+ #' gets created within a Shiny session. Set to `TRUE` to always clean up,
+ #' or `FALSE` to never clean up automatically.
+ #'
+ #' @return A new `QueryChat` object.
+ #'
+ #' @examples
+ #' \dontrun{
+ #' # Basic usage
+ #' qc <- QueryChat$new(mtcars, "mtcars")
+ #'
+ #' # With options
+ #' qc <- QueryChat$new(
+ #' mtcars,
+ #' "mtcars",
+ #' greeting = "Welcome to the mtcars explorer!",
+ #' client = "openai/gpt-4o",
+ #' data_description = "Motor Trend car road tests dataset"
+ #' )
+ #'
+ #' # With database
+ #' library(DBI)
+ #' conn <- dbConnect(RSQLite::SQLite(), ":memory:")
+ #' dbWriteTable(conn, "mtcars", mtcars)
+ #' qc <- QueryChat$new(conn, "mtcars")
+ #' }
+ initialize = function(
+ data_source,
+ table_name,
+ ...,
+ id = NULL,
+ greeting = NULL,
+ client = NULL,
+ data_description = NULL,
+ categorical_threshold = 20,
+ extra_instructions = NULL,
+ prompt_template = NULL,
+ cleanup = NA
+ ) {
+ rlang::check_dots_empty()
+
+ private$.data_source <- normalize_data_source(data_source, table_name)
+
+ # Validate table name
+ if (!grepl("^[a-zA-Z][a-zA-Z0-9_]*$", table_name)) {
+ rlang::abort(
+ "Table name must begin with a letter and contain only letters, numbers, and underscores"
+ )
+ }
+
+ self$id <- id %||% table_name
+
+ if (!is.null(greeting) && file.exists(greeting)) {
+ greeting <- paste(readLines(greeting), collapse = "\n")
+ }
+ self$greeting <- greeting
+
+ if (is.null(greeting)) {
+ rlang::warn(c(
+ "No greeting provided; the LLM will be invoked at conversation start to generate one.",
+ "*" = "For faster startup, lower cost, and determinism, please save a greeting and pass it to QueryChat$new().",
+ "i" = "You can generate a greeting with $generate_greeting()."
+ ))
+ }
+
+ prompt <- create_system_prompt(
+ private$.data_source,
+ data_description = data_description,
+ categorical_threshold = categorical_threshold,
+ extra_instructions = extra_instructions,
+ prompt_template = prompt_template
+ )
+
+ # Fork and empty chat now so the per-session forks are fast
+ client <- as_querychat_client(client)
+ private$.client <- client$clone()
+ private$.client$set_turns(list())
+ private$.client$set_system_prompt(prompt)
+
+ # By default, only close automatically if a Shiny session is active
+ if (is.na(cleanup)) {
+ cleanup <- !is.null(shiny::getDefaultReactiveDomain())
+ }
+
+ if (cleanup) {
+ shiny::onStop(function() {
+ message("Closing data source...")
+ self$cleanup()
+ })
+ }
+ },
+
+ #' @description
+ #' Create and run a Shiny gadget for chatting with data
+ #'
+ #' Runs a Shiny gadget (designed for interactive use) that provides
+ #' a complete interface for chatting with your data using natural language.
+ #' If you're looking to deploy this app or run it through some other means,
+ #' see `$app_obj()`.
+ #'
+ #' @param ... Arguments passed to `$app_obj()`.
+ #' @param bookmark_store The bookmarking storage method. Passed to
+ #' [shiny::enableBookmarking()]. If `"url"` or `"server"`, the chat state
+ #' (including current query) will be bookmarked. Default is `"url"`.
+ #'
+ #' @return Invisibly returns a list of session-specific values:
+ #' - `df`: The final filtered data frame
+ #' - `sql`: The final SQL query string
+ #' - `title`: The final title
+ #' - `client`: The session-specific chat client instance
+ #'
+ #' @examples
+ #' \dontrun{
+ #' library(querychat)
+ #'
+ #' qc <- QueryChat$new(mtcars, "mtcars")
+ #' qc$app()
+ #' }
+ #'
+ app = function(..., bookmark_store = "url") {
+ app <- self$app_obj(..., bookmark_store = bookmark_store)
+ vals <- tryCatch(shiny::runGadget(app), interrupt = function(cnd) NULL)
+ invisible(vals)
+ },
+
+ #' @description
+ #' A streamlined Shiny app for chatting with data
+ #'
+ #' Creates a Shiny app designed for chatting with data, with:
+ #' - A sidebar containing the chat interface
+ #' - A card displaying the current SQL query
+ #' - A card displaying the filtered data table
+ #' - A reset button to clear the query
+ #'
+ #' @param ... Additional arguments (currently unused).
+ #' @param bookmark_store The bookmarking storage method. Passed to
+ #' [shiny::enableBookmarking()]. If `"url"` or `"server"`, the chat state
+ #' (including current query) will be bookmarked. Default is `"url"`.
+ #'
+ #' @return A Shiny app object that can be run with `shiny::runApp()`.
+ #'
+ #' @examples
+ #' \dontrun{
+ #' library(querychat)
+ #'
+ #' qc <- QueryChat$new(mtcars, "mtcars")
+ #' app <- qc$app_obj()
+ #' shiny::runApp(app)
+ #' }
+ #'
+ app_obj = function(..., bookmark_store = "url") {
+ rlang::check_installed("DT")
+ rlang::check_installed("bsicons")
+ rlang::check_dots_empty()
+
+ table_name <- private$.data_source$table_name
+
+ ui <- function(req) {
+ bslib::page_sidebar(
+ title = shiny::HTML(sprintf(
+ "querychat with %s",
+ table_name
+ )),
+ class = "bslib-page-dashboard",
+ sidebar = self$sidebar(),
+ shiny::useBusyIndicators(pulse = TRUE, spinners = FALSE),
+ bslib::card(
+ fill = FALSE,
+ style = bslib::css(max_height = "33%"),
+ bslib::card_header(
+ shiny::div(
+ class = "hstack",
+ shiny::div(
+ bsicons::bs_icon("terminal-fill"),
+ shiny::textOutput("query_title", inline = TRUE)
+ ),
+ shiny::div(
+ class = "ms-auto",
+ shiny::uiOutput("ui_reset", inline = TRUE)
+ )
+ )
+ ),
+ shiny::uiOutput("sql_output")
+ ),
+ bslib::card(
+ full_screen = TRUE,
+ bslib::card_header(bsicons::bs_icon("table"), "Data"),
+ DT::DTOutput("dt")
+ ),
+ shiny::actionButton(
+ "close_btn",
+ label = "",
+ class = "btn-close",
+ style = "position: fixed; top: 6px; right: 6px;"
+ )
+ )
+ }
+
+ server <- function(input, output, session) {
+ qc_vals <- self$server()
+
+ output$query_title <- shiny::renderText({
+ if (shiny::isTruthy(qc_vals$title())) {
+ qc_vals$title()
+ } else {
+ "SQL Query"
+ }
+ })
+
+ output$ui_reset <- shiny::renderUI({
+ shiny::req(qc_vals$sql())
+
+ shiny::actionButton(
+ "reset_query",
+ label = "Reset Query",
+ class = "btn btn-outline-danger btn-sm lh-1"
+ )
+ })
+
+ shiny::observeEvent(input$reset_query, label = "on_reset_query", {
+ qc_vals$sql("")
+ qc_vals$title(NULL)
+ })
+
+ output$dt <- DT::renderDT({
+ DT::datatable(
+ qc_vals$df(),
+ fillContainer = TRUE,
+ options = list(pageLength = 25, scrollX = TRUE)
+ )
+ })
+
+ output$sql_output <- shiny::renderUI({
+ sql <- if (shiny::isTruthy(qc_vals$sql())) {
+ qc_vals$sql()
+ } else {
+ paste("SELECT * FROM", table_name)
+ }
+
+ sql_code <- paste(c("```sql", sql, "```"), collapse = "\n")
+
+ shinychat::output_markdown_stream(
+ "sql_code",
+ content = sql_code,
+ auto_scroll = FALSE,
+ width = "100%"
+ )
+ })
+
+ shiny::observeEvent(input$close_btn, label = "on_close_btn", {
+ shiny::stopApp(list(
+ df = qc_vals$df(),
+ sql = qc_vals$sql(),
+ title = qc_vals$title(),
+ client = qc_vals$client
+ ))
+ })
+ }
+
+ shiny::shinyApp(ui, server, enableBookmarking = bookmark_store)
+ },
+
+ #' @description
+ #' Create a sidebar containing the querychat UI.
+ #'
+ #' This method generates a [bslib::sidebar()] component containing the chat
+ #' interface, suitable for use with [bslib::page_sidebar()] or similar layouts.
+ #'
+ #' @param width Width of the sidebar in pixels. Default is 400.
+ #' @param height Height of the sidebar. Default is "100%".
+ #' @param ... Additional arguments passed to [bslib::sidebar()].
+ #'
+ #' @return A [bslib::sidebar()] UI component.
+ #'
+ #' @examples
+ #' \dontrun{
+ #' qc <- QueryChat$new(mtcars, "mtcars")
+ #'
+ #' ui <- page_sidebar(
+ #' qc$sidebar(),
+ #' # Main content here
+ #' )
+ #' }
+ sidebar = function(width = 400, height = "100%", ...) {
+ bslib::sidebar(
+ width = width,
+ height = height,
+ class = "querychat-sidebar",
+ ...,
+ self$ui()
+ )
+ },
+
+ #' @description
+ #' Create the UI for the querychat chat interface.
+ #'
+ #' This method generates the chat UI component. Typically you'll use
+ #' `$sidebar()` instead, which wraps this in a sidebar layout.
+ #'
+ #' @param ... Additional arguments passed to [shinychat::chat_ui()].
+ #'
+ #' @return A UI component containing the chat interface.
+ #'
+ #' @examples
+ #' \dontrun{
+ #' qc <- QueryChat$new(mtcars, "mtcars")
+ #'
+ #' ui <- fluidPage(
+ #' qc$ui()
+ #' )
+ #' }
+ ui = function(...) {
+ mod_ui(self$id, ...)
+ },
+
+ #' @description
+ #' Initialize the querychat server logic.
+ #'
+ #' This method must be called within a Shiny server function. It sets up
+ #' the reactive logic for the chat interface and returns session-specific
+ #' reactive values.
+ #'
+ #' @param session The Shiny session object.
+ #'
+ #' @return A list containing session-specific reactive values and the chat
+ #' client with the following elements:
+ #' - `df`: Reactive expression returning the current filtered data frame
+ #' - `sql`: Reactive value for the current SQL query string
+ #' - `title`: Reactive value for the current title
+ #' - `client`: The session-specific chat client instance
+ #'
+ #' @examples
+ #' \dontrun{
+ #' qc <- QueryChat$new(mtcars, "mtcars")
+ #'
+ #' server <- function(input, output, session) {
+ #' qc_vals <- qc$server()
+ #'
+ #' output$data <- renderDataTable(qc_vals$df())
+ #' output$query <- renderText(qc_vals$sql())
+ #' output$title <- renderText(qc_vals$title() %||% "No Query")
+ #' }
+ #' }
+ server = function(session = shiny::getDefaultReactiveDomain()) {
+ if (is.null(session)) {
+ rlang::abort(
+ "$server() must be called within a Shiny server function."
+ )
+ }
+
+ mod_server(
+ self$id,
+ data_source = private$.data_source,
+ greeting = self$greeting,
+ client = private$.client
+ )
+ },
+
+ #' @description
+ #' Generate a welcome greeting for the chat.
+ #'
+ #' By default, `QueryChat$new()` generates a greeting at the start of every
+ #' new conversation, which is convenient for getting started and development,
+ #' but also might add unnecessary latency and cost. Use this method to
+ #' generate a greeting once and save it for reuse.
+ #'
+ #' @param echo Whether to print the greeting to the console. Options are
+ #' `"none"` (default, no output) or `"output"` (print to console).
+ #'
+ #' @return The greeting string in Markdown format.
+ #'
+ #' @examples
+ #' \dontrun{
+ #' # Create QueryChat object
+ #' qc <- QueryChat$new(mtcars, "mtcars")
+ #'
+ #' # Generate a greeting and save it
+ #' greeting <- qc$generate_greeting()
+ #' writeLines(greeting, "mtcars_greeting.md")
+ #'
+ #' # Later, use the saved greeting
+ #' qc2 <- QueryChat$new(mtcars, "mtcars", greeting = "mtcars_greeting.md")
+ #' }
+ generate_greeting = function(echo = c("none", "output")) {
+ echo <- match.arg(echo)
+
+ chat <- private$.client$clone()
+ chat$set_turns(list())
+
+ prompt <- "Please give me a friendly greeting. Include a few sample prompts in a two-level bulleted list."
+ as.character(chat$chat(prompt, echo = echo))
+ },
+
+ #' @description
+ #' Clean up resources associated with the data source.
+ #'
+ #' This method releases any resources (e.g., database connections)
+ #' associated with the data source. Call this when you are done using
+ #' the QueryChat object to avoid resource leaks.
+ #'
+ #' Note: If `auto_cleanup` was set to `TRUE` in the constructor,
+ #' this will be called automatically when the Shiny app stops.
+ #'
+ #' @return Invisibly returns `NULL`. Resources are cleaned up internally.
+ cleanup = function() {
+ cleanup_source(private$.data_source)
+ }
+ ),
+ active = list(
+ #' @field system_prompt Get the system prompt.
+ system_prompt = function() {
+ private$.client$get_system_prompt()
+ },
+
+ #' @field data_source Get the current data source.
+ data_source = function() {
+ private$.data_source
+ }
+ )
+)
+
+
+#' QueryChat convenience functions
+#'
+#' Convenience functions for wrapping [QueryChat] creation (i.e., `querychat()`)
+#' and app launching (i.e., `querychat_app()`).
+#'
+#' @param data_source Either a data.frame or a database connection (e.g., DBI
+#' connection).
+#' @param table_name A string specifying the table name to use in SQL queries.
+#' If `data_source` is a data.frame, this is the name to refer to it by in
+#' queries (typically the variable name). If `data_source` is a database
+#' connection, this is the name of the table in the database.
+#' @param ... Additional arguments (currently unused).
+#' @param id Optional module ID for the QueryChat instance. If not provided,
+#' will be auto-generated from `table_name`. The ID is used to namespace
+#' the Shiny module.
+#' @param greeting Optional initial message to display to users. Can be a
+#' character string (in Markdown format) or a file path. If not provided,
+#' a greeting will be generated at the start of each conversation using the
+#' LLM, which adds latency and cost. Use `$generate_greeting()` to create
+#' a greeting to save and reuse.
+#' @param client Optional chat client. Can be:
+#' - An [ellmer::Chat] object
+#' - A string to pass to [ellmer::chat()] (e.g., `"openai/gpt-4o"`)
+#' - `NULL` (default): Uses the `querychat.client` option, the
+#' `QUERYCHAT_CLIENT` environment variable, or defaults to
+#' [ellmer::chat_openai()]
+#' @param data_description Optional description of the data in plain text or
+#' Markdown. Can be a string or a file path. This provides context to the
+#' LLM about what the data represents.
+#' @param categorical_threshold For text columns, the maximum number of unique
+#' values to consider as a categorical variable. Default is 20.
+#' @param extra_instructions Optional additional instructions for the chat
+#' model in plain text or Markdown. Can be a string or a file path.
+#' @param prompt_template Optional path to or string of a custom prompt
+#' template file. If not provided, the default querychat template will be
+#' used. See the package prompts directory for the default template format.
+#' @param cleanup Whether or not to automatically run `$cleanup()` when the
+#' Shiny session/app stops. By default, cleanup only occurs if `QueryChat`
+#' gets created within a Shiny session. Set to `TRUE` to always clean up,
+#' or `FALSE` to never clean up automatically.
+#'
+#' @return A `QueryChat` object. See [QueryChat] for available methods.
+#'
+#' @rdname querychat-convenience
+#'
+#' @export
+#' @examples
+#' \dontrun{
+#' # Quick start - chat with mtcars dataset in one line
+#' querychat_app(mtcars, "mtcars")
+#'
+#' # Add options
+#' querychat_app(
+#' mtcars,
+#' "mtcars",
+#' greeting = "Welcome to the mtcars explorer!",
+#' client = "openai/gpt-4o"
+#' )
+#'
+#' # Chat with a database table
+#' library(DBI)
+#' conn <- dbConnect(RSQLite::SQLite(), ":memory:")
+#' dbWriteTable(conn, "mtcars", mtcars)
+#' querychat_app(conn, "mtcars")
+#'
+#' # Create QueryChat class object
+#' qc <- querychat(mtcars, "mtcars")
+#'
+#' # Run the app later
+#' qc$app()
+#'
+#' }
+querychat <- function(
+ data_source,
+ table_name,
+ ...,
+ id = NULL,
+ greeting = NULL,
+ client = NULL,
+ data_description = NULL,
+ categorical_threshold = 20,
+ extra_instructions = NULL,
+ prompt_template = NULL,
+ cleanup = NA
+) {
+ QueryChat$new(
+ data_source = data_source,
+ table_name = table_name,
+ ...,
+ id = id,
+ greeting = greeting,
+ client = client,
+ data_description = data_description,
+ categorical_threshold = categorical_threshold,
+ extra_instructions = extra_instructions,
+ prompt_template = prompt_template,
+ cleanup = cleanup
+ )
+}
+
+
+#' @rdname querychat-convenience
+#' @param bookmark_store The bookmarking storage method. Passed to
+#' [shiny::enableBookmarking()]. If `"url"` or `"server"`, the chat state
+#' (including current query) will be bookmarked. Default is `"url"`.
+#' @return Invisibly returns the chat object after the app stops.
+#'
+#' @export
+querychat_app <- function(
+ data_source,
+ table_name,
+ ...,
+ id = NULL,
+ greeting = NULL,
+ client = NULL,
+ data_description = NULL,
+ categorical_threshold = 20,
+ extra_instructions = NULL,
+ prompt_template = NULL,
+ cleanup = TRUE,
+ bookmark_store = "url"
+) {
+ qc <- QueryChat$new(
+ data_source = data_source,
+ table_name = table_name,
+ ...,
+ id = id,
+ greeting = greeting,
+ client = client,
+ data_description = data_description,
+ categorical_threshold = categorical_threshold,
+ extra_instructions = extra_instructions,
+ prompt_template = prompt_template,
+ cleanup = cleanup
+ )
+
+ qc$app(bookmark_store = bookmark_store)
+}
+
+
+normalize_data_source <- function(data_source, table_name) {
+ if (is_data_source(data_source)) {
+ data_source
+ } else {
+ as_querychat_data_source(data_source, table_name)
+ }
+}
diff --git a/pkg-r/R/data_source.R b/pkg-r/R/data_source.R
index d7beb36b..73684c55 100644
--- a/pkg-r/R/data_source.R
+++ b/pkg-r/R/data_source.R
@@ -1,28 +1,22 @@
#' Create a data source for querychat
#'
-#' Generic function to create a data source for querychat. This function
-#' dispatches to appropriate methods based on input.
+#' An entrypoint for developers to create custom data sources for use with
+#' querychat. Most users shouldn't use this function directly; instead, they
+#' should pass their data to `QueryChat$new()`.
#'
#' @param x A data frame or DBI connection
#' @param table_name The name to use for the table in the data source. Can be:
#' - A character string (e.g., "table_name")
#' - Or, for tables contained within catalogs or schemas, a [DBI::Id()] object (e.g., `DBI::Id(schema = "schema_name", table = "table_name")`)
-#' @param categorical_threshold For text columns, the maximum number of unique values to consider as a categorical variable
-#' @param ... Additional arguments passed to specific methods
#' @return A querychat_data_source object
+#' @keywords internal
#' @export
-querychat_data_source <- function(x, ...) {
- UseMethod("querychat_data_source")
+as_querychat_data_source <- function(x, table_name = NULL, ...) {
+ UseMethod("as_querychat_data_source")
}
#' @export
-#' @rdname querychat_data_source
-querychat_data_source.data.frame <- function(
- x,
- table_name = NULL,
- categorical_threshold = 20,
- ...
-) {
+as_querychat_data_source.data.frame <- function(x, table_name = NULL, ...) {
if (is.null(table_name)) {
# Infer table name from dataframe name, if not already added
table_name <- deparse(substitute(x))
@@ -47,23 +41,13 @@ querychat_data_source.data.frame <- function(
duckdb::duckdb_register(conn, table_name, x, experimental = FALSE)
structure(
- list(
- conn = conn,
- table_name = table_name,
- categorical_threshold = categorical_threshold
- ),
+ list(conn = conn, table_name = table_name),
class = c("data_frame_source", "dbi_source", "querychat_data_source")
)
}
#' @export
-#' @rdname querychat_data_source
-querychat_data_source.DBIConnection <- function(
- x,
- table_name,
- categorical_threshold = 20,
- ...
-) {
+as_querychat_data_source.DBIConnection <- function(x, table_name, ...) {
# Handle different types of table_name inputs
if (inherits(table_name, "Id")) {
# DBI::Id object - keep as is
@@ -87,21 +71,26 @@ querychat_data_source.DBIConnection <- function(
}
structure(
- list(
- conn = x,
- table_name = table_name,
- categorical_threshold = categorical_threshold
- ),
+ list(conn = x, table_name = table_name),
class = c("dbi_source", "querychat_data_source")
)
}
-#' Execute a SQL query on a data source
+is_data_source <- function(x) {
+ inherits(x, "querychat_data_source")
+}
+
+#' Execute an SQL query on a data source
+#'
+#' An entrypoint for developers to create custom data source objects for use
+#' with querychat. Most users shouldn't use this function directly; instead,
+#' they call the `$sql()` method on the [QueryChat] object to run queries.
#'
#' @param source A querychat_data_source object
#' @param query SQL query string
#' @param ... Additional arguments passed to methods
#' @return Result of the query as a data frame
+#' @keywords internal
#' @export
execute_query <- function(source, query, ...) {
UseMethod("execute_query")
@@ -122,10 +111,15 @@ execute_query.dbi_source <- function(source, query, ...) {
#' Test a SQL query on a data source.
#'
+#' An entrypoint for developers to create custom data sources for use with
+#' querychat. Most users shouldn't use this function directly; instead, they
+#' should call the `$sql()` method on the [QueryChat] object to run queries.
+#'
#' @param source A querychat_data_source object
#' @param query SQL query string
#' @param ... Additional arguments passed to methods
#' @return Result of the query, limited to one row of data.
+#' @keywords internal
#' @export
test_query <- function(source, query, ...) {
UseMethod("test_query")
@@ -142,9 +136,14 @@ test_query.dbi_source <- function(source, query, ...) {
#' Get type information for a data source
#'
+#' An entrypoint for developers to create custom data sources for use with
+#' querychat. Most users shouldn't use this function directly; instead, they
+#' should call the `$set_system_prompt()` method on the [QueryChat] object.
+#'
#' @param source A querychat_data_source object
#' @param ... Additional arguments passed to methods
#' @return A character string containing the type information
+#' @keywords internal
#' @export
get_db_type <- function(source, ...) {
UseMethod("get_db_type")
@@ -184,16 +183,24 @@ get_db_type.dbi_source <- function(source, ...) {
#' Create a system prompt for the data source
#'
+#' An entrypoint for developers to create custom data sources for use with
+#' querychat. Most users shouldn't use this function directly; instead, they
+#' should call the `$set_system_prompt()` method on the [QueryChat] object.
+#'
#' @param source A querychat_data_source object
#' @param data_description Optional description of the data
#' @param extra_instructions Optional additional instructions
+#' @param categorical_threshold For text columns, the maximum number of unique
+#' values to consider as a categorical variable
#' @param ... Additional arguments passed to methods
#' @return A string with the system prompt
+#' @keywords internal
#' @export
create_system_prompt <- function(
source,
data_description = NULL,
extra_instructions = NULL,
+ categorical_threshold = 20,
...
) {
UseMethod("create_system_prompt")
@@ -204,6 +211,7 @@ create_system_prompt.querychat_data_source <- function(
source,
data_description = NULL,
extra_instructions = NULL,
+ categorical_threshold = 20,
...
) {
if (!is.null(data_description)) {
@@ -219,7 +227,7 @@ create_system_prompt.querychat_data_source <- function(
prompt_text <- paste(prompt_content, collapse = "\n")
# Get schema for the data source
- schema <- get_schema(source)
+ schema <- get_schema(source, categorical_threshold = categorical_threshold)
# Examine the data source and get the type for the prompt
db_type <- get_db_type(source)
@@ -238,9 +246,14 @@ create_system_prompt.querychat_data_source <- function(
#' Clean up a data source (close connections, etc.)
#'
+#' An entrypoint for developers to create custom data sources for use with
+#' querychat. Most users shouldn't use this function directly; instead, they
+#' should call the `$cleanup()` method on the [QueryChat] object.
+#'
#' @param source A querychat_data_source object
#' @param ... Additional arguments passed to methods
#' @return NULL (invisibly)
+#' @keywords internal
#' @export
cleanup_source <- function(source, ...) {
UseMethod("cleanup_source")
@@ -257,19 +270,24 @@ cleanup_source.dbi_source <- function(source, ...) {
#' Get schema for a data source
#'
+#' An entrypoint for developers to create custom data sources for use with
+#' querychat. Most users shouldn't use this function directly; instead, they
+#' should call the `$set_system_prompt()` method on the [QueryChat] object.
+#'
#' @param source A querychat_data_source object
+#' @param categorical_threshold For text columns, the maximum number of unique values to consider as a categorical variable
#' @param ... Additional arguments passed to methods
#' @return A character string describing the schema
+#' @keywords internal
#' @export
-get_schema <- function(source, ...) {
+get_schema <- function(source, categorical_threshold = 20, ...) {
UseMethod("get_schema")
}
#' @export
-get_schema.dbi_source <- function(source, ...) {
+get_schema.dbi_source <- function(source, categorical_threshold = 20, ...) {
conn <- source$conn
table_name <- source$table_name
- categorical_threshold <- source$categorical_threshold
# Get column information
columns <- DBI::dbListFields(conn, table_name)
diff --git a/pkg-r/R/deprecated.R b/pkg-r/R/deprecated.R
new file mode 100644
index 00000000..c5721e5f
--- /dev/null
+++ b/pkg-r/R/deprecated.R
@@ -0,0 +1,138 @@
+#' Deprecated functions
+#'
+#' These functions have been replaced by the new `QueryChat` R6 class API.
+#' Please update your code to use the new class-based approach.
+#'
+#' @name deprecated
+#' @keywords internal
+NULL
+
+#' @rdname deprecated
+#' @export
+querychat_init <- function(...) {
+ lifecycle::deprecate_stop(
+ when = "0.1.0",
+ what = "querychat_init()",
+ with = "QueryChat$new()",
+ details = c(
+ "Old code:",
+ " config <- querychat_init(mtcars, greeting = 'Hello!')",
+ " ui <- page_sidebar(sidebar = querychat_sidebar('chat'), ...)",
+ " server <- function(input, output, session) {",
+ " chat <- querychat_server('chat', config)",
+ " output$data <- renderDataTable(chat$df())",
+ " }",
+ "",
+ "New code:",
+ " qc <- QueryChat$new(mtcars, 'mtcars', greeting = 'Hello!')",
+ " ui <- page_sidebar(sidebar = qc$sidebar(), ...)",
+ " server <- function(input, output, session) {",
+ " qc$server()",
+ " output$data <- renderDataTable(qc$df())",
+ " }",
+ "",
+ "See ?QueryChat for more information."
+ )
+ )
+}
+
+#' @rdname deprecated
+#' @export
+querychat_sidebar <- function(...) {
+ lifecycle::deprecate_stop(
+ when = "0.1.0",
+ what = "querychat_sidebar()",
+ with = "QueryChat$sidebar()",
+ details = c(
+ "Old code:",
+ " querychat_sidebar('chat')",
+ "",
+ "New code:",
+ " qc <- QueryChat$new(data, 'table_name')",
+ " qc$sidebar()",
+ "",
+ "See ?QueryChat for more information."
+ )
+ )
+}
+
+#' @rdname deprecated
+#' @export
+querychat_ui <- function(...) {
+ lifecycle::deprecate_stop(
+ when = "0.1.0",
+ what = "querychat_ui()",
+ with = "QueryChat$ui()",
+ details = c(
+ "Old code:",
+ " querychat_ui('chat')",
+ "",
+ "New code:",
+ " qc <- QueryChat$new(data, 'table_name')",
+ " qc$ui()",
+ "",
+ "See ?QueryChat for more information."
+ )
+ )
+}
+
+#' @rdname deprecated
+#' @export
+querychat_server <- function(...) {
+ lifecycle::deprecate_stop(
+ when = "0.1.0",
+ what = "querychat_server()",
+ with = "QueryChat$server()",
+ details = c(
+ "Old code:",
+ " chat <- querychat_server('chat', config)",
+ " output$data <- renderDataTable(chat$df())",
+ "",
+ "New code:",
+ " qc <- QueryChat$new(data, 'table_name')",
+ " qc$server() # Must be called within server function",
+ " output$data <- renderDataTable(qc$df())",
+ "",
+ "See ?QueryChat for more information."
+ )
+ )
+}
+
+#' @rdname deprecated
+#' @export
+querychat_greeting <- function(...) {
+ lifecycle::deprecate_stop(
+ when = "0.1.0",
+ what = "querychat_greeting()",
+ with = "QueryChat$generate_greeting()",
+ details = c(
+ "Old code:",
+ " greeting <- querychat_greeting(config)",
+ "",
+ "New code:",
+ " qc <- QueryChat$new(data, 'table_name')",
+ " greeting <- qc$generate_greeting(echo = 'text')",
+ "",
+ "See ?QueryChat for more information."
+ )
+ )
+}
+
+#' @rdname deprecated
+#' @export
+querychat_data_source <- function(...) {
+ lifecycle::deprecate_stop(
+ when = "0.1.0",
+ what = "querychat_data_source()",
+ with = "QueryChat$new()",
+ details = c(
+ "Old code:",
+ " data_source <- querychat_data_source(db_connection, 'table_name')",
+ "",
+ "New code:",
+ " qc <- QueryChat$new(db_connection, 'table_name')",
+ "",
+ "See ?QueryChat for more information."
+ )
+ )
+}
diff --git a/pkg-r/R/querychat-package.R b/pkg-r/R/querychat-package.R
index 425b3c1c..e19e1636 100644
--- a/pkg-r/R/querychat-package.R
+++ b/pkg-r/R/querychat-package.R
@@ -1,7 +1,70 @@
+#' querychat: Chat with Your Data Using Natural Language
+#'
+#' @description
+#' querychat provides an interactive chat interface for querying data using
+#' natural language. It translates your questions into SQL queries, executes
+#' them against your data, and displays the results. The package works with
+#' both data frames and database connections.
+#'
+#' @section Quick Start:
+#' The easiest way to get started is with the [QueryChat] R6 class:
+#'
+#' ```r
+#' library(querychat)
+#'
+#' # Create a QueryChat object
+#' qc <- QueryChat$new(mtcars, "mtcars")
+#'
+#' # Option 1: Run a complete app with sensible defaults
+#' qc$app()
+#'
+#' # Option 2: Build a custom Shiny app
+#' ui <- page_sidebar(
+#' qc$sidebar(),
+#' dataTableOutput("data")
+#' )
+#'
+#' server <- function(input, output, session) {
+#' qc$server()
+#' output$data <- renderDataTable(qc$df())
+#' }
+#'
+#' shinyApp(ui, server)
+#' ```
+#'
+#' @section Key Features:
+#' - **Natural language queries**: Ask questions in plain English
+#' - **SQL transparency**: See the generated SQL queries
+#' - **Multiple data sources**: Works with data frames and database connections
+#' - **Customizable**: Add data descriptions, extra instructions, and custom greetings
+#' - **LLM agnostic**: Works with OpenAI, Anthropic, Google, and other providers via ellmer
+#'
+#' @section Main Components:
+#' - [QueryChat]: The main R6 class for creating chat interfaces
+#' - [as_querychat_data_source()]: (Advanced) Create custom data source objects
+#'
+#' @section Examples:
+#' To see examples included with the package, run:
+#'
+#' ```r
+#' shiny::runExample(package = "querychat")
+#' ```
+#'
+#' This provides a list of available examples. To run a specific example, like
+#' '01-hello-app', use:
+#'
+#' ```r
+#' shiny::runExample("01-hello-app", package = "querychat")
+#' ```
+#'
+#'
#' @keywords internal
"_PACKAGE"
## usethis namespace: start
#' @importFrom lifecycle deprecated
+#' @importFrom R6 R6Class
+#' @importFrom bslib sidebar
+#' @importFrom rlang %||%
## usethis namespace: end
NULL
diff --git a/pkg-r/R/querychat.R b/pkg-r/R/querychat.R
deleted file mode 100644
index 1bb9d5e8..00000000
--- a/pkg-r/R/querychat.R
+++ /dev/null
@@ -1,309 +0,0 @@
-#' Call this once outside of any server function
-#'
-#' This will perform one-time initialization that can then be shared by all
-#' Shiny sessions in the R process.
-#'
-#' @param data_source A querychat_data_source object created by
-#' `querychat_data_source()`.
-#'
-#' To create a data source:
-#' - For data frame: `querychat_data_source(df, tbl_name = "my_table")`
-#' - For database: `querychat_data_source(conn, "table_name")`
-#' @param greeting A string in Markdown format, containing the initial message
-#' to display to the user upon first loading the chatbot. If not provided, the
-#' LLM will be invoked at the start of the conversation to generate one. You
-#' can also use [querychat_greeting()] to generate a greeting.
-#' @param data_description A string containing a data description for the chat
-#' model. We have found that formatting the data description as a markdown
-#' bulleted list works best.
-#' @param extra_instructions A string containing extra instructions for the
-#' chat model.
-#' @param client An `ellmer::Chat` object, a string to be passed to
-#' [ellmer::chat()] describing the model to use (e.g. `"openai/gpt-4o"`), or a
-#' function that creates a chat client. When using a function, the function
-#' should take `system_prompt` as an argument and return an `ellmer::Chat`
-#' object.
-#'
-#' If `client` is not provided, querychat consults the `querychat.client` R
-#' option, which can be any of the described options, or the
-#' `QUERYCHAT_CLIENT` environment variable, which can be set to a a
-#' provider-model string. If no option is provided, querychat defaults to
-#' using [ellmer::chat_openai()].
-#' @param create_chat_func `r lifecycle::badge('deprecated')`. Use the `client`
-#' argument instead.
-#' @param system_prompt A string containing the system prompt for the chat
-#' model. The default generates a generic prompt, which you can enhance via
-#' the `data_description` and `extra_instructions` arguments.
-#' @param auto_close_data_source Should the data source connection be
-#' automatically closed when the shiny app stops? Defaults to TRUE.
-#'
-#' @returns An object that can be passed to `querychat_server()` as the
-#' `querychat_config` argument. By convention, this object should be named
-#' `querychat_config`.
-#'
-#' @export
-querychat_init <- function(
- data_source,
- greeting = NULL,
- data_description = NULL,
- extra_instructions = NULL,
- create_chat_func = deprecated(),
- system_prompt = NULL,
- auto_close_data_source = TRUE,
- client = NULL
-) {
- if (lifecycle::is_present(create_chat_func)) {
- lifecycle::deprecate_warn(
- "0.0.1",
- "querychat_init(create_chat_func=)",
- "querychat_init(client =)"
- )
- if (!is.null(client)) {
- rlang::abort(
- "You cannot pass both `create_chat_func` and `client` to `querychat_init()`."
- )
- }
- client <- create_chat_func
- }
-
- client <- querychat_client(client)
-
- # If the user passes a data.frame to data_source, create a correct data source for them
- if (inherits(data_source, "data.frame")) {
- data_source <- querychat_data_source(
- data_source,
- table_name = deparse(substitute(data_source))
- )
- }
-
- # Check that data_source is a querychat_data_source object
- if (!inherits(data_source, "querychat_data_source")) {
- rlang::abort(
- "`data_source` must be a querychat_data_source object. Use querychat_data_source() to create one."
- )
- }
-
- if (auto_close_data_source) {
- # Close the data source when the Shiny app stops (or, if some reason the
- # querychat_init call is within a specific session, when the session ends)
- shiny::onStop(function() {
- message("Closing data source...")
- cleanup_source(data_source)
- })
- }
-
- # Generate system prompt if not provided
- if (is.null(system_prompt)) {
- system_prompt <- create_system_prompt(
- data_source,
- data_description = data_description,
- extra_instructions = extra_instructions
- )
- }
-
- # Validate system prompt
- stopifnot(
- "system_prompt must be a string" = is.character(system_prompt)
- )
-
- if (!is.null(greeting)) {
- greeting <- paste(collapse = "\n", greeting)
- } else {
- rlang::warn(c(
- "No greeting provided; the LLM will be invoked at the start of the conversation to generate one.",
- "*" = "For faster startup, lower cost, and determinism, please save a greeting and pass it to querychat_init().",
- "i" = "You can generate a greeting by passing this config object to `querychat_greeting()`."
- ))
- }
-
- structure(
- list(
- data_source = data_source,
- system_prompt = system_prompt,
- greeting = greeting,
- client = client
- ),
- class = "querychat_config"
- )
-}
-
-#' UI components for querychat
-#'
-#' These functions create UI components for the querychat interface.
-#' `querychat_ui()` creates a basic chat interface, while `querychat_sidebar()`
-#' wraps the chat interface in a [bslib::sidebar()] component designed to be
-#' used as the `sidebar` argument to [bslib::page_sidebar()].
-#'
-#' @param id The ID of the module instance.
-#' @param width,height In `querychat_sidebar()`: the width and height of the
-#' sidebar.
-#' @param ... In `querychat_sidebar()`: additional arguments passed to
-#' [bslib::sidebar()].
-#'
-#' @return A UI object that can be embedded in a Shiny app.
-#'
-#' @name querychat_ui
-#' @export
-querychat_sidebar <- function(id, width = 400, height = "100%", ...) {
- bslib::sidebar(
- width = width,
- height = height,
- class = "querychat-sidebar",
- ...,
- # purposely NOT using ns() for `id`, we're just a passthrough
- querychat_ui(id)
- )
-}
-
-#' @rdname querychat_ui
-#' @export
-querychat_ui <- function(id) {
- ns <- shiny::NS(id)
- htmltools::tagList(
- htmltools::htmlDependency(
- "querychat",
- version = "0.0.1",
- package = "querychat",
- src = "htmldep",
- script = "querychat.js",
- stylesheet = "styles.css"
- ),
- shinychat::chat_ui(
- ns("chat"),
- height = "100%",
- fill = TRUE,
- class = "querychat"
- )
- )
-}
-
-#' Initialize the querychat server
-#'
-#' @param id The ID of the module instance. Must match the ID passed to
-#' the corresponding call to `querychat_ui()`.
-#' @param querychat_config An object created by `querychat_init()`.
-#'
-#' @returns A querychat instance, which is a named list with the following
-#' elements:
-#'
-#' - `sql`: A reactive that returns the current SQL query.
-#' - `title`: A reactive that returns the current title.
-#' - `df`: A reactive that returns the filtered data as a data.frame.
-#' - `chat`: The [ellmer::Chat] object that powers the chat interface.
-#'
-#' @export
-querychat_server <- function(id, querychat_config) {
- shiny::moduleServer(id, function(input, output, session) {
- # 🔄 Reactive state/computation --------------------------------------------
-
- data_source <- querychat_config[["data_source"]]
- system_prompt <- querychat_config[["system_prompt"]]
- greeting <- querychat_config[["greeting"]]
- client <- querychat_config[["client"]]
-
- current_title <- shiny::reactiveVal(NULL, label = "current_title")
- current_query <- shiny::reactiveVal("", label = "current_query")
- filtered_df <- shiny::reactive(label = "filtered_df", {
- execute_query(data_source, query = DBI::SQL(current_query()))
- })
-
- append_output <- function(...) {
- txt <- paste0(...)
- shinychat::chat_append_message(
- "chat",
- list(role = "assistant", content = txt),
- chunk = TRUE,
- operation = "append",
- session = session
- )
- }
-
- reset_query <- function() {
- current_query("")
- current_title(NULL)
- querychat_tool_result(action = "reset")
- }
-
- # Preload the conversation with the system prompt. These are instructions for
- # the chat model, and must not be shown to the end user.
- chat <- client$clone()
- chat$set_turns(list())
- chat$set_system_prompt(system_prompt)
- chat$register_tool(
- tool_update_dashboard(data_source, current_query, current_title)
- )
- chat$register_tool(tool_query(data_source))
- chat$register_tool(tool_reset_dashboard(reset_query))
-
- # Prepopulate the chat UI with a welcome message that appears to be from the
- # chat model (but is actually hard-coded). This is just for the user, not for
- # the chat model to see.
- shinychat::chat_append(
- "chat",
- querychat_greeting(querychat_config, generate = TRUE, stream = TRUE)
- )
-
- append_stream_task <- shiny::ExtendedTask$new(
- function(client, user_input) {
- stream <- client$stream_async(
- user_input,
- stream = "content"
- )
-
- p <- promises::promise_resolve(stream)
- promises::then(p, function(stream) {
- shinychat::chat_append("chat", stream)
- })
- }
- )
-
- shiny::observeEvent(input$chat_user_input, label = "on_chat_user_input", {
- append_stream_task$invoke(chat, input$chat_user_input)
- })
-
- shiny::observeEvent(input$chat_update, label = "on_chat_update", {
- current_query(input$chat_update$query)
- current_title(input$chat_update$title)
- })
-
- list(
- chat = chat,
- sql = shiny::reactive(current_query()),
- title = shiny::reactive(current_title()),
- df = filtered_df,
- update_query = function(query, title = NULL) {
- current_query(query)
- current_title(title)
- }
- )
- })
-}
-
-df_to_html <- function(df, maxrows = 5) {
- df_short <- if (nrow(df) > 10) utils::head(df, maxrows) else df
-
- tbl_html <- utils::capture.output(
- df_short |>
- xtable::xtable() |>
- print(
- type = "html",
- include.rownames = FALSE,
- html.table.attributes = NULL
- )
- ) |>
- paste(collapse = "\n")
-
- if (nrow(df_short) != nrow(df)) {
- rows_notice <- paste0(
- "\n\n(Showing only the first ",
- maxrows,
- " rows out of ",
- nrow(df),
- ".)\n"
- )
- } else {
- rows_notice <- ""
- }
-
- paste0(tbl_html, "\n", rows_notice)
-}
diff --git a/pkg-r/R/querychat_app.R b/pkg-r/R/querychat_app.R
deleted file mode 100644
index 06d55759..00000000
--- a/pkg-r/R/querychat_app.R
+++ /dev/null
@@ -1,138 +0,0 @@
-#' A Simple App for Chatting with Data
-#'
-#' Creates a Shiny app that allows users to interact with a data source using
-#' natural language queries. The app uses a pre-configured Shiny app built on
-#' [querychat_sidebar()] and [querychat_server()] to provide a quick-and-easy
-#' way to chat with your data.
-#'
-#' @examplesIf rlang::is_interactive()
-#' # Pass in a data frame to start querychatting
-#' querychat_app(mtcars)
-#'
-#' # Or choose your LLM client using ellmer::chat_*() functions
-#' querychat_app(mtcars, client = ellmer::chat_anthropic())
-#'
-#' @param config A `querychat_config` object or a data source that can be used
-#' to create one.
-#' @param ... Additional arguments passed to [querychat_init()] if `config` is
-#' not already a `querychat_config` object.
-#' @inheritParams shinychat::chat_app
-#'
-#' @return Invisibly returns the `chat` object containing the chat history.
-#'
-#' @export
-querychat_app <- function(config, ..., bookmark_store = "url") {
- rlang::check_installed("DT")
- rlang::check_installed("bsicons")
-
- if (!inherits(config, "querychat_config")) {
- if (inherits(config, "querychat_data_source")) {
- data_source <- config
- } else {
- data_source <- querychat_data_source(
- config,
- table_name = deparse(substitute(config))
- )
- }
- config <- querychat_init(data_source, ...)
- }
-
- ui <- function(req) {
- bslib::page_sidebar(
- title = shiny::HTML(paste0(
- "querychat with ",
- config$data_source$table_name,
- ""
- )),
- class = "bslib-page-dashboard",
- sidebar = querychat_sidebar("chat"),
- shiny::useBusyIndicators(pulse = TRUE, spinners = FALSE),
- bslib::card(
- fill = FALSE,
- style = bslib::css(max_height = "33%"),
- bslib::card_header(
- shiny::div(
- class = "hstack",
- shiny::div(
- bsicons::bs_icon("terminal-fill"),
- shiny::textOutput("query_title", inline = TRUE)
- ),
- shiny::div(
- class = "ms-auto",
- shiny::uiOutput("ui_reset", inline = TRUE)
- )
- )
- ),
- shiny::uiOutput("sql_output")
- ),
- bslib::card(
- bslib::card_header(bsicons::bs_icon("table"), "Data"),
- DT::DTOutput("dt")
- ),
- shiny::actionButton(
- "close_btn",
- label = "",
- class = "btn-close",
- style = "position: fixed; top: 6px; right: 6px;"
- )
- )
- }
-
- chat <- NULL
-
- server <- function(input, output, session) {
- qc <- querychat_server("chat", config)
- chat <<- qc$chat
-
- output$query_title <- shiny::renderText({
- if (shiny::isTruthy(qc$title())) {
- qc$title()
- } else {
- "SQL Query"
- }
- })
-
- output$ui_reset <- shiny::renderUI({
- shiny::req(qc$sql())
-
- shiny::actionButton(
- "reset_query",
- label = "Reset Query",
- class = "btn btn-outline-danger btn-sm lh-1"
- )
- })
-
- shiny::observeEvent(input$reset_query, label = "on_reset_query", {
- qc$update_query("", NULL)
- })
-
- output$dt <- DT::renderDT({
- DT::datatable(qc$df())
- })
-
- output$sql_output <- shiny::renderUI({
- sql <- if (shiny::isTruthy(qc$sql())) {
- qc$sql()
- } else {
- paste("SELECT * FROM", config$data_source$table_name)
- }
-
- sql_code <- paste(c("```sql", sql, "```"), collapse = "\n")
-
- shinychat::output_markdown_stream(
- "sql_code",
- content = sql_code,
- auto_scroll = FALSE,
- width = "100%"
- )
- })
-
- shiny::observeEvent(input$close_btn, label = "on_close_btn", {
- shiny::stopApp()
- })
- }
-
- app <- shiny::shinyApp(ui, server, enableBookmarking = bookmark_store)
- tryCatch(shiny::runGadget(app), interrupt = function(cnd) NULL)
- invisible(chat)
-}
diff --git a/pkg-r/R/querychat_client.R b/pkg-r/R/querychat_client.R
deleted file mode 100644
index 98741d8a..00000000
--- a/pkg-r/R/querychat_client.R
+++ /dev/null
@@ -1,42 +0,0 @@
-querychat_client <- function(client = NULL) {
- if (is.null(client)) {
- client <- querychat_client_option()
- }
-
- if (is.null(client)) {
- # Use OpenAI with ellmer's default model
- return(ellmer::chat_openai())
- }
-
- if (rlang::is_function(client)) {
- # `client` as a function was the first interface we supported and expected
- # `system_prompt` as an argument. This avoids breaking existing code.
- client <- client(system_prompt = NULL)
- }
-
- if (rlang::is_string(client)) {
- client <- ellmer::chat(client)
- }
-
- if (!inherits(client, "Chat")) {
- rlang::abort(
- "`client` must be an {ellmer} Chat object or a function that returns one.",
- )
- }
-
- client
-}
-
-querychat_client_option <- function() {
- opt <- getOption("querychat.client", NULL)
- if (!is.null(opt)) {
- return(opt)
- }
-
- env <- Sys.getenv("QUERYCHAT_CLIENT", "")
- if (nzchar(env)) {
- return(env)
- }
-
- NULL
-}
diff --git a/pkg-r/R/querychat_greeting.R b/pkg-r/R/querychat_greeting.R
deleted file mode 100644
index f7464160..00000000
--- a/pkg-r/R/querychat_greeting.R
+++ /dev/null
@@ -1,77 +0,0 @@
-#' Generate or retrieve a greeting message
-#'
-#' Use this function to generate a friendly greeting message using the chat
-#' client and data source specified in the `querychat_config` object. You can
-#' pass this greeting to [querychat_init()] to set an initial greeting for users
-#' for faster startup times and lower costs. If you don't provide a greeting in
-#' [querychat_init()], one will be generated at the start of every new
-#' conversation, using this function.
-#'
-#' @examplesIf interactive()
-#' penguins_config <- querychat_init(penguins)
-#'
-#' # Generate a new greeting
-#' querychat_greeting(penguins_config)
-#'
-#' # Update the config with the generated greeting
-#' penguins_config <- querychat_init(
-#' penguins,
-#' greeting = "Hello! I’m here to help you explore and analyze the penguins..."
-#' )
-#'
-#' # Alternatively, you could generate the greeting once when starting up your
-#' # Shiny app server, to be shared across all users.
-#' penguins_config <- querychat_init(penguins)
-#' penguins_config$greeting <- querychat_greeting(penguins_config)
-#'
-#' @param querychat_config A `querychat_config` object from [querychat_init()].
-#' @param generate If `TRUE` and if `querychat_config` does not include a
-#' `greeting`, a new greeting is generated. If `FALSE`, returns the existing
-#' greeting from the configuration (if any).
-#' @param stream If `TRUE`, calls `$stream_async()` on the [ellmer::Chat]
-#' client, suitable for streaming the greeting into a Shiny app with
-#' [shinychat::chat_append()]. If `FALSE` (default), calls `$chat()` to get
-#' the full greeting at once. Only relevant when `generate = TRUE`.
-#'
-#' @return
-#' - When `generate = FALSE`: Returns the existing greeting as a string or
-#' `NULL` if no greeting exists.
-#' - When `generate = TRUE`: Returns the chat response containing a greeting and
-#' sample prompts.
-#'
-#' @export
-querychat_greeting <- function(
- querychat_config,
- generate = TRUE,
- stream = FALSE
-) {
- if (!inherits(querychat_config, "querychat_config")) {
- rlang::abort("`querychat_config` must be a `querychat_config` object.")
- }
-
- greeting <- querychat_config$greeting
-
- has_greeting <- !is.null(greeting) && any(nzchar(greeting))
-
- if (has_greeting) {
- return(greeting)
- }
-
- if (!isTRUE(generate)) {
- # No greeting and not generating one
- return(NULL)
- }
-
- chat <- querychat_config$client$clone()
- chat$set_system_prompt(querychat_config$system_prompt)
-
- prompt <- "Please give me a friendly greeting. Include a few sample prompts in a two-level bulleted list."
-
- if (isTRUE(stream)) {
- chat$stream_async(prompt)
- } else {
- is_user_facing <- rlang::env_is_user_facing(rlang::caller_env())
- echo = if (is_user_facing) "output" else "none"
- chat$chat(prompt, echo = echo)
- }
-}
diff --git a/pkg-r/R/querychat_module.R b/pkg-r/R/querychat_module.R
new file mode 100644
index 00000000..8e3dc20c
--- /dev/null
+++ b/pkg-r/R/querychat_module.R
@@ -0,0 +1,104 @@
+# Main module UI function
+mod_ui <- function(id, ...) {
+ ns <- shiny::NS(id)
+ htmltools::tagList(
+ htmltools::htmlDependency(
+ "querychat",
+ version = "0.0.1",
+ package = "querychat",
+ src = "htmldep",
+ script = "querychat.js",
+ stylesheet = "styles.css"
+ ),
+ shinychat::chat_ui(
+ ns("chat"),
+ height = "100%",
+ class = "querychat",
+ ...
+ )
+ )
+}
+
+# Main module server function
+mod_server <- function(id, data_source, greeting, client) {
+ shiny::moduleServer(id, function(input, output, session) {
+ current_title <- shiny::reactiveVal(NULL, label = "current_title")
+ current_query <- shiny::reactiveVal("", label = "current_query")
+ filtered_df <- shiny::reactive(label = "filtered_df", {
+ execute_query(data_source, query = DBI::SQL(current_query()))
+ })
+
+ append_output <- function(...) {
+ txt <- paste0(...)
+ shinychat::chat_append_message(
+ "chat",
+ list(role = "assistant", content = txt),
+ chunk = TRUE,
+ operation = "append",
+ session = session
+ )
+ }
+
+ reset_query <- function() {
+ current_query("")
+ current_title(NULL)
+ querychat_tool_result(action = "reset")
+ }
+
+ # Set up the chat object for this session
+ chat <- client$clone()
+ chat$register_tool(
+ tool_update_dashboard(data_source, current_query, current_title)
+ )
+ chat$register_tool(tool_query(data_source))
+ chat$register_tool(tool_reset_dashboard(reset_query))
+
+ # Prepopulate the chat UI with a welcome message that appears to be from the
+ # chat model (but is actually hard-coded). This is just for the user, not for
+ # the chat model to see.
+ greeting_content <- if (!is.null(greeting) && any(nzchar(greeting))) {
+ greeting
+ } else {
+ # Generate greeting on the fly if none provided
+ rlang::warn(c(
+ "No greeting provided; generating one now. This adds latency and cost.",
+ "i" = "Consider using $generate_greeting() to create a reusable greeting."
+ ))
+ chat_temp <- client$clone()
+ prompt <- "Please give me a friendly greeting. Include a few sample prompts in a two-level bulleted list."
+ chat_temp$stream_async(prompt)
+ }
+
+ shinychat::chat_append("chat", greeting_content)
+
+ append_stream_task <- shiny::ExtendedTask$new(
+ function(client, user_input) {
+ stream <- client$stream_async(
+ user_input,
+ stream = "content"
+ )
+
+ p <- promises::promise_resolve(stream)
+ promises::then(p, function(stream) {
+ shinychat::chat_append("chat", stream)
+ })
+ }
+ )
+
+ shiny::observeEvent(input$chat_user_input, label = "on_chat_user_input", {
+ append_stream_task$invoke(chat, input$chat_user_input)
+ })
+
+ shiny::observeEvent(input$chat_update, label = "on_chat_update", {
+ current_query(input$chat_update$query)
+ current_title(input$chat_update$title)
+ })
+
+ list(
+ client = chat,
+ sql = current_query,
+ title = current_title,
+ df = filtered_df
+ )
+ })
+}
diff --git a/pkg-r/R/querychat_tools.R b/pkg-r/R/querychat_tools.R
index 02baaf25..e37a7a7b 100644
--- a/pkg-r/R/querychat_tools.R
+++ b/pkg-r/R/querychat_tools.R
@@ -6,8 +6,7 @@
tool_update_dashboard <- function(
data_source,
current_query,
- current_title,
- filtered_df
+ current_title
) {
db_type <- get_db_type(data_source)
diff --git a/pkg-r/R/utils-ellmer.R b/pkg-r/R/utils-ellmer.R
index 50f4eb48..51002f8d 100644
--- a/pkg-r/R/utils-ellmer.R
+++ b/pkg-r/R/utils-ellmer.R
@@ -13,3 +13,47 @@ interpolate_package <- function(path, ..., .envir = parent.frame()) {
ellmer::interpolate_file(path, ..., .envir = .envir)
}
+
+
+as_querychat_client <- function(client = NULL) {
+ if (is.null(client)) {
+ client <- querychat_client_option()
+ }
+
+ if (is.null(client)) {
+ # Use OpenAI with ellmer's default model
+ return(ellmer::chat_openai())
+ }
+
+ if (rlang::is_function(client)) {
+ # `client` as a function was the first interface we supported and expected
+ # `system_prompt` as an argument. This avoids breaking existing code.
+ client <- client(system_prompt = NULL)
+ }
+
+ if (rlang::is_string(client)) {
+ client <- ellmer::chat(client)
+ }
+
+ if (!inherits(client, "Chat")) {
+ rlang::abort(
+ "`client` must be an {ellmer} Chat object or a function that returns one.",
+ )
+ }
+
+ client
+}
+
+querychat_client_option <- function() {
+ opt <- getOption("querychat.client", NULL)
+ if (!is.null(opt)) {
+ return(opt)
+ }
+
+ env <- Sys.getenv("QUERYCHAT_CLIENT", "")
+ if (nzchar(env)) {
+ return(env)
+ }
+
+ NULL
+}
diff --git a/pkg-r/README.md b/pkg-r/README.md
index ff75d6ec..23a67b46 100644
--- a/pkg-r/README.md
+++ b/pkg-r/README.md
@@ -20,36 +20,46 @@ pak::pak("posit-dev/querychat/pkg-r")
First, you'll need an OpenAI API key. See the [instructions from Ellmer](https://ellmer.tidyverse.org/reference/chat_openai.html). (Or use a different LLM provider, see below.)
-Here's a very minimal example that shows the three function calls you need to make.
+### Quick Start
+
+The fastest way to get started is with the built-in app:
+
+```r
+library(querychat)
+
+qc <- QueryChat$new(mtcars, "mtcars")
+qc$app()
+```
+
+This launches a complete Shiny app with a chat interface, SQL query display, and data table. Perfect for quick exploration and prototyping!
+
+### Custom Shiny Apps
+
+For more control, integrate querychat into your own Shiny app:
```r
library(shiny)
library(bslib)
library(querychat)
-# 1. Create a data source for querychat
-mtcars_source <- querychat_data_source(mtcars)
-
-# 2. Configure querychat with the data source
-querychat_config <- querychat_init(mtcars_source)
+# 1. Create a QueryChat instance with your data
+qc <- QueryChat$new(mtcars, "mtcars")
ui <- page_sidebar(
- # 3. Use querychat_sidebar(id) in a bslib::page_sidebar.
- # Alternatively, use querychat_ui(id) elsewhere if you don't want your
+ # 2. Use qc$sidebar() in a bslib::page_sidebar.
+ # Alternatively, use qc$ui() elsewhere if you don't want your
# chat interface to live in a sidebar.
- sidebar = querychat_sidebar("chat"),
+ sidebar = qc$sidebar(),
DT::DTOutput("dt")
)
server <- function(input, output, session) {
-
- # 4. Create a querychat object using the config from step 2.
- querychat <- querychat_server("chat", querychat_config)
+ # 3. Initialize the QueryChat server (returns session-specific reactive values)
+ qc_vals <- qc$server()
output$dt <- DT::renderDT({
- # 5. Use the filtered/sorted data frame anywhere you wish, via the
- # querychat$df() reactive.
- DT::datatable(querychat$df())
+ # 4. Use the filtered/sorted data frame anywhere you wish, via qc_vals$df()
+ DT::datatable(qc_vals$df())
})
}
@@ -70,13 +80,11 @@ library(RSQLite)
# 1. Connect to a database
conn <- DBI::dbConnect(RSQLite::SQLite(), "path/to/database.db")
-# 2. Create a database data source for querychat
-db_source <- querychat_data_source(conn, "table_name")
-
-# 3. Configure querychat with the database source
-querychat_config <- querychat_init(db_source)
+# 2. Create a QueryChat instance with the database connection
+qc <- QueryChat$new(conn, "table_name")
-# Then use querychat_config in your Shiny app as shown above
+# 3. Use it in your Shiny app as shown above
+qc$app()
```
## How it works
@@ -107,7 +115,7 @@ Currently, querychat uses DuckDB for its SQL engine when working with data frame
### Provide a greeting (recommended)
-When the querychat UI first appears, you will usually want it to greet the user with some basic instructions. By default, these instructions are auto-generated every time a user arrives; this is slow, wasteful, and unpredictable. Instead, you should create a file called `greeting.md`, and when calling `querychat_init`, pass `greeting = readLines("greeting.md")`.
+When the querychat UI first appears, you will usually want it to greet the user with some basic instructions. By default, these instructions are auto-generated every time a user arrives; this is potentially slow, wasteful, and unpredictable. Instead, you should create a file called `greeting.md`, and when creating your `QueryChat` instance, pass `greeting = "greeting.md"` (or use `readLines()` to read the file as a string).
You can provide suggestions to the user by using the ` ` tag.
@@ -128,11 +136,18 @@ For example:
These suggestions appear in the greeting and automatically populate the chat text box when clicked.
This gives the user a few ideas to explore on their own.
-If you need help coming up with a greeting, your own app can help you! Just launch it and paste this into the chat interface:
+You can use the `$generate_greeting()` method to help create a greeting:
-> Help me create a greeting for your future users. Include some example questions. Format your suggested greeting as Markdown, in a code block.
+```r
+qc <- QueryChat$new(mtcars, "mtcars")
+greeting <- qc$generate_greeting(echo = "text")
-And keep giving it feedback until you're happy with the result, which will then be ready to be pasted into `greeting.md`.
+# Save it for reuse
+writeLines(greeting, "greeting.md")
+
+# Then use it in your app
+qc <- QueryChat$new(mtcars, "mtcars", greeting = "greeting.md")
+```
Alternatively, you can completely suppress the greeting by passing `greeting = ""`.
@@ -183,13 +198,10 @@ performance for 32 automobiles (1973–74 models).
which you can then pass via:
```r
-# Create data source first
-mtcars_source <- querychat_data_source(mtcars, tbl_name = "cars")
-
-# Then initialize with the data source and description
-querychat_config <- querychat_init(
- data_source = mtcars_source,
- data_description = readLines("data_description.md")
+qc <- QueryChat$new(
+ mtcars,
+ "mtcars",
+ data_description = "data_description.md"
)
```
@@ -197,15 +209,12 @@ querychat doesn't need this information in any particular format; just put whate
#### Additional instructions
-You can add additional instructions of your own to the end of the system prompt, by passing `extra_instructions` into `query_init`.
+You can add additional instructions of your own to the end of the system prompt, by passing `extra_instructions` to `QueryChat$new()`.
```r
-# Create data source first
-mtcars_source <- querychat_data_source(mtcars, tbl_name = "cars")
-
-# Then initialize with instructions
-querychat_config <- querychat_init(
- data_source = mtcars_source,
+qc <- QueryChat$new(
+ mtcars,
+ "mtcars",
extra_instructions = c(
"You're speaking to a British audience--please use appropriate spelling conventions.",
"Use lots of emojis! 😃 Emojis everywhere, 🌍 emojis forever. ♾️",
@@ -214,22 +223,20 @@ querychat_config <- querychat_init(
)
```
-You can also put these instructions in a separate file and use `readLines()` to load them, as we did for `data_description` above.
+You can also put these instructions in a separate file and pass the file path, as we did for `data_description` above.
**Warning:** It is not 100% guaranteed that the LLM will always—or in many cases, ever—obey your instructions, and it can be difficult to predict which instructions will be a problem. So be sure to test extensively each time you change your instructions, and especially, if you change the model you use.
### Use a different LLM provider
-By default, querychat uses OpenAI with the default model chosen by `ellmer::chat_openai()`. If you want to use a different model, you can provide an ellmer chat object to the `client` argument of `querychat_init()`.
+By default, querychat uses OpenAI with the default model chosen by `ellmer::chat_openai()`. If you want to use a different model, you can provide an ellmer chat object to the `client` argument of `QueryChat$new()`.
```r
library(ellmer)
-library(purrr)
-
-mtcars_source <- querychat_data_source(mtcars, tbl_name = "cars")
-querychat_config <- querychat_init(
- data_source = mtcars_source,
+qc <- QueryChat$new(
+ mtcars,
+ "mtcars",
client = ellmer::chat_anthropic(model = "claude-3-7-sonnet-latest")
)
```
@@ -240,8 +247,9 @@ See the [instructions from Ellmer](https://ellmer.tidyverse.org/reference/chat_a
Alternatively, you can use a provider-model string, which will be passed to `ellmer::chat()`:
```r
-querychat_config <- querychat_init(
- data_source = mtcars_source,
+qc <- QueryChat$new(
+ mtcars,
+ "mtcars",
client = "anthropic/claude-3-7-sonnet-latest"
)
```
@@ -249,5 +257,5 @@ querychat_config <- querychat_init(
Or you can set the `querychat.client` R option to a chat object or provider-model string, which will be used as the default client for all querychat apps in your session:
```r
-option(querychat.client = "anthropic/claude-3-7-sonnet-latest")
+options(querychat.client = "anthropic/claude-3-7-sonnet-latest")
```
diff --git a/pkg-r/inst/examples-shiny/01-hello-app/app.R b/pkg-r/inst/examples-shiny/01-hello-app/app.R
new file mode 100644
index 00000000..a8d63a10
--- /dev/null
+++ b/pkg-r/inst/examples-shiny/01-hello-app/app.R
@@ -0,0 +1,12 @@
+library(querychat)
+library(palmerpenguins)
+
+# Create a QueryChat object and generate a complete app with $app()
+qc <- QueryChat$new(penguins, "penguins")
+qc$app()
+
+# That's it! The app includes:
+# - A sidebar with the chat interface
+# - SQL query display with syntax highlighting
+# - Data table showing filtered results
+# - Reset button to clear queries
diff --git a/pkg-r/inst/examples-shiny/02-sidebar-app/app.R b/pkg-r/inst/examples-shiny/02-sidebar-app/app.R
new file mode 100644
index 00000000..450ff6f1
--- /dev/null
+++ b/pkg-r/inst/examples-shiny/02-sidebar-app/app.R
@@ -0,0 +1,96 @@
+library(shiny)
+library(bslib)
+library(querychat)
+library(palmerpenguins)
+
+# Define a custom greeting for the penguins app
+greeting <- r"(
+# Welcome to the Palmer Penguins Explorer! 🐧
+
+I can help you explore and analyze the Palmer Penguins dataset. Ask me questions
+about the penguins, and I'll generate SQL queries to get the answers.
+
+Try asking:
+- Show me the first 10 rows of the penguins dataset
+- What's the average bill length by species?
+- Which species has the largest body mass?
+- Create a summary of measurements grouped by species and island
+)"
+
+# Create QueryChat object with custom options
+qc <- QueryChat$new(
+ penguins,
+ "penguins",
+ greeting = greeting,
+ data_description = paste(
+ "The Palmer Penguins dataset contains measurements of bill",
+ "dimensions, flipper length, body mass, sex, and species",
+ "(Adelie, Chinstrap, and Gentoo) collected from three islands in",
+ "the Palmer Archipelago, Antarctica."
+ ),
+ extra_instructions = paste(
+ "When showing results, always explain what the data represents",
+ "and highlight any interesting patterns you observe."
+ )
+)
+
+
+# Define custom UI with sidebar
+ui <- page_sidebar(
+ title = "Palmer Penguins Chat Explorer",
+ sidebar = qc$sidebar(),
+
+ card(
+ fill = FALSE,
+ card_header("Current SQL Query"),
+ verbatimTextOutput("sql_query")
+ ),
+
+ card(
+ full_screen = TRUE,
+ card_header(
+ "Current Data View",
+ tooltip(
+ bsicons::bs_icon("question-circle-fill", class = "mx-1"),
+ "The table below shows the current filtered data based on your chat queries"
+ ),
+ tooltip(
+ bsicons::bs_icon("info-circle-fill"),
+ "The penguins dataset contains measurements on 344 penguins."
+ )
+ ),
+ DT::DTOutput("data_table"),
+ card_footer(
+ markdown(
+ "Data source: [palmerpenguins package](https://allisonhorst.github.io/palmerpenguins/)"
+ )
+ )
+ )
+)
+
+# Define server logic
+server <- function(input, output, session) {
+ # Initialize QueryChat server
+ qc_vals <- qc$server()
+
+ # Render the data table
+ output$data_table <- DT::renderDT(
+ {
+ qc_vals$df()
+ },
+ fillContainer = TRUE,
+ options = list(pageLength = 25, scrollX = TRUE)
+ )
+
+ # Render the SQL query
+ output$sql_query <- renderText({
+ query <- qc_vals$sql()
+ if (query == "") {
+ "No filter applied - showing all data"
+ } else {
+ query
+ }
+ })
+}
+
+shinyApp(ui = ui, server = server)
diff --git a/pkg-r/inst/examples-shiny/sqlite/README.md b/pkg-r/inst/examples-shiny/sqlite/README.md
index 31426f9e..053b0432 100644
--- a/pkg-r/inst/examples-shiny/sqlite/README.md
+++ b/pkg-r/inst/examples-shiny/sqlite/README.md
@@ -1,6 +1,6 @@
# Database Setup Examples for querychat
-This document provides examples of how to set up querychat with various database types using the new `database_source()` functionality.
+This document provides examples of how to set up querychat with various database types.
## SQLite
@@ -12,15 +12,16 @@ library(querychat)
# Connect to SQLite database
conn <- dbConnect(RSQLite::SQLite(), "path/to/your/database.db")
-# Create database source
-db_source <- database_source(conn, "your_table_name")
-
-# Initialize querychat
-config <- querychat_init(
- data_source = db_source,
+# Create QueryChat instance
+qc <- QueryChat$new(
+ conn,
+ "your_table_name",
greeting = "Welcome! Ask me about your data.",
data_description = "Description of your data..."
)
+
+# Launch the app
+qc$app()
```
## PostgreSQL
@@ -40,11 +41,11 @@ conn <- dbConnect(
password = "your_password"
)
-# Create database source
-db_source <- database_source(conn, "your_table_name")
+# Create QueryChat instance
+qc <- QueryChat$new(conn, "your_table_name")
-# Initialize querychat
-config <- querychat_init(data_source = db_source)
+# Launch the app
+qc$app()
```
## MySQL
@@ -63,22 +64,25 @@ conn <- dbConnect(
password = "your_password"
)
-# Create database source
-db_source <- database_source(conn, "your_table_name")
+# Create QueryChat instance
+qc <- QueryChat$new(conn, "your_table_name")
-# Initialize querychat
-config <- querychat_init(data_source = db_source)
+# Launch the app
+qc$app()
```
## Connection Management
-When using database sources in Shiny apps, make sure to properly manage connections:
+When using database sources in custom Shiny apps, make sure to properly manage connections:
```r
server <- function(input, output, session) {
- # Your querychat server logic here
- chat <- querychat_server("chat", querychat_config)
-
+ # Initialize QueryChat server
+ qc$server()
+
+ # Your custom outputs here
+ output$table <- renderTable(qc$df())
+
# Clean up connection when session ends
session$onSessionEnded(function() {
if (dbIsValid(conn)) {
@@ -88,15 +92,6 @@ server <- function(input, output, session) {
}
```
-## Configuration Options
-
-The `database_source()` function accepts a `categorical_threshold` parameter:
-
-```r
-# Columns with <= 50 unique values will be treated as categorical
-db_source <- database_source(conn, "table_name", categorical_threshold = 50)
-```
-
## Security Considerations
- Only SELECT queries are allowed - no INSERT, UPDATE, or DELETE operations
diff --git a/pkg-r/inst/examples-shiny/sqlite/app.R b/pkg-r/inst/examples-shiny/sqlite/app.R
index 6e7bbbe5..63dcbd4a 100644
--- a/pkg-r/inst/examples-shiny/sqlite/app.R
+++ b/pkg-r/inst/examples-shiny/sqlite/app.R
@@ -15,8 +15,6 @@ onStop(function() {
})
conn <- dbConnect(RSQLite::SQLite(), temp_db)
-# The connection will automatically be closed when the app stops, thanks to
-# querychat_init
# Create sample data in the database
dbWriteTable(conn, "penguins", palmerpenguins::penguins, overwrite = TRUE)
@@ -35,12 +33,10 @@ Try asking:
- Create a summary of measurements grouped by species and island
"
-# Create data source using querychat_data_source
-penguins_source <- querychat_data_source(conn, table_name = "penguins")
-
-# Configure querychat for database
-querychat_config <- querychat_init(
- data_source = penguins_source,
+# Create QueryChat object with database connection
+qc <- QueryChat$new(
+ conn,
+ "penguins",
greeting = greeting,
data_description = "This database contains the Palmer Penguins dataset with measurements of bill dimensions, flipper length, body mass, sex, and species (Adelie, Chinstrap, and Gentoo) collected from three islands in the Palmer Archipelago, Antarctica.",
extra_instructions = "When showing results, always explain what the data represents and highlight any interesting patterns you observe."
@@ -48,39 +44,49 @@ querychat_config <- querychat_init(
ui <- page_sidebar(
title = "Database Query Chat",
- sidebar = querychat_sidebar("chat"),
+ sidebar = qc$sidebar(),
- h2("Current Data View"),
- p(
- "The table below shows the current filtered data based on your chat queries:"
+ card(
+ fill = FALSE,
+ card_header("Current SQL Query"),
+ verbatimTextOutput("sql_query")
),
- DT::DTOutput("data_table", fill = FALSE),
-
- h2("Current SQL Query"),
- verbatimTextOutput("sql_query"),
- h2("Dataset Information"),
- p("This demo database contains:"),
- tags$ul(
- tags$li("penguins - Palmer Penguins dataset (344 rows, 8 columns)"),
- tags$li(
- "Columns: species, island, bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g, sex, year"
+ card(
+ full_screen = TRUE,
+ card_header(
+ "Current Data View",
+ tooltip(
+ bsicons::bs_icon("question-circle-fill", class = "mx-1"),
+ "The table below shows the current filtered data based on your chat queries"
+ ),
+ tooltip(
+ bsicons::bs_icon("info-circle-fill"),
+ "The penguins dataset contains measurements on 344 penguins."
+ )
+ ),
+ DT::DTOutput("data_table"),
+ card_footer(
+ markdown(
+ "Data source: [palmerpenguins package](https://allisonhorst.github.io/palmerpenguins/)"
+ )
)
)
)
server <- function(input, output, session) {
- chat <- querychat_server("chat", querychat_config)
+ qc_vals <- qc$server()
output$data_table <- DT::renderDT(
{
- chat$df()
+ qc_vals$df()
},
+ fillContainer = TRUE,
options = list(pageLength = 10, scrollX = TRUE)
)
output$sql_query <- renderText({
- query <- chat$sql()
+ query <- qc_vals$sql()
if (query == "") {
"No filter applied - showing all data"
} else {
diff --git a/pkg-r/man/QueryChat.Rd b/pkg-r/man/QueryChat.Rd
new file mode 100644
index 00000000..6612e235
--- /dev/null
+++ b/pkg-r/man/QueryChat.Rd
@@ -0,0 +1,618 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/QueryChat.R
+\name{QueryChat}
+\alias{QueryChat}
+\title{QueryChat: Interactive Data Querying with Natural Language}
+\description{
+\code{QueryChat} is an R6 class built on Shiny, shinychat, and ellmer to enable
+interactive querying of data using natural language. It leverages large
+language models (LLMs) to translate user questions into SQL queries, execute
+them against a data source (data frame or database), and various ways of
+accessing/displaying the results.
+}
+\details{
+The \code{QueryChat} class takes your data (a data frame or database connection)
+as input and provides methods to:
+\itemize{
+\item Generate a chat UI for natural language queries (e.g., \verb{$app()}, \verb{$sidebar()})
+\item Initialize server logic that returns session-specific reactive values (via \verb{$server()})
+\item Access reactive data, SQL queries, and titles through the returned server values
+}
+}
+\section{Usage}{
+
+
+\if{html}{\out{