From ec2b2bf366505275ee3b9c043961fcbce8633b5f Mon Sep 17 00:00:00 2001 From: Carson Date: Fri, 21 Nov 2025 17:47:37 -0600 Subject: [PATCH 01/25] feat(pkg-r): first pass at new QueryChat() API --- pkg-r/DESCRIPTION | 7 +- pkg-r/NAMESPACE | 8 +- pkg-r/NEWS.md | 3 + pkg-r/R/QueryChat-class.R | 658 +++++++++++++++ pkg-r/R/data_source.R | 91 +- pkg-r/R/deprecated.R | 158 ++++ pkg-r/R/querychat-package.R | 53 ++ pkg-r/R/querychat.R | 309 ------- pkg-r/R/querychat_app.R | 138 --- pkg-r/R/querychat_client.R | 42 - pkg-r/R/querychat_greeting.R | 77 -- pkg-r/R/querychat_module.R | 105 +++ pkg-r/R/querychat_tools.R | 3 +- pkg-r/R/utils-ellmer.R | 44 + pkg-r/README.md | 106 +-- pkg-r/inst/examples-shiny/01-hello-app/app.R | 15 + .../inst/examples-shiny/02-sidebar-app/app.R | 73 ++ pkg-r/inst/examples-shiny/sqlite/README.md | 51 +- pkg-r/inst/examples-shiny/sqlite/app.R | 20 +- pkg-r/man/QueryChat.Rd | 784 ++++++++++++++++++ pkg-r/man/cleanup_source.Rd | 5 +- pkg-r/man/create_data_source.Rd | 26 + pkg-r/man/create_system_prompt.Rd | 9 +- pkg-r/man/deprecated.Rd | 32 + pkg-r/man/execute_query.Rd | 9 +- pkg-r/man/get_db_type.Rd | 5 +- pkg-r/man/get_schema.Rd | 9 +- pkg-r/man/querychat-package.Rd | 63 +- pkg-r/man/querychat_app.Rd | 39 - pkg-r/man/querychat_data_source.Rd | 34 - pkg-r/man/querychat_greeting.Rd | 55 -- pkg-r/man/querychat_init.Rd | 70 -- pkg-r/man/querychat_server.Rd | 27 - pkg-r/man/querychat_ui.Rd | 29 - pkg-r/man/test_query.Rd | 5 +- pkg-r/pkgdown/_pkgdown.yml | 7 +- pkg-r/tests/testthat/apps/basic/app.R | 18 +- pkg-r/tests/testthat/test-data-source.R | 66 +- pkg-r/tests/testthat/test-db-type.R | 8 +- .../tests/testthat/test-querychat-greeting.R | 46 - pkg-r/tests/testthat/test-querychat-server.R | 2 +- pkg-r/tests/testthat/test-shiny-app.R | 15 +- pkg-r/tests/testthat/test-sql-comments.R | 10 +- pkg-r/tests/testthat/test-test-query.R | 6 +- 44 files changed, 2258 insertions(+), 1082 deletions(-) create mode 100644 pkg-r/R/QueryChat-class.R create mode 100644 pkg-r/R/deprecated.R delete mode 100644 pkg-r/R/querychat.R delete mode 100644 pkg-r/R/querychat_app.R delete mode 100644 pkg-r/R/querychat_client.R delete mode 100644 pkg-r/R/querychat_greeting.R create mode 100644 pkg-r/R/querychat_module.R create mode 100644 pkg-r/inst/examples-shiny/01-hello-app/app.R create mode 100644 pkg-r/inst/examples-shiny/02-sidebar-app/app.R create mode 100644 pkg-r/man/QueryChat.Rd create mode 100644 pkg-r/man/create_data_source.Rd create mode 100644 pkg-r/man/deprecated.Rd delete mode 100644 pkg-r/man/querychat_app.Rd delete mode 100644 pkg-r/man/querychat_data_source.Rd delete mode 100644 pkg-r/man/querychat_greeting.Rd delete mode 100644 pkg-r/man/querychat_init.Rd delete mode 100644 pkg-r/man/querychat_server.Rd delete mode 100644 pkg-r/man/querychat_ui.Rd delete mode 100644 pkg-r/tests/testthat/test-querychat-greeting.R diff --git a/pkg-r/DESCRIPTION b/pkg-r/DESCRIPTION index f11321b5..29339371 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")) @@ -26,14 +26,13 @@ Imports: rlang, shiny, shinychat (>= 0.2.0.9000), + R6, 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..92ecc69a 100644 --- a/pkg-r/NAMESPACE +++ b/pkg-r/NAMESPACE @@ -1,16 +1,18 @@ # Generated by roxygen2: do not edit by hand S3method(cleanup_source,dbi_source) +S3method(create_data_source,DBIConnection) +S3method(create_data_source,data.frame) S3method(create_system_prompt,querychat_data_source) S3method(execute_query,dbi_source) 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(cleanup_source) +export(create_data_source) export(create_system_prompt) export(execute_query) export(get_db_type) @@ -23,4 +25,6 @@ export(querychat_server) export(querychat_sidebar) export(querychat_ui) export(test_query) +importFrom(R6,R6Class) +importFrom(bslib,sidebar) importFrom(lifecycle,deprecated) diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md index 74aced17..ea64625f 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). (#xxx) + * In addition, `querychat_data_source()` was renamed to `create_data_source()`, and remains exported for a developer extension point, but users no longer have to explicitly create a data source. (#xxx) + * 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-class.R b/pkg-r/R/QueryChat-class.R new file mode 100644 index 00000000..edde7198 --- /dev/null +++ b/pkg-r/R/QueryChat-class.R @@ -0,0 +1,658 @@ +#' 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()`) +#' - Reactively read SQL results in your Shiny app (e.g., `$df()`) +#' - Programmatically get/set the current query and title (e.g., `$sql()`, `$title()`) +#' +#' @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$server() +#' +#' output$sql <- renderText(qc$sql()) +#' output$data <- renderDataTable(qc$df()) +#' } +#' +#' shinyApp(ui, server) +#' ``` +#' +#' @section Constructor: +#' `QueryChat$new(data_source, table_name, id = NULL, greeting = NULL, +#' client = NULL, data_description = NULL, +#' extra_instructions = NULL, prompt_template = NULL)` +#' +#' @section Methods: +#' \describe{ +#' \item{`$new(...)`}{Create a new QueryChat object.} +#' \item{`$sidebar(...)`}{Create a sidebar UI with chat interface.} +#' \item{`$ui(...)`}{Create the chat UI component.} +#' \item{`$server()`}{Initialize server logic (call within server function).} +#' \item{`$df()`}{Get the current filtered data frame (reactive).} +#' \item{`$sql(query)`}{Get or set the current SQL query (reactive).} +#' \item{`$title(value)`}{Get or set the current title (reactive).} +#' \item{`$app()`}{Create a complete Shiny app with sensible defaults.} +#' \item{`$generate_greeting(echo)`}{Generate a greeting message using the LLM.} +#' \item{`$cleanup()`}{Clean up data source resources.} +#' } +#' +#' @field data_source The normalized data source object (read-only). +#' @field client The LLM chat client (read-only, session-specific). +#' @field greeting The greeting message displayed to users. +#' @field id The module ID for namespacing. +#' +#' @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( + greeting = NULL, + 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 auto_cleanup Logical indicating whether to automatically close the + #' data source when the Shiny app stops. Default is `TRUE`. + #' + #' @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, + auto_cleanup = TRUE + ) { + 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 <- normalize_client(client) + private$.client <- client$clone() + private$.client$set_turns(list()) + private$.client$set_system_prompt(prompt) + + if (auto_cleanup) { + # 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...") + self$cleanup() + }) + } + }, + + #' @description + #' Create a complete Shiny app with sensible defaults. + #' + #' This is a convenience method that creates a full Shiny application 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 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()] or + #' passed to [shiny::shinyApp()]. + #' + #' @examples + #' \dontrun{ + #' library(querychat) + #' + #' qc <- QueryChat$new(mtcars, "mtcars") + #' app <- qc$app() + #' + #' # Run the app + #' shiny::runApp(app) + #' + #' # Or return from a script + #' app + #' } + app = function(bookmark_store = "url") { + rlang::check_installed("DT") + rlang::check_installed("bsicons") + + table_name <- private$.data_source$table_name + + ui <- function(req) { + bslib::page_sidebar( + title = shiny::HTML(paste0( + "querychat with ", + 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( + 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) { + self$server() + chat <<- private$server_values$chat + + output$query_title <- shiny::renderText({ + if (shiny::isTruthy(self$title())) { + self$title() + } else { + "SQL Query" + } + }) + + output$ui_reset <- shiny::renderUI({ + shiny::req(self$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", { + self$sql("") + self$title(NULL) + }) + + output$dt <- DT::renderDT({ + DT::datatable(self$df()) + }) + + output$sql_output <- shiny::renderUI({ + sql <- if (shiny::isTruthy(self$sql())) { + self$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() + }) + } + + app <- shiny::shinyApp(ui, server, enableBookmarking = bookmark_store) + tryCatch(shiny::runGadget(app), interrupt = function(cnd) NULL) + invisible(chat) + }, + + #' @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 populates the internal + #' server values that are accessed via `$df()`, `$sql()`, and `$title()`. + #' + #' @param session The Shiny session object. + #' + #' @return Invisibly returns `NULL`. Access reactive values via `$df()`, + #' `$sql()`, and `$title()` methods. + #' + #' @examples + #' \dontrun{ + #' qc <- QueryChat$new(mtcars, "mtcars") + #' + #' server <- function(input, output, session) { + #' qc$server() + #' + #' output$data <- renderDataTable(qc$df()) + #' output$query <- renderText(qc$sql()) + #' } + #' } + server = function(session = shiny::getDefaultReactiveDomain()) { + if (is.null(session)) { + rlang::abort( + "$server() must be called within a Shiny server function." + ) + } + + private$server_values <- mod_server( + self$id, + data_source = private$.data_source, + greeting = self$greeting, + client = private$.client + ) + + invisible(NULL) + }, + + #' @description + #' Get the current filtered data frame. + #' + #' This is a reactive expression that returns the data after applying the + #' current SQL query. If no query has been set, returns the unfiltered data. + #' + #' @return A data.frame with the filtered/transformed data. + #' + #' @examples + #' \dontrun{ + #' qc <- QueryChat$new(mtcars, "mtcars") + #' + #' server <- function(input, output, session) { + #' qc$server() + #' + #' output$table <- renderDataTable({ + #' qc$df() # Reactive - will update when query changes + #' }) + #' } + #' } + df = function() { + vals <- private$server_values + if (is.null(vals)) { + rlang::abort( + "Must call $server() before accessing $df(). Make sure to call server() within your Shiny server function." + ) + } + vals$df() + }, + + #' @description + #' Get or set the current SQL query. + #' + #' This method provides both getter and setter functionality for the SQL + #' query. When called without arguments, it returns the current query. + #' When called with a query string, it sets the query and returns whether + #' the query changed. + #' + #' @param query Optional SQL query string. If provided, sets the current + #' query to this value. If `NULL` (default), returns the current query. + #' + #' @return + #' - When `query = NULL` (getter): Returns the current SQL query as a string + #' (may be an empty string `""` if no query has been set). + #' - When `query` is provided (setter): Returns `TRUE` if the query was + #' changed to a new value, `FALSE` if it was the same as the current value. + #' + #' @examples + #' \dontrun{ + #' qc <- QueryChat$new(mtcars, "mtcars") + #' + #' server <- function(input, output, session) { + #' qc$server() + #' + #' # Get current query + #' output$current_query <- renderText({ + #' qc$sql() + #' }) + #' + #' # Set query programmatically + #' observeEvent(input$filter_button, { + #' qc$sql("SELECT * FROM mtcars WHERE cyl = 6") + #' }) + #' } + #' } + sql = function(query = NULL) { + vals <- private$server_values + if (is.null(vals)) { + rlang::abort( + "Must call $server() before accessing $sql(). Make sure to call $server() within your Shiny server function." + ) + } + + if (is.null(query)) { + vals$sql() + } else { + old_query <- shiny::isolate(vals$sql()) + vals$sql(query) + return(!identical(old_query, query)) + } + }, + + #' @description + #' Get or set the current title. + #' + #' The title is a short description of the current query that the LLM + #' provides whenever it generates a new SQL query. It can be used as a + #' status string for the data dashboard. + #' + #' @param value Optional title string. If provided, sets the current title + #' to this value. If `NULL` (default), returns the current title. + #' + #' @return + #' - When `value = NULL` (getter): Returns the current title as a string, + #' or `NULL` if no title has been set (because no SQL query has been set). + #' - When `value` is provided (setter): Returns `TRUE` if the title was + #' changed to a new value, `FALSE` if it was the same as the current value. + #' + #' @examples + #' \dontrun{ + #' qc <- QueryChat$new(mtcars, "mtcars") + #' + #' server <- function(input, output, session) { + #' qc$server() + #' + #' # Get current title + #' output$title <- renderText({ + #' qc$title() %||% "No Query" + #' }) + #' + #' # Set title programmatically + #' observeEvent(input$set_title, { + #' qc$title("Filtered Cars") + #' }) + #' } + #' } + title = function(value = NULL) { + vals <- private$server_values + if (is.null(vals)) { + rlang::abort( + "Must call $server() before accessing $title(). Make sure to call $server() within your Shiny server function." + ) + } + + if (is.null(value)) { + vals$title() + } else { + old_value <- shiny::isolate(vals$title()) + vals$title(value) + return(!identical(old_value, value)) + } + }, + + #' @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 `"text"` (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(echo = "text") + #' 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 client Get the (session-specific) chat client. + client = function() { + vals <- private$server_values + if (is.null(vals)) { + rlang::abort("Must call $server() before accessing $client") + } + vals$chat + }, + + #' @field data_source Get the current data source. + data_source = function() { + private$.data_source + } + ) +) + + +normalize_data_source <- function(data_source, table_name) { + if (is_data_source(data_source)) { + return(data_source) + } else { + create_data_source(data_source, table_name) + } +} \ No newline at end of file diff --git a/pkg-r/R/data_source.R b/pkg-r/R/data_source.R index d7beb36b..6c77d637 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") +create_data_source <- function(x, table_name = NULL, ...) { + UseMethod("create_data_source") } #' @export -#' @rdname querychat_data_source -querychat_data_source.data.frame <- function( - x, - table_name = NULL, - categorical_threshold = 20, - ... -) { +create_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, - ... -) { +create_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,25 @@ 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") @@ -121,11 +109,16 @@ 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") @@ -141,10 +134,15 @@ 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") @@ -183,17 +181,25 @@ 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 +210,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 +226,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 +245,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") @@ -256,20 +268,25 @@ 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..e9875f51 --- /dev/null +++ b/pkg-r/R/deprecated.R @@ -0,0 +1,158 @@ +#' 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_app <- function(...) { + lifecycle::deprecate_stop( + when = "0.1.0", + what = "querychat_app()", + with = "QueryChat$app()", + details = c( + "Old code:", + " querychat_app(config)", + "", + "New code:", + " qc <- QueryChat$new(data, 'table_name')", + " qc$app()", + "", + "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..7e809b2a 100644 --- a/pkg-r/R/querychat-package.R +++ b/pkg-r/R/querychat-package.R @@ -1,7 +1,60 @@ +#' 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 +#' - [create_data_source()]: (Advanced) Create custom data source objects +#' +#' @section Examples: +#' See the package examples directory for complete working apps: +#' - `01-hello-app/`: Minimal example using `$app()` +#' - `02-sidebar-app/`: Custom layout with additional UI elements +#' - `sqlite/`: Database connection example +#' #' @keywords internal "_PACKAGE" ## usethis namespace: start #' @importFrom lifecycle deprecated +#' @importFrom R6 R6Class +#' @importFrom bslib sidebar ## 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..19f2bbf7 --- /dev/null +++ b/pkg-r/R/querychat_module.R @@ -0,0 +1,105 @@ +# 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%", + fill = TRUE, + 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( + chat = chat, + sql = current_query, + title = current_title, + df = filtered_df + ) + }) +} \ No newline at end of file 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..ac0299e3 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) } + + +normalize_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..7354e69e 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 + 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$df() + DT::datatable(qc$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 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..af2b3ac0 --- /dev/null +++ b/pkg-r/inst/examples-shiny/01-hello-app/app.R @@ -0,0 +1,15 @@ +library(querychat) +library(palmerpenguins) + +# Create a QueryChat object and generate a complete app with $app() +qc <- QueryChat$new(penguins, "penguins") +app <- 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 + +# Run with: shiny::runApp() +app 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..ef4ad120 --- /dev/null +++ b/pkg-r/inst/examples-shiny/02-sidebar-app/app.R @@ -0,0 +1,73 @@ +library(shiny) +library(bslib) +library(querychat) +library(palmerpenguins) + +# Define a custom greeting for the penguins app +greeting <- " +# 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 = "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 = "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(), + + h2("Current Data View"), + p("The table below shows the current filtered data based on your chat queries:"), + DT::DTOutput("data_table", fill = FALSE), + + h2("Current SQL Query"), + verbatimTextOutput("sql_query"), + + h2("Dataset Information"), + p("This dataset contains:"), + tags$ul( + tags$li("344 observations of penguins"), + tags$li("Columns: species, island, bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g, sex, year") + ) +) + +# Define server logic +server <- function(input, output, session) { + # Initialize QueryChat server + qc$server() + + # Render the data table + output$data_table <- DT::renderDT( + { + qc$df() + }, + options = list(pageLength = 10, scrollX = TRUE) + ) + + # Render the SQL query + output$sql_query <- renderText({ + query <- qc$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..3c0340c7 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,7 +44,7 @@ querychat_config <- querychat_init( ui <- page_sidebar( title = "Database Query Chat", - sidebar = querychat_sidebar("chat"), + sidebar = qc$sidebar(), h2("Current Data View"), p( @@ -70,17 +66,17 @@ ui <- page_sidebar( ) server <- function(input, output, session) { - chat <- querychat_server("chat", querychat_config) + qc$server() output$data_table <- DT::renderDT( { - chat$df() + qc$df() }, options = list(pageLength = 10, scrollX = TRUE) ) output$sql_query <- renderText({ - query <- chat$sql() + query <- qc$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..158ef6dd --- /dev/null +++ b/pkg-r/man/QueryChat.Rd @@ -0,0 +1,784 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/QueryChat-class.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 Reactively read SQL results in your Shiny app (e.g., \verb{$df()}) +\item Programmatically get/set the current query and title (e.g., \verb{$sql()}, \verb{$title()}) +} +} +\section{Usage}{ + + +\if{html}{\out{
}}\preformatted{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$server() + + output$sql <- renderText(qc$sql()) + output$data <- renderDataTable(qc$df()) +\} + +shinyApp(ui, server) +}\if{html}{\out{
}} +} + +\section{Constructor}{ + +\code{QueryChat$new(data_source, table_name, id = NULL, greeting = NULL, client = NULL, data_description = NULL, extra_instructions = NULL, prompt_template = NULL)} +} + +\section{Methods}{ + +\describe{ +\item{\verb{$new(...)}}{Create a new QueryChat object.} +\item{\verb{$sidebar(...)}}{Create a sidebar UI with chat interface.} +\item{\verb{$ui(...)}}{Create the chat UI component.} +\item{\verb{$server()}}{Initialize server logic (call within server function).} +\item{\verb{$df()}}{Get the current filtered data frame (reactive).} +\item{\verb{$sql(query)}}{Get or set the current SQL query (reactive).} +\item{\verb{$title(value)}}{Get or set the current title (reactive).} +\item{\verb{$app()}}{Create a complete Shiny app with sensible defaults.} +\item{\verb{$generate_greeting(echo)}}{Generate a greeting message using the LLM.} +\item{\verb{$cleanup()}}{Clean up data source resources.} +} +} + +\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") +} + +## ------------------------------------------------ +## Method `QueryChat$new` +## ------------------------------------------------ + +\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") +} + +## ------------------------------------------------ +## Method `QueryChat$app` +## ------------------------------------------------ + +\dontrun{ +library(querychat) + +qc <- QueryChat$new(mtcars, "mtcars") +app <- qc$app() + +# Run the app +shiny::runApp(app) + +# Or return from a script +app +} + +## ------------------------------------------------ +## Method `QueryChat$sidebar` +## ------------------------------------------------ + +\dontrun{ +qc <- QueryChat$new(mtcars, "mtcars") + +ui <- page_sidebar( + qc$sidebar(), + # Main content here +) +} + +## ------------------------------------------------ +## Method `QueryChat$ui` +## ------------------------------------------------ + +\dontrun{ +qc <- QueryChat$new(mtcars, "mtcars") + +ui <- fluidPage( + qc$ui() +) +} + +## ------------------------------------------------ +## Method `QueryChat$server` +## ------------------------------------------------ + +\dontrun{ +qc <- QueryChat$new(mtcars, "mtcars") + +server <- function(input, output, session) { + qc$server() + + output$data <- renderDataTable(qc$df()) + output$query <- renderText(qc$sql()) +} +} + +## ------------------------------------------------ +## Method `QueryChat$df` +## ------------------------------------------------ + +\dontrun{ +qc <- QueryChat$new(mtcars, "mtcars") + +server <- function(input, output, session) { + qc$server() + + output$table <- renderDataTable({ + qc$df() # Reactive - will update when query changes + }) +} +} + +## ------------------------------------------------ +## Method `QueryChat$sql` +## ------------------------------------------------ + +\dontrun{ +qc <- QueryChat$new(mtcars, "mtcars") + +server <- function(input, output, session) { + qc$server() + + # Get current query + output$current_query <- renderText({ + qc$sql() + }) + + # Set query programmatically + observeEvent(input$filter_button, { + qc$sql("SELECT * FROM mtcars WHERE cyl = 6") + }) +} +} + +## ------------------------------------------------ +## Method `QueryChat$title` +## ------------------------------------------------ + +\dontrun{ +qc <- QueryChat$new(mtcars, "mtcars") + +server <- function(input, output, session) { + qc$server() + + # Get current title + output$title <- renderText({ + qc$title() \%||\% "No Query" + }) + + # Set title programmatically + observeEvent(input$set_title, { + qc$title("Filtered Cars") + }) +} +} + +## ------------------------------------------------ +## Method `QueryChat$generate_greeting` +## ------------------------------------------------ + +\dontrun{ +# Create QueryChat object +qc <- QueryChat$new(mtcars, "mtcars") + +# Generate a greeting and save it +greeting <- qc$generate_greeting(echo = "text") +writeLines(greeting, "mtcars_greeting.md") + +# Later, use the saved greeting +qc2 <- QueryChat$new(mtcars, "mtcars", greeting = "mtcars_greeting.md") +} +} +\section{Public fields}{ +\if{html}{\out{
}} +\describe{ +\item{\code{greeting}}{The greeting message displayed to users.} + +\item{\code{id}}{The module ID for namespacing.} +} +\if{html}{\out{
}} +} +\section{Active bindings}{ +\if{html}{\out{
}} +\describe{ +\item{\code{data_source}}{The normalized data source object (read-only).} + +\item{\code{client}}{The LLM chat client (read-only, session-specific).} + +\item{\code{client}}{Get the (session-specific) chat client.} + +\item{\code{data_source}}{Get the current data source.} +} +\if{html}{\out{
}} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-QueryChat-new}{\code{QueryChat$new()}} +\item \href{#method-QueryChat-app}{\code{QueryChat$app()}} +\item \href{#method-QueryChat-sidebar}{\code{QueryChat$sidebar()}} +\item \href{#method-QueryChat-ui}{\code{QueryChat$ui()}} +\item \href{#method-QueryChat-server}{\code{QueryChat$server()}} +\item \href{#method-QueryChat-df}{\code{QueryChat$df()}} +\item \href{#method-QueryChat-sql}{\code{QueryChat$sql()}} +\item \href{#method-QueryChat-title}{\code{QueryChat$title()}} +\item \href{#method-QueryChat-generate_greeting}{\code{QueryChat$generate_greeting()}} +\item \href{#method-QueryChat-cleanup}{\code{QueryChat$cleanup()}} +\item \href{#method-QueryChat-clone}{\code{QueryChat$clone()}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-QueryChat-new}{}}} +\subsection{Method \code{new()}}{ +Create a new QueryChat object. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{QueryChat$new( + data_source, + table_name, + ..., + id = NULL, + greeting = NULL, + client = NULL, + data_description = NULL, + categorical_threshold = 20, + extra_instructions = NULL, + prompt_template = NULL, + auto_cleanup = TRUE +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{data_source}}{Either a data.frame or a database connection (e.g., DBI +connection).} + +\item{\code{table_name}}{A string specifying the table name to use in SQL queries. +If \code{data_source} is a data.frame, this is the name to refer to it by in +queries (typically the variable name). If \code{data_source} is a database +connection, this is the name of the table in the database.} + +\item{\code{...}}{Additional arguments (currently unused).} + +\item{\code{id}}{Optional module ID for the QueryChat instance. If not provided, +will be auto-generated from \code{table_name}. The ID is used to namespace +the Shiny module.} + +\item{\code{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 \verb{$generate_greeting()} to create +a greeting to save and reuse.} + +\item{\code{client}}{Optional chat client. Can be: +\itemize{ +\item An \link[ellmer:Chat]{ellmer::Chat} object +\item A string to pass to \code{\link[ellmer:chat-any]{ellmer::chat()}} (e.g., \code{"openai/gpt-4o"}) +\item \code{NULL} (default): Uses the \code{querychat.client} option, the +\code{QUERYCHAT_CLIENT} environment variable, or defaults to +\code{\link[ellmer:chat_openai]{ellmer::chat_openai()}} +}} + +\item{\code{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.} + +\item{\code{categorical_threshold}}{For text columns, the maximum number of unique +values to consider as a categorical variable. Default is 20.} + +\item{\code{extra_instructions}}{Optional additional instructions for the chat +model in plain text or Markdown. Can be a string or a file path.} + +\item{\code{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.} + +\item{\code{auto_cleanup}}{Logical indicating whether to automatically close the +data source when the Shiny app stops. Default is \code{TRUE}.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A new \code{QueryChat} object. +} +\subsection{Examples}{ +\if{html}{\out{
}} +\preformatted{\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") +} +} +\if{html}{\out{
}} + +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-QueryChat-app}{}}} +\subsection{Method \code{app()}}{ +Create a complete Shiny app with sensible defaults. + +This is a convenience method that creates a full Shiny application with: +\itemize{ +\item A sidebar containing the chat interface +\item A card displaying the current SQL query +\item A card displaying the filtered data table +\item A reset button to clear the query +} +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{QueryChat$app(bookmark_store = "url")}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{bookmark_store}}{The bookmarking storage method. Passed to +\code{\link[shiny:enableBookmarking]{shiny::enableBookmarking()}}. If \code{"url"} or \code{"server"}, the chat state +(including current query) will be bookmarked. Default is \code{"url"}.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A Shiny app object that can be run with \code{\link[shiny:runApp]{shiny::runApp()}} or +passed to \code{\link[shiny:shinyApp]{shiny::shinyApp()}}. +} +\subsection{Examples}{ +\if{html}{\out{
}} +\preformatted{\dontrun{ +library(querychat) + +qc <- QueryChat$new(mtcars, "mtcars") +app <- qc$app() + +# Run the app +shiny::runApp(app) + +# Or return from a script +app +} +} +\if{html}{\out{
}} + +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-QueryChat-sidebar}{}}} +\subsection{Method \code{sidebar()}}{ +Create a sidebar containing the querychat UI. + +This method generates a \code{\link[bslib:sidebar]{bslib::sidebar()}} component containing the chat +interface, suitable for use with \code{\link[bslib:page_sidebar]{bslib::page_sidebar()}} or similar layouts. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{QueryChat$sidebar(width = 400, height = "100\%", ...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{width}}{Width of the sidebar in pixels. Default is 400.} + +\item{\code{height}}{Height of the sidebar. Default is "100\%".} + +\item{\code{...}}{Additional arguments passed to \code{\link[bslib:sidebar]{bslib::sidebar()}}.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A \code{\link[bslib:sidebar]{bslib::sidebar()}} UI component. +} +\subsection{Examples}{ +\if{html}{\out{
}} +\preformatted{\dontrun{ +qc <- QueryChat$new(mtcars, "mtcars") + +ui <- page_sidebar( + qc$sidebar(), + # Main content here +) +} +} +\if{html}{\out{
}} + +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-QueryChat-ui}{}}} +\subsection{Method \code{ui()}}{ +Create the UI for the querychat chat interface. + +This method generates the chat UI component. Typically you'll use +\verb{$sidebar()} instead, which wraps this in a sidebar layout. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{QueryChat$ui(...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{...}}{Additional arguments passed to \code{\link[shinychat:chat_ui]{shinychat::chat_ui()}}.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +A UI component containing the chat interface. +} +\subsection{Examples}{ +\if{html}{\out{
}} +\preformatted{\dontrun{ +qc <- QueryChat$new(mtcars, "mtcars") + +ui <- fluidPage( + qc$ui() +) +} +} +\if{html}{\out{
}} + +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-QueryChat-server}{}}} +\subsection{Method \code{server()}}{ +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 populates the internal +server values that are accessed via \verb{$df()}, \verb{$sql()}, and \verb{$title()}. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{QueryChat$server(session = shiny::getDefaultReactiveDomain())}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{session}}{The Shiny session object.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Invisibly returns \code{NULL}. Access reactive values via \verb{$df()}, +\verb{$sql()}, and \verb{$title()} methods. +} +\subsection{Examples}{ +\if{html}{\out{
}} +\preformatted{\dontrun{ +qc <- QueryChat$new(mtcars, "mtcars") + +server <- function(input, output, session) { + qc$server() + + output$data <- renderDataTable(qc$df()) + output$query <- renderText(qc$sql()) +} +} +} +\if{html}{\out{
}} + +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-QueryChat-df}{}}} +\subsection{Method \code{df()}}{ +Get the current filtered data frame. + +This is a reactive expression that returns the data after applying the +current SQL query. If no query has been set, returns the unfiltered data. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{QueryChat$df()}\if{html}{\out{
}} +} + +\subsection{Returns}{ +A data.frame with the filtered/transformed data. +} +\subsection{Examples}{ +\if{html}{\out{
}} +\preformatted{\dontrun{ +qc <- QueryChat$new(mtcars, "mtcars") + +server <- function(input, output, session) { + qc$server() + + output$table <- renderDataTable({ + qc$df() # Reactive - will update when query changes + }) +} +} +} +\if{html}{\out{
}} + +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-QueryChat-sql}{}}} +\subsection{Method \code{sql()}}{ +Get or set the current SQL query. + +This method provides both getter and setter functionality for the SQL +query. When called without arguments, it returns the current query. +When called with a query string, it sets the query and returns whether +the query changed. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{QueryChat$sql(query = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{query}}{Optional SQL query string. If provided, sets the current +query to this value. If \code{NULL} (default), returns the current query.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +\itemize{ +\item When \code{query = NULL} (getter): Returns the current SQL query as a string +(may be an empty string \code{""} if no query has been set). +\item When \code{query} is provided (setter): Returns \code{TRUE} if the query was +changed to a new value, \code{FALSE} if it was the same as the current value. +} +} +\subsection{Examples}{ +\if{html}{\out{
}} +\preformatted{\dontrun{ +qc <- QueryChat$new(mtcars, "mtcars") + +server <- function(input, output, session) { + qc$server() + + # Get current query + output$current_query <- renderText({ + qc$sql() + }) + + # Set query programmatically + observeEvent(input$filter_button, { + qc$sql("SELECT * FROM mtcars WHERE cyl = 6") + }) +} +} +} +\if{html}{\out{
}} + +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-QueryChat-title}{}}} +\subsection{Method \code{title()}}{ +Get or set the current title. + +The title is a short description of the current query that the LLM +provides whenever it generates a new SQL query. It can be used as a +status string for the data dashboard. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{QueryChat$title(value = NULL)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{value}}{Optional title string. If provided, sets the current title +to this value. If \code{NULL} (default), returns the current title.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +\itemize{ +\item When \code{value = NULL} (getter): Returns the current title as a string, +or \code{NULL} if no title has been set (because no SQL query has been set). +\item When \code{value} is provided (setter): Returns \code{TRUE} if the title was +changed to a new value, \code{FALSE} if it was the same as the current value. +} +} +\subsection{Examples}{ +\if{html}{\out{
}} +\preformatted{\dontrun{ +qc <- QueryChat$new(mtcars, "mtcars") + +server <- function(input, output, session) { + qc$server() + + # Get current title + output$title <- renderText({ + qc$title() \%||\% "No Query" + }) + + # Set title programmatically + observeEvent(input$set_title, { + qc$title("Filtered Cars") + }) +} +} +} +\if{html}{\out{
}} + +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-QueryChat-generate_greeting}{}}} +\subsection{Method \code{generate_greeting()}}{ +Generate a welcome greeting for the chat. + +By default, \code{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. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{QueryChat$generate_greeting(echo = c("none", "output"))}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{echo}}{Whether to print the greeting to the console. Options are +\code{"none"} (default, no output) or \code{"text"} (print to console).} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +The greeting string in Markdown format. +} +\subsection{Examples}{ +\if{html}{\out{
}} +\preformatted{\dontrun{ +# Create QueryChat object +qc <- QueryChat$new(mtcars, "mtcars") + +# Generate a greeting and save it +greeting <- qc$generate_greeting(echo = "text") +writeLines(greeting, "mtcars_greeting.md") + +# Later, use the saved greeting +qc2 <- QueryChat$new(mtcars, "mtcars", greeting = "mtcars_greeting.md") +} +} +\if{html}{\out{
}} + +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-QueryChat-cleanup}{}}} +\subsection{Method \code{cleanup()}}{ +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 \code{auto_cleanup} was set to \code{TRUE} in the constructor, +this will be called automatically when the Shiny app stops. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{QueryChat$cleanup()}\if{html}{\out{
}} +} + +\subsection{Returns}{ +Invisibly returns \code{NULL}. Resources are cleaned up internally. +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-QueryChat-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{QueryChat$clone(deep = FALSE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} +} diff --git a/pkg-r/man/cleanup_source.Rd b/pkg-r/man/cleanup_source.Rd index 25f3f31e..938f585b 100644 --- a/pkg-r/man/cleanup_source.Rd +++ b/pkg-r/man/cleanup_source.Rd @@ -15,5 +15,8 @@ cleanup_source(source, ...) NULL (invisibly) } \description{ -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 \verb{$cleanup()} method on the \link{QueryChat} object. } +\keyword{internal} diff --git a/pkg-r/man/create_data_source.Rd b/pkg-r/man/create_data_source.Rd new file mode 100644 index 00000000..07acddc2 --- /dev/null +++ b/pkg-r/man/create_data_source.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/data_source.R +\name{create_data_source} +\alias{create_data_source} +\title{Create a data source for querychat} +\usage{ +create_data_source(x, table_name = NULL, ...) +} +\arguments{ +\item{x}{A data frame or DBI connection} + +\item{table_name}{The name to use for the table in the data source. Can be: +\itemize{ +\item A character string (e.g., "table_name") +\item Or, for tables contained within catalogs or schemas, a \code{\link[DBI:Id]{DBI::Id()}} object (e.g., \code{DBI::Id(schema = "schema_name", table = "table_name")}) +}} +} +\value{ +A querychat_data_source object +} +\description{ +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 \code{QueryChat$new()}. +} +\keyword{internal} diff --git a/pkg-r/man/create_system_prompt.Rd b/pkg-r/man/create_system_prompt.Rd index 34269018..46e93c4b 100644 --- a/pkg-r/man/create_system_prompt.Rd +++ b/pkg-r/man/create_system_prompt.Rd @@ -8,6 +8,7 @@ create_system_prompt( source, data_description = NULL, extra_instructions = NULL, + categorical_threshold = 20, ... ) } @@ -18,11 +19,17 @@ create_system_prompt( \item{extra_instructions}{Optional additional instructions} +\item{categorical_threshold}{For text columns, the maximum number of unique +values to consider as a categorical variable} + \item{...}{Additional arguments passed to methods} } \value{ A string with the system prompt } \description{ -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 \verb{$set_system_prompt()} method on the \link{QueryChat} object. } +\keyword{internal} diff --git a/pkg-r/man/deprecated.Rd b/pkg-r/man/deprecated.Rd new file mode 100644 index 00000000..11f1186d --- /dev/null +++ b/pkg-r/man/deprecated.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/deprecated.R +\name{deprecated} +\alias{deprecated} +\alias{querychat_init} +\alias{querychat_app} +\alias{querychat_sidebar} +\alias{querychat_ui} +\alias{querychat_server} +\alias{querychat_greeting} +\alias{querychat_data_source} +\title{Deprecated functions} +\usage{ +querychat_init(...) + +querychat_app(...) + +querychat_sidebar(...) + +querychat_ui(...) + +querychat_server(...) + +querychat_greeting(...) + +querychat_data_source(...) +} +\description{ +These functions have been replaced by the new \code{QueryChat} R6 class API. +Please update your code to use the new class-based approach. +} +\keyword{internal} diff --git a/pkg-r/man/execute_query.Rd b/pkg-r/man/execute_query.Rd index 00bc34fb..f2fb3bf0 100644 --- a/pkg-r/man/execute_query.Rd +++ b/pkg-r/man/execute_query.Rd @@ -2,7 +2,7 @@ % Please edit documentation in R/data_source.R \name{execute_query} \alias{execute_query} -\title{Execute a SQL query on a data source} +\title{Execute an SQL query on a data source} \usage{ execute_query(source, query, ...) } @@ -10,12 +10,13 @@ execute_query(source, query, ...) \item{source}{A querychat_data_source object} \item{query}{SQL query string} - -\item{...}{Additional arguments passed to methods} } \value{ Result of the query as a data frame } \description{ -Execute a 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 \verb{$sql()} method on the \link{QueryChat} object to run queries. } +\keyword{internal} diff --git a/pkg-r/man/get_db_type.Rd b/pkg-r/man/get_db_type.Rd index e3fd6429..70d4e01b 100644 --- a/pkg-r/man/get_db_type.Rd +++ b/pkg-r/man/get_db_type.Rd @@ -15,5 +15,8 @@ get_db_type(source, ...) A character string containing the type information } \description{ -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 \verb{$set_system_prompt()} method on the \link{QueryChat} object. } +\keyword{internal} diff --git a/pkg-r/man/get_schema.Rd b/pkg-r/man/get_schema.Rd index 22d24ff1..2b4c9ded 100644 --- a/pkg-r/man/get_schema.Rd +++ b/pkg-r/man/get_schema.Rd @@ -4,16 +4,21 @@ \alias{get_schema} \title{Get schema for a data source} \usage{ -get_schema(source, ...) +get_schema(source, categorical_threshold = 20, ...) } \arguments{ \item{source}{A querychat_data_source object} +\item{categorical_threshold}{For text columns, the maximum number of unique values to consider as a categorical variable} + \item{...}{Additional arguments passed to methods} } \value{ A character string describing the schema } \description{ -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 \verb{$set_system_prompt()} method on the \link{QueryChat} object. } +\keyword{internal} diff --git a/pkg-r/man/querychat-package.Rd b/pkg-r/man/querychat-package.Rd index 1cae4261..2bf6f0cb 100644 --- a/pkg-r/man/querychat-package.Rd +++ b/pkg-r/man/querychat-package.Rd @@ -4,10 +4,69 @@ \name{querychat-package} \alias{querychat} \alias{querychat-package} -\title{querychat: Filter and Query Data Frames in 'shiny' Using an LLM Chat Interface} +\title{querychat: Chat with Your Data Using Natural Language} \description{ -Adds an LLM-powered chatbot to your 'shiny' app, that can turn your users' natural language questions into SQL queries that run against your data, and return the result as a reactive dataframe. Use it to drive reactive calculations, visualizations, downloads, etc. +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 \link{QueryChat} R6 class: + +\if{html}{\out{
}}\preformatted{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) +}\if{html}{\out{
}} +} + +\section{Key Features}{ + +\itemize{ +\item \strong{Natural language queries}: Ask questions in plain English +\item \strong{SQL transparency}: See the generated SQL queries +\item \strong{Multiple data sources}: Works with data frames and database connections +\item \strong{Customizable}: Add data descriptions, extra instructions, and custom greetings +\item \strong{LLM agnostic}: Works with OpenAI, Anthropic, Google, and other providers via ellmer +} +} + +\section{Main Components}{ + +\itemize{ +\item \link{QueryChat}: The main R6 class for creating chat interfaces +\item \code{\link[=create_data_source]{create_data_source()}}: (Advanced) Create custom data source objects +} +} + +\section{Examples}{ + +See the package examples directory for complete working apps: +\itemize{ +\item \verb{01-hello-app/}: Minimal example using \verb{$app()} +\item \verb{02-sidebar-app/}: Custom layout with additional UI elements +\item \verb{sqlite/}: Database connection example +} +} + \seealso{ Useful links: \itemize{ diff --git a/pkg-r/man/querychat_app.Rd b/pkg-r/man/querychat_app.Rd deleted file mode 100644 index 156c0157..00000000 --- a/pkg-r/man/querychat_app.Rd +++ /dev/null @@ -1,39 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/querychat_app.R -\name{querychat_app} -\alias{querychat_app} -\title{A Simple App for Chatting with Data} -\usage{ -querychat_app(config, ..., bookmark_store = "url") -} -\arguments{ -\item{config}{A \code{querychat_config} object or a data source that can be used -to create one.} - -\item{...}{Additional arguments passed to \code{\link[=querychat_init]{querychat_init()}} if \code{config} is -not already a \code{querychat_config} object.} - -\item{bookmark_store}{The bookmarking store to use for the app. Passed to -\code{enable_bookmarking} in \code{\link[shiny:shinyApp]{shiny::shinyApp()}}. Defaults to \code{"url"}, which -uses the URL to store the chat state. URL-based bookmarking is limited in -size; use \code{"server"} to store the state on the server side without size -limitations; or disable bookmarking by setting this to \code{"disable"}.} -} -\value{ -Invisibly returns the \code{chat} object containing the chat history. -} -\description{ -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 -\code{\link[=querychat_sidebar]{querychat_sidebar()}} and \code{\link[=querychat_server]{querychat_server()}} to provide a quick-and-easy -way to chat with your data. -} -\examples{ -\dontshow{if (rlang::is_interactive()) withAutoprint(\{ # examplesIf} -# 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()) -\dontshow{\}) # examplesIf} -} diff --git a/pkg-r/man/querychat_data_source.Rd b/pkg-r/man/querychat_data_source.Rd deleted file mode 100644 index 7d99ac5a..00000000 --- a/pkg-r/man/querychat_data_source.Rd +++ /dev/null @@ -1,34 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/data_source.R -\name{querychat_data_source} -\alias{querychat_data_source} -\alias{querychat_data_source.data.frame} -\alias{querychat_data_source.DBIConnection} -\title{Create a data source for querychat} -\usage{ -querychat_data_source(x, ...) - -\method{querychat_data_source}{data.frame}(x, table_name = NULL, categorical_threshold = 20, ...) - -\method{querychat_data_source}{DBIConnection}(x, table_name, categorical_threshold = 20, ...) -} -\arguments{ -\item{x}{A data frame or DBI connection} - -\item{...}{Additional arguments passed to specific methods} - -\item{table_name}{The name to use for the table in the data source. Can be: -\itemize{ -\item A character string (e.g., "table_name") -\item Or, for tables contained within catalogs or schemas, a \code{\link[DBI:Id]{DBI::Id()}} object (e.g., \code{DBI::Id(schema = "schema_name", table = "table_name")}) -}} - -\item{categorical_threshold}{For text columns, the maximum number of unique values to consider as a categorical variable} -} -\value{ -A querychat_data_source object -} -\description{ -Generic function to create a data source for querychat. This function -dispatches to appropriate methods based on input. -} diff --git a/pkg-r/man/querychat_greeting.Rd b/pkg-r/man/querychat_greeting.Rd deleted file mode 100644 index 4bb295a3..00000000 --- a/pkg-r/man/querychat_greeting.Rd +++ /dev/null @@ -1,55 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/querychat_greeting.R -\name{querychat_greeting} -\alias{querychat_greeting} -\title{Generate or retrieve a greeting message} -\usage{ -querychat_greeting(querychat_config, generate = TRUE, stream = FALSE) -} -\arguments{ -\item{querychat_config}{A \code{querychat_config} object from \code{\link[=querychat_init]{querychat_init()}}.} - -\item{generate}{If \code{TRUE} and if \code{querychat_config} does not include a -\code{greeting}, a new greeting is generated. If \code{FALSE}, returns the existing -greeting from the configuration (if any).} - -\item{stream}{If \code{TRUE}, calls \verb{$stream_async()} on the \link[ellmer:Chat]{ellmer::Chat} -client, suitable for streaming the greeting into a Shiny app with -\code{\link[shinychat:chat_append]{shinychat::chat_append()}}. If \code{FALSE} (default), calls \verb{$chat()} to get -the full greeting at once. Only relevant when \code{generate = TRUE}.} -} -\value{ -\itemize{ -\item When \code{generate = FALSE}: Returns the existing greeting as a string or -\code{NULL} if no greeting exists. -\item When \code{generate = TRUE}: Returns the chat response containing a greeting and -sample prompts. -} -} -\description{ -Use this function to generate a friendly greeting message using the chat -client and data source specified in the \code{querychat_config} object. You can -pass this greeting to \code{\link[=querychat_init]{querychat_init()}} to set an initial greeting for users -for faster startup times and lower costs. If you don't provide a greeting in -\code{\link[=querychat_init]{querychat_init()}}, one will be generated at the start of every new -conversation, using this function. -} -\examples{ -\dontshow{if (interactive()) withAutoprint(\{ # examplesIf} -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) -\dontshow{\}) # examplesIf} -} diff --git a/pkg-r/man/querychat_init.Rd b/pkg-r/man/querychat_init.Rd deleted file mode 100644 index 5d77c05c..00000000 --- a/pkg-r/man/querychat_init.Rd +++ /dev/null @@ -1,70 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/querychat.R -\name{querychat_init} -\alias{querychat_init} -\title{Call this once outside of any server function} -\usage{ -querychat_init( - data_source, - greeting = NULL, - data_description = NULL, - extra_instructions = NULL, - create_chat_func = deprecated(), - system_prompt = NULL, - auto_close_data_source = TRUE, - client = NULL -) -} -\arguments{ -\item{data_source}{A querychat_data_source object created by -\code{querychat_data_source()}. - -To create a data source: -\itemize{ -\item For data frame: \code{querychat_data_source(df, tbl_name = "my_table")} -\item For database: \code{querychat_data_source(conn, "table_name")} -}} - -\item{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 \code{\link[=querychat_greeting]{querychat_greeting()}} to generate a greeting.} - -\item{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.} - -\item{extra_instructions}{A string containing extra instructions for the -chat model.} - -\item{create_chat_func}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}}. Use the \code{client} -argument instead.} - -\item{system_prompt}{A string containing the system prompt for the chat -model. The default generates a generic prompt, which you can enhance via -the \code{data_description} and \code{extra_instructions} arguments.} - -\item{auto_close_data_source}{Should the data source connection be -automatically closed when the shiny app stops? Defaults to TRUE.} - -\item{client}{An \code{ellmer::Chat} object, a string to be passed to -\code{\link[ellmer:chat-any]{ellmer::chat()}} describing the model to use (e.g. \code{"openai/gpt-4o"}), or a -function that creates a chat client. When using a function, the function -should take \code{system_prompt} as an argument and return an \code{ellmer::Chat} -object. - -If \code{client} is not provided, querychat consults the \code{querychat.client} R -option, which can be any of the described options, or the -\code{QUERYCHAT_CLIENT} environment variable, which can be set to a a -provider-model string. If no option is provided, querychat defaults to -using \code{\link[ellmer:chat_openai]{ellmer::chat_openai()}}.} -} -\value{ -An object that can be passed to \code{querychat_server()} as the -\code{querychat_config} argument. By convention, this object should be named -\code{querychat_config}. -} -\description{ -This will perform one-time initialization that can then be shared by all -Shiny sessions in the R process. -} diff --git a/pkg-r/man/querychat_server.Rd b/pkg-r/man/querychat_server.Rd deleted file mode 100644 index 25bed75f..00000000 --- a/pkg-r/man/querychat_server.Rd +++ /dev/null @@ -1,27 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/querychat.R -\name{querychat_server} -\alias{querychat_server} -\title{Initialize the querychat server} -\usage{ -querychat_server(id, querychat_config) -} -\arguments{ -\item{id}{The ID of the module instance. Must match the ID passed to -the corresponding call to \code{querychat_ui()}.} - -\item{querychat_config}{An object created by \code{querychat_init()}.} -} -\value{ -A querychat instance, which is a named list with the following -elements: -\itemize{ -\item \code{sql}: A reactive that returns the current SQL query. -\item \code{title}: A reactive that returns the current title. -\item \code{df}: A reactive that returns the filtered data as a data.frame. -\item \code{chat}: The \link[ellmer:Chat]{ellmer::Chat} object that powers the chat interface. -} -} -\description{ -Initialize the querychat server -} diff --git a/pkg-r/man/querychat_ui.Rd b/pkg-r/man/querychat_ui.Rd deleted file mode 100644 index 664e3b6a..00000000 --- a/pkg-r/man/querychat_ui.Rd +++ /dev/null @@ -1,29 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/querychat.R -\name{querychat_ui} -\alias{querychat_ui} -\alias{querychat_sidebar} -\title{UI components for querychat} -\usage{ -querychat_sidebar(id, width = 400, height = "100\%", ...) - -querychat_ui(id) -} -\arguments{ -\item{id}{The ID of the module instance.} - -\item{width, height}{In \code{querychat_sidebar()}: the width and height of the -sidebar.} - -\item{...}{In \code{querychat_sidebar()}: additional arguments passed to -\code{\link[bslib:sidebar]{bslib::sidebar()}}.} -} -\value{ -A UI object that can be embedded in a Shiny app. -} -\description{ -These functions create UI components for the querychat interface. -\code{querychat_ui()} creates a basic chat interface, while \code{querychat_sidebar()} -wraps the chat interface in a \code{\link[bslib:sidebar]{bslib::sidebar()}} component designed to be -used as the \code{sidebar} argument to \code{\link[bslib:page_sidebar]{bslib::page_sidebar()}}. -} diff --git a/pkg-r/man/test_query.Rd b/pkg-r/man/test_query.Rd index ec3411de..d8307c6f 100644 --- a/pkg-r/man/test_query.Rd +++ b/pkg-r/man/test_query.Rd @@ -17,5 +17,8 @@ test_query(source, query, ...) Result of the query, limited to one row of data. } \description{ -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 \verb{$sql()} method on the \link{QueryChat} object to run queries. } +\keyword{internal} diff --git a/pkg-r/pkgdown/_pkgdown.yml b/pkg-r/pkgdown/_pkgdown.yml index f94529c9..7534c3eb 100644 --- a/pkg-r/pkgdown/_pkgdown.yml +++ b/pkg-r/pkgdown/_pkgdown.yml @@ -32,9 +32,4 @@ template: reference: - title: Chat interfaces contents: - - querychat_init - - querychat_server - - querychat_sidebar - - querychat_system_prompt - - querychat_ui - - querychat_app + - QueryChat diff --git a/pkg-r/tests/testthat/apps/basic/app.R b/pkg-r/tests/testthat/apps/basic/app.R index 7caf0253..fa449163 100644 --- a/pkg-r/tests/testthat/apps/basic/app.R +++ b/pkg-r/tests/testthat/apps/basic/app.R @@ -21,20 +21,20 @@ conn <- dbConnect(RSQLite::SQLite(), temp_db) dbWriteTable(conn, "iris", iris, overwrite = TRUE) dbDisconnect(conn) -# Setup database source +# Setup database source and QueryChat instance db_conn <- dbConnect(RSQLite::SQLite(), temp_db) -iris_source <- querychat_data_source(db_conn, "iris") -# Configure querychat with mock -querychat_config <- querychat_init( - data_source = iris_source, +# Create QueryChat instance +qc <- QueryChat$new( + data_source = db_conn, + table_name = "iris", greeting = "Welcome to the test app!", client = MockChat$new(ellmer::Provider("test", "test", "test")) ) ui <- page_sidebar( title = "Test Database App", - sidebar = querychat_sidebar("chat"), + sidebar = qc$sidebar(), h2("Data"), DT::DTOutput("data_table"), h3("SQL Query"), @@ -42,17 +42,17 @@ ui <- page_sidebar( ) server <- function(input, output, session) { - chat <- querychat_server("chat", querychat_config) + qc$server() output$data_table <- DT::renderDT( { - chat$df() + qc$df() }, options = list(pageLength = 5) ) output$sql_query <- renderText({ - query <- chat$sql() + query <- qc$sql() if (query == "") "No filter applied" else query }) diff --git a/pkg-r/tests/testthat/test-data-source.R b/pkg-r/tests/testthat/test-data-source.R index a4e324ce..183602c6 100644 --- a/pkg-r/tests/testthat/test-data-source.R +++ b/pkg-r/tests/testthat/test-data-source.R @@ -3,7 +3,7 @@ library(DBI) library(RSQLite) library(querychat) -test_that("querychat_data_source.data.frame creates proper S3 object", { +test_that("create_data_source.data.frame creates proper S3 object", { # Create a simple data frame test_df <- data.frame( id = 1:5, @@ -13,7 +13,7 @@ test_that("querychat_data_source.data.frame creates proper S3 object", { ) # Test with explicit table name - source <- querychat_data_source(test_df, table_name = "test_table") + source <- create_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(source)) expect_s3_class(source, "data_frame_source") @@ -22,7 +22,7 @@ test_that("querychat_data_source.data.frame creates proper S3 object", { expect_true(inherits(source$conn, "DBIConnection")) }) -test_that("querychat_data_source.DBIConnection creates proper S3 object", { +test_that("create_data_source.DBIConnection creates proper S3 object", { # Create temporary SQLite database temp_db <- withr::local_tempfile(fileext = ".db") conn <- dbConnect(RSQLite::SQLite(), temp_db) @@ -39,11 +39,10 @@ test_that("querychat_data_source.DBIConnection creates proper S3 object", { dbWriteTable(conn, "users", test_data, overwrite = TRUE) # Test DBI source creation - db_source <- querychat_data_source(conn, "users") + db_source <- create_data_source(conn, "users") expect_s3_class(db_source, "dbi_source") expect_s3_class(db_source, "querychat_data_source") expect_equal(db_source$table_name, "users") - expect_equal(db_source$categorical_threshold, 20) }) test_that("get_schema methods return proper schema", { @@ -55,7 +54,7 @@ test_that("get_schema methods return proper schema", { stringsAsFactors = FALSE ) - df_source <- querychat_data_source(test_df, table_name = "test_table") + df_source <- create_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) schema <- get_schema(df_source) @@ -76,7 +75,7 @@ test_that("get_schema methods return proper schema", { dbWriteTable(conn, "test_table", test_df, overwrite = TRUE) - dbi_source <- querychat_data_source(conn, "test_table") + dbi_source <- create_data_source(conn, "test_table") schema <- get_schema(dbi_source) expect_type(schema, "character") expect_match(schema, "Table: `test_table`") @@ -95,7 +94,7 @@ test_that("execute_query works for both source types", { stringsAsFactors = FALSE ) - df_source <- querychat_data_source(test_df, table_name = "test_table") + df_source <- create_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) result <- execute_query( df_source, @@ -110,7 +109,7 @@ test_that("execute_query works for both source types", { withr::defer(dbDisconnect(conn)) dbWriteTable(conn, "test_table", test_df, overwrite = TRUE) - dbi_source <- querychat_data_source(conn, "test_table") + dbi_source <- create_data_source(conn, "test_table") result <- execute_query( dbi_source, "SELECT * FROM test_table WHERE value > 25" @@ -127,7 +126,7 @@ test_that("execute_query works with empty/null queries", { stringsAsFactors = FALSE ) - df_source <- querychat_data_source(test_df, table_name = "test_table") + df_source <- create_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) # Test with NULL query @@ -149,7 +148,7 @@ test_that("execute_query works with empty/null queries", { dbWriteTable(conn, "test_table", test_df, overwrite = TRUE) - dbi_source <- querychat_data_source(conn, "test_table") + dbi_source <- create_data_source(conn, "test_table") # Test with NULL query result_null <- execute_query(dbi_source, NULL) @@ -174,7 +173,7 @@ test_that("get_schema correctly reports min/max values for numeric columns", { stringsAsFactors = FALSE ) - df_source <- querychat_data_source(test_df, table_name = "test_metrics") + df_source <- create_data_source(test_df, table_name = "test_metrics") withr::defer(cleanup_source(df_source)) schema <- get_schema(df_source) @@ -192,7 +191,7 @@ test_that("create_system_prompt generates appropriate system prompt", { stringsAsFactors = FALSE ) - df_source <- querychat_data_source(test_df, table_name = "test_table") + df_source <- create_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) prompt <- create_system_prompt( @@ -205,27 +204,26 @@ test_that("create_system_prompt generates appropriate system prompt", { expect_match(prompt, "Table: test_table") }) -test_that("querychat_init automatically handles data.frame inputs", { - # Test that querychat_init accepts data frames directly +test_that("QueryChat$new() automatically handles data.frame inputs", { + # Test that QueryChat$new() accepts data frames directly test_df <- data.frame(id = 1:3, name = c("A", "B", "C")) # Should work with data frame and auto-convert it - config <- querychat_init(data_source = test_df, greeting = "Test greeting") - withr::defer(cleanup_source(config$data_source)) + qc <- QueryChat$new(data_source = test_df, table_name = "test_df", greeting = "Test greeting") + withr::defer(qc$cleanup()) - expect_s3_class(config, "querychat_config") - expect_s3_class(config$data_source, "querychat_data_source") - expect_s3_class(config$data_source, "data_frame_source") + expect_s3_class(qc$data_source, "querychat_data_source") + expect_s3_class(qc$data_source, "data_frame_source") # Should work with proper data source too - df_source <- querychat_data_source(test_df, table_name = "test_table") + df_source <- create_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) - config <- querychat_init(data_source = df_source, greeting = "Test greeting") - expect_s3_class(config, "querychat_config") + qc2 <- QueryChat$new(data_source = df_source, table_name = "test_table", greeting = "Test greeting") + expect_s3_class(qc2$data_source, "querychat_data_source") }) -test_that("querychat_init works with both source types", { +test_that("QueryChat$new() works with both source types", { # Test with data frame test_df <- data.frame( id = 1:3, @@ -233,15 +231,14 @@ test_that("querychat_init works with both source types", { stringsAsFactors = FALSE ) - # Create data source and test with querychat_init - df_source <- querychat_data_source(test_df, table_name = "test_source") + # Create data source and test with QueryChat$new() + df_source <- create_data_source(test_df, table_name = "test_source") withr::defer(cleanup_source(df_source)) - config <- querychat_init(data_source = df_source, greeting = "Test greeting") + qc <- QueryChat$new(data_source = df_source, table_name = "test_source", greeting = "Test greeting") - expect_s3_class(config, "querychat_config") - expect_s3_class(config$data_source, "data_frame_source") - expect_equal(config$data_source$table_name, "test_source") + expect_s3_class(qc$data_source, "data_frame_source") + expect_equal(qc$data_source$table_name, "test_source") # Test with database connection temp_db <- withr::local_tempfile(fileext = ".db") @@ -250,9 +247,8 @@ test_that("querychat_init works with both source types", { dbWriteTable(conn, "test_table", test_df, overwrite = TRUE) - dbi_source <- querychat_data_source(conn, "test_table") - config <- querychat_init(data_source = dbi_source, greeting = "Test greeting") - expect_s3_class(config, "querychat_config") - expect_s3_class(config$data_source, "dbi_source") - expect_equal(config$data_source$table_name, "test_table") + dbi_source <- create_data_source(conn, "test_table") + qc2 <- QueryChat$new(data_source = dbi_source, table_name = "test_table", greeting = "Test greeting") + expect_s3_class(qc2$data_source, "dbi_source") + expect_equal(qc2$data_source$table_name, "test_table") }) diff --git a/pkg-r/tests/testthat/test-db-type.R b/pkg-r/tests/testthat/test-db-type.R index 72181823..217f7054 100644 --- a/pkg-r/tests/testthat/test-db-type.R +++ b/pkg-r/tests/testthat/test-db-type.R @@ -3,7 +3,7 @@ library(testthat) test_that("get_db_type returns correct type for data_frame_source", { # Create a simple data frame source df <- data.frame(x = 1:5, y = letters[1:5]) - df_source <- querychat_data_source(df, "test_table") + df_source <- create_data_source(df, "test_table") # Test that get_db_type returns "DuckDB" expect_equal(get_db_type(df_source), "DuckDB") @@ -17,7 +17,7 @@ test_that("get_db_type returns correct type for dbi_source with SQLite", { conn <- DBI::dbConnect(RSQLite::SQLite(), temp_db) withr::defer(DBI::dbDisconnect(conn)) DBI::dbWriteTable(conn, "test_table", data.frame(x = 1:5, y = letters[1:5])) - db_source <- querychat_data_source(conn, "test_table") + db_source <- create_data_source(conn, "test_table") # Test that get_db_type returns the correct database type expect_equal(get_db_type(db_source), "SQLite") @@ -26,7 +26,7 @@ test_that("get_db_type returns correct type for dbi_source with SQLite", { test_that("get_db_type is correctly used in create_system_prompt", { # Create a simple data frame source df <- data.frame(x = 1:5, y = letters[1:5]) - df_source <- querychat_data_source(df, "test_table") + df_source <- create_data_source(df, "test_table") # Generate system prompt sys_prompt <- create_system_prompt(df_source) @@ -38,7 +38,7 @@ test_that("get_db_type is correctly used in create_system_prompt", { test_that("get_db_type is used to customize prompt template", { # Create a simple data frame source df <- data.frame(x = 1:5, y = letters[1:5]) - df_source <- querychat_data_source(df, "test_table") + df_source <- create_data_source(df, "test_table") # Get the db_type db_type <- get_db_type(df_source) diff --git a/pkg-r/tests/testthat/test-querychat-greeting.R b/pkg-r/tests/testthat/test-querychat-greeting.R deleted file mode 100644 index f5bfa848..00000000 --- a/pkg-r/tests/testthat/test-querychat-greeting.R +++ /dev/null @@ -1,46 +0,0 @@ -test_that("querychat_greeting without an initial greeting", { - called <- NULL - - expect_warning( - config <- querychat_init( - mtcars, - client = ellmer::chat_openai(api_key = "boop") - ) - ) - - result <- querychat_greeting(config, generate = FALSE) - expect_null(result) -}) - -test_that("querychat_greeting handles empty greeting correctly", { - # Create a basic config - mock_client <- ellmer::chat_openai(api_key = "boop") - config <- querychat_init(mtcars, client = mock_client, greeting = "") - - # Test that empty greeting returns NULL when generate = FALSE - result <- querychat_greeting(config, generate = FALSE) - expect_null(result) -}) - -test_that("querychat_greeting returns existing greeting correctly", { - # Create a config with a greeting - mock_client <- ellmer::chat_openai(api_key = "boop") - test_greeting <- "Hello! This is a test greeting." - config <- querychat_init( - mtcars, - greeting = test_greeting, - client = mock_client - ) - - # Test that existing greeting is returned when generate = FALSE - result <- querychat_greeting(config, generate = FALSE) - expect_equal(result, test_greeting) -}) - -test_that("querychat_greeting validates input type", { - # Test that non-querychat_config input raises error - expect_error( - querychat_greeting("not a config"), - "`querychat_config` must be a `querychat_config` object." - ) -}) diff --git a/pkg-r/tests/testthat/test-querychat-server.R b/pkg-r/tests/testthat/test-querychat-server.R index 3e6421f8..6b30409f 100644 --- a/pkg-r/tests/testthat/test-querychat-server.R +++ b/pkg-r/tests/testthat/test-querychat-server.R @@ -20,7 +20,7 @@ test_that("database source query functionality", { dbWriteTable(conn, "users", test_data, overwrite = TRUE) # Create database source - db_source <- querychat_data_source(conn, "users") + db_source <- create_data_source(conn, "users") # Test that we can execute queries result <- execute_query(db_source, "SELECT * FROM users WHERE age > 30") diff --git a/pkg-r/tests/testthat/test-shiny-app.R b/pkg-r/tests/testthat/test-shiny-app.R index ff183c7e..e6c4c5c9 100644 --- a/pkg-r/tests/testthat/test-shiny-app.R +++ b/pkg-r/tests/testthat/test-shiny-app.R @@ -35,30 +35,31 @@ test_that("database reactive functionality works correctly", { db_conn <- dbConnect(RSQLite::SQLite(), temp_db) withr::defer(dbDisconnect(db_conn)) - iris_source <- querychat_data_source(db_conn, "iris") + iris_source <- create_data_source(db_conn, "iris") # Mock chat function mock_client <- ellmer::chat_openai(api_key = "boop") - # Test querychat_init with database source - config <- querychat_init( + # Test QueryChat$new() with database source + qc <- QueryChat$new( data_source = iris_source, + table_name = "iris", greeting = "Test greeting", client = mock_client ) - expect_s3_class(config$data_source, "dbi_source") - expect_s3_class(config$data_source, "querychat_data_source") + expect_s3_class(qc$data_source, "dbi_source") + expect_s3_class(qc$data_source, "querychat_data_source") # Test that we can get all data - result_data <- execute_query(config$data_source, NULL) + result_data <- execute_query(qc$data_source, NULL) expect_s3_class(result_data, "data.frame") expect_equal(nrow(result_data), 150) expect_equal(ncol(result_data), 5) # Test with a specific query query_result <- execute_query( - config$data_source, + qc$data_source, "SELECT \"Sepal.Length\", \"Sepal.Width\" FROM iris WHERE \"Species\" = 'setosa'" ) expect_s3_class(query_result, "data.frame") diff --git a/pkg-r/tests/testthat/test-sql-comments.R b/pkg-r/tests/testthat/test-sql-comments.R index 39a0415c..992a5160 100644 --- a/pkg-r/tests/testthat/test-sql-comments.R +++ b/pkg-r/tests/testthat/test-sql-comments.R @@ -12,7 +12,7 @@ test_that("execute_query handles SQL with inline comments", { ) # Create data source - df_source <- querychat_data_source(test_df, table_name = "test_table") + df_source <- create_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) # Test with inline comments @@ -51,7 +51,7 @@ test_that("execute_query handles SQL with multiline comments", { ) # Create data source - df_source <- querychat_data_source(test_df, table_name = "test_table") + df_source <- create_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) # Test with multiline comments @@ -95,7 +95,7 @@ test_that("execute_query handles SQL with trailing semicolons", { ) # Create data source - df_source <- querychat_data_source(test_df, table_name = "test_table") + df_source <- create_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) # Test with trailing semicolon @@ -132,7 +132,7 @@ test_that("execute_query handles SQL with mixed comments and semicolons", { ) # Create data source - df_source <- querychat_data_source(test_df, table_name = "test_table") + df_source <- create_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) # Test with a mix of comment styles and semicolons @@ -180,7 +180,7 @@ test_that("execute_query handles SQL with unusual whitespace patterns", { ) # Create data source - df_source <- querychat_data_source(test_df, table_name = "test_table") + df_source <- create_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) # Test with unusual whitespace patterns (which LLMs might generate) diff --git a/pkg-r/tests/testthat/test-test-query.R b/pkg-r/tests/testthat/test-test-query.R index 0df278ab..4c15b70d 100644 --- a/pkg-r/tests/testthat/test-test-query.R +++ b/pkg-r/tests/testthat/test-test-query.R @@ -18,7 +18,7 @@ test_that("test_query.dbi_source correctly retrieves one row of data", { dbWriteTable(conn, "test_table", test_df, overwrite = TRUE) - dbi_source <- querychat_data_source(conn, "test_table") + dbi_source <- create_data_source(conn, "test_table") withr::defer(cleanup_source(dbi_source)) # Test basic query - should only return one row @@ -62,7 +62,7 @@ test_that("test_query.dbi_source handles errors correctly", { ) dbWriteTable(conn, "test_table", test_df, overwrite = TRUE) - dbi_source <- querychat_data_source(conn, "test_table") + dbi_source <- create_data_source(conn, "test_table") withr::defer(cleanup_source(dbi_source), priority = "last") # Test with invalid SQL @@ -96,7 +96,7 @@ test_that("test_query.dbi_source works with different data types", { dbWriteTable(conn, "types_table", test_df, overwrite = TRUE) - dbi_source <- querychat_data_source(conn, "types_table") + dbi_source <- create_data_source(conn, "types_table") withr::defer(cleanup_source(dbi_source), priority = "last") # Test query with different column types From ea86b33def6ccd9a19e82b26bdeac2977d078135 Mon Sep 17 00:00:00 2001 From: cpsievert Date: Mon, 24 Nov 2025 22:41:27 +0000 Subject: [PATCH 02/25] `air format` (GitHub Actions) --- pkg-r/R/QueryChat-class.R | 2 +- pkg-r/R/data_source.R | 12 +++++----- pkg-r/R/querychat_module.R | 3 +-- .../inst/examples-shiny/02-sidebar-app/app.R | 8 +++++-- pkg-r/tests/testthat/test-data-source.R | 24 +++++++++++++++---- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/pkg-r/R/QueryChat-class.R b/pkg-r/R/QueryChat-class.R index edde7198..caf94561 100644 --- a/pkg-r/R/QueryChat-class.R +++ b/pkg-r/R/QueryChat-class.R @@ -655,4 +655,4 @@ normalize_data_source <- function(data_source, table_name) { } else { create_data_source(data_source, table_name) } -} \ No newline at end of file +} diff --git a/pkg-r/R/data_source.R b/pkg-r/R/data_source.R index 6c77d637..cb00ae84 100644 --- a/pkg-r/R/data_source.R +++ b/pkg-r/R/data_source.R @@ -3,7 +3,7 @@ #' 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") @@ -81,7 +81,7 @@ is_data_source <- function(x) { } #' 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. @@ -109,7 +109,7 @@ 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. @@ -134,7 +134,7 @@ 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. @@ -181,7 +181,7 @@ 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. @@ -268,7 +268,7 @@ 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. diff --git a/pkg-r/R/querychat_module.R b/pkg-r/R/querychat_module.R index 19f2bbf7..07e872bf 100644 --- a/pkg-r/R/querychat_module.R +++ b/pkg-r/R/querychat_module.R @@ -22,7 +22,6 @@ mod_ui <- function(id) { # 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", { @@ -102,4 +101,4 @@ mod_server <- function(id, data_source, greeting, client) { df = filtered_df ) }) -} \ No newline at end of file +} diff --git a/pkg-r/inst/examples-shiny/02-sidebar-app/app.R b/pkg-r/inst/examples-shiny/02-sidebar-app/app.R index ef4ad120..fddaa947 100644 --- a/pkg-r/inst/examples-shiny/02-sidebar-app/app.R +++ b/pkg-r/inst/examples-shiny/02-sidebar-app/app.R @@ -32,7 +32,9 @@ ui <- page_sidebar( sidebar = qc$sidebar(), h2("Current Data View"), - p("The table below shows the current filtered data based on your chat queries:"), + p( + "The table below shows the current filtered data based on your chat queries:" + ), DT::DTOutput("data_table", fill = FALSE), h2("Current SQL Query"), @@ -42,7 +44,9 @@ ui <- page_sidebar( p("This dataset contains:"), tags$ul( tags$li("344 observations of penguins"), - tags$li("Columns: species, island, bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g, sex, year") + tags$li( + "Columns: species, island, bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g, sex, year" + ) ) ) diff --git a/pkg-r/tests/testthat/test-data-source.R b/pkg-r/tests/testthat/test-data-source.R index 183602c6..7d62bc3e 100644 --- a/pkg-r/tests/testthat/test-data-source.R +++ b/pkg-r/tests/testthat/test-data-source.R @@ -209,7 +209,11 @@ test_that("QueryChat$new() automatically handles data.frame inputs", { test_df <- data.frame(id = 1:3, name = c("A", "B", "C")) # Should work with data frame and auto-convert it - qc <- QueryChat$new(data_source = test_df, table_name = "test_df", greeting = "Test greeting") + qc <- QueryChat$new( + data_source = test_df, + table_name = "test_df", + greeting = "Test greeting" + ) withr::defer(qc$cleanup()) expect_s3_class(qc$data_source, "querychat_data_source") @@ -219,7 +223,11 @@ test_that("QueryChat$new() automatically handles data.frame inputs", { df_source <- create_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) - qc2 <- QueryChat$new(data_source = df_source, table_name = "test_table", greeting = "Test greeting") + qc2 <- QueryChat$new( + data_source = df_source, + table_name = "test_table", + greeting = "Test greeting" + ) expect_s3_class(qc2$data_source, "querychat_data_source") }) @@ -235,7 +243,11 @@ test_that("QueryChat$new() works with both source types", { df_source <- create_data_source(test_df, table_name = "test_source") withr::defer(cleanup_source(df_source)) - qc <- QueryChat$new(data_source = df_source, table_name = "test_source", greeting = "Test greeting") + qc <- QueryChat$new( + data_source = df_source, + table_name = "test_source", + greeting = "Test greeting" + ) expect_s3_class(qc$data_source, "data_frame_source") expect_equal(qc$data_source$table_name, "test_source") @@ -248,7 +260,11 @@ test_that("QueryChat$new() works with both source types", { dbWriteTable(conn, "test_table", test_df, overwrite = TRUE) dbi_source <- create_data_source(conn, "test_table") - qc2 <- QueryChat$new(data_source = dbi_source, table_name = "test_table", greeting = "Test greeting") + qc2 <- QueryChat$new( + data_source = dbi_source, + table_name = "test_table", + greeting = "Test greeting" + ) expect_s3_class(qc2$data_source, "dbi_source") expect_equal(qc2$data_source$table_name, "test_table") }) From e58c87969bc92efa71403c24aeed4dfbdc84812d Mon Sep 17 00:00:00 2001 From: cpsievert Date: Mon, 24 Nov 2025 22:41:28 +0000 Subject: [PATCH 03/25] `usethis::use_tidy_description()` (GitHub Actions) --- pkg-r/DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg-r/DESCRIPTION b/pkg-r/DESCRIPTION index 29339371..e8bd4104 100644 --- a/pkg-r/DESCRIPTION +++ b/pkg-r/DESCRIPTION @@ -23,10 +23,10 @@ Imports: lifecycle, promises, purrr, + R6, rlang, shiny, shinychat (>= 0.2.0.9000), - R6, utils, whisker Suggests: From 55f17ee371141028638f4bc2b1d16bdf9b525f0b Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 24 Nov 2025 16:49:05 -0600 Subject: [PATCH 04/25] Address feedback --- pkg-r/R/QueryChat-class.R | 2 -- pkg-r/R/data_source.R | 1 + pkg-r/man/QueryChat.Rd | 4 ---- pkg-r/man/execute_query.Rd | 2 ++ 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pkg-r/R/QueryChat-class.R b/pkg-r/R/QueryChat-class.R index caf94561..14521275 100644 --- a/pkg-r/R/QueryChat-class.R +++ b/pkg-r/R/QueryChat-class.R @@ -61,8 +61,6 @@ #' \item{`$cleanup()`}{Clean up data source resources.} #' } #' -#' @field data_source The normalized data source object (read-only). -#' @field client The LLM chat client (read-only, session-specific). #' @field greeting The greeting message displayed to users. #' @field id The module ID for namespacing. #' diff --git a/pkg-r/R/data_source.R b/pkg-r/R/data_source.R index cb00ae84..80fa4a67 100644 --- a/pkg-r/R/data_source.R +++ b/pkg-r/R/data_source.R @@ -88,6 +88,7 @@ is_data_source <- function(x) { #' #' @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 diff --git a/pkg-r/man/QueryChat.Rd b/pkg-r/man/QueryChat.Rd index 158ef6dd..f6e53b1a 100644 --- a/pkg-r/man/QueryChat.Rd +++ b/pkg-r/man/QueryChat.Rd @@ -258,10 +258,6 @@ qc2 <- QueryChat$new(mtcars, "mtcars", greeting = "mtcars_greeting.md") \section{Active bindings}{ \if{html}{\out{
}} \describe{ -\item{\code{data_source}}{The normalized data source object (read-only).} - -\item{\code{client}}{The LLM chat client (read-only, session-specific).} - \item{\code{client}}{Get the (session-specific) chat client.} \item{\code{data_source}}{Get the current data source.} diff --git a/pkg-r/man/execute_query.Rd b/pkg-r/man/execute_query.Rd index f2fb3bf0..ab8fd054 100644 --- a/pkg-r/man/execute_query.Rd +++ b/pkg-r/man/execute_query.Rd @@ -10,6 +10,8 @@ execute_query(source, query, ...) \item{source}{A querychat_data_source object} \item{query}{SQL query string} + +\item{...}{Additional arguments passed to methods} } \value{ Result of the query as a data frame From 32de0d4c8eae043409be6ba26652843b65dd71a9 Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 24 Nov 2025 17:14:34 -0600 Subject: [PATCH 05/25] Get check passing --- pkg-r/NAMESPACE | 1 + pkg-r/R/querychat-package.R | 1 + 2 files changed, 2 insertions(+) diff --git a/pkg-r/NAMESPACE b/pkg-r/NAMESPACE index 92ecc69a..bea335a3 100644 --- a/pkg-r/NAMESPACE +++ b/pkg-r/NAMESPACE @@ -28,3 +28,4 @@ export(test_query) importFrom(R6,R6Class) importFrom(bslib,sidebar) importFrom(lifecycle,deprecated) +importFrom(rlang,"%||%") diff --git a/pkg-r/R/querychat-package.R b/pkg-r/R/querychat-package.R index 7e809b2a..f5132ace 100644 --- a/pkg-r/R/querychat-package.R +++ b/pkg-r/R/querychat-package.R @@ -56,5 +56,6 @@ #' @importFrom lifecycle deprecated #' @importFrom R6 R6Class #' @importFrom bslib sidebar +#' @importFrom rlang %||% ## usethis namespace: end NULL From ede59ed215f77109dfeb1a6de5bbf6e10a6aee38 Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 24 Nov 2025 17:18:51 -0600 Subject: [PATCH 06/25] Add some DT/card polish to () and examples --- pkg-r/R/QueryChat-class.R | 7 +++- .../inst/examples-shiny/02-sidebar-app/app.R | 38 ++++++++++++------- pkg-r/inst/examples-shiny/sqlite/app.R | 36 +++++++++++------- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/pkg-r/R/QueryChat-class.R b/pkg-r/R/QueryChat-class.R index 14521275..53f8224e 100644 --- a/pkg-r/R/QueryChat-class.R +++ b/pkg-r/R/QueryChat-class.R @@ -281,6 +281,7 @@ QueryChat <- R6::R6Class( shiny::uiOutput("sql_output") ), bslib::card( + full_screen = TRUE, bslib::card_header(bsicons::bs_icon("table"), "Data"), DT::DTOutput("dt") ), @@ -323,7 +324,11 @@ QueryChat <- R6::R6Class( }) output$dt <- DT::renderDT({ - DT::datatable(self$df()) + DT::datatable( + self$df(), + fillContainer = TRUE, + options = list(pageLength = 25, scrollX = TRUE) + ) }) output$sql_output <- shiny::renderUI({ diff --git a/pkg-r/inst/examples-shiny/02-sidebar-app/app.R b/pkg-r/inst/examples-shiny/02-sidebar-app/app.R index fddaa947..1b0b9204 100644 --- a/pkg-r/inst/examples-shiny/02-sidebar-app/app.R +++ b/pkg-r/inst/examples-shiny/02-sidebar-app/app.R @@ -31,21 +31,30 @@ ui <- page_sidebar( title = "Palmer Penguins Chat Explorer", 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 dataset contains:"), - tags$ul( - tags$li("344 observations of penguins"), - 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/)" + ) ) ) ) @@ -60,7 +69,8 @@ server <- function(input, output, session) { { qc$df() }, - options = list(pageLength = 10, scrollX = TRUE) + fillContainer = TRUE, + options = list(pageLength = 25, scrollX = TRUE) ) # Render the SQL query diff --git a/pkg-r/inst/examples-shiny/sqlite/app.R b/pkg-r/inst/examples-shiny/sqlite/app.R index 3c0340c7..c1d5bcee 100644 --- a/pkg-r/inst/examples-shiny/sqlite/app.R +++ b/pkg-r/inst/examples-shiny/sqlite/app.R @@ -46,21 +46,30 @@ ui <- page_sidebar( title = "Database Query 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/)" + ) ) ) ) @@ -72,6 +81,7 @@ server <- function(input, output, session) { { qc$df() }, + fillContainer = TRUE, options = list(pageLength = 10, scrollX = TRUE) ) From 3a14a487a57c90fb0d11e90336293c80b1400245 Mon Sep 17 00:00:00 2001 From: cpsievert Date: Mon, 24 Nov 2025 23:24:47 +0000 Subject: [PATCH 07/25] `air format` (GitHub Actions) --- pkg-r/R/QueryChat-class.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg-r/R/QueryChat-class.R b/pkg-r/R/QueryChat-class.R index 53f8224e..a4395739 100644 --- a/pkg-r/R/QueryChat-class.R +++ b/pkg-r/R/QueryChat-class.R @@ -325,7 +325,7 @@ QueryChat <- R6::R6Class( output$dt <- DT::renderDT({ DT::datatable( - self$df(), + self$df(), fillContainer = TRUE, options = list(pageLength = 25, scrollX = TRUE) ) From d1a15c17108908823284adf0fae61e21b527884f Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Tue, 25 Nov 2025 14:14:17 -0600 Subject: [PATCH 08/25] Apply suggestions from code review Co-authored-by: Garrick Aden-Buie --- pkg-r/NEWS.md | 4 +-- pkg-r/README.md | 2 +- pkg-r/inst/examples-shiny/01-hello-app/app.R | 5 +--- .../inst/examples-shiny/02-sidebar-app/app.R | 25 +++++++++++++------ 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md index ea64625f..aae5bfca 100644 --- a/pkg-r/NEWS.md +++ b/pkg-r/NEWS.md @@ -1,7 +1,7 @@ # 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). (#xxx) - * In addition, `querychat_data_source()` was renamed to `create_data_source()`, and remains exported for a developer extension point, but users no longer have to explicitly create a data source. (#xxx) +* 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 `create_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) diff --git a/pkg-r/README.md b/pkg-r/README.md index 7354e69e..8ed52cb8 100644 --- a/pkg-r/README.md +++ b/pkg-r/README.md @@ -115,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 creating your `QueryChat` instance, pass `greeting = "greeting.md"` (or use `readLines()` to read the file as a string). +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. diff --git a/pkg-r/inst/examples-shiny/01-hello-app/app.R b/pkg-r/inst/examples-shiny/01-hello-app/app.R index af2b3ac0..a8d63a10 100644 --- a/pkg-r/inst/examples-shiny/01-hello-app/app.R +++ b/pkg-r/inst/examples-shiny/01-hello-app/app.R @@ -3,13 +3,10 @@ library(palmerpenguins) # Create a QueryChat object and generate a complete app with $app() qc <- QueryChat$new(penguins, "penguins") -app <- qc$app() +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 - -# Run with: shiny::runApp() -app diff --git a/pkg-r/inst/examples-shiny/02-sidebar-app/app.R b/pkg-r/inst/examples-shiny/02-sidebar-app/app.R index 1b0b9204..a0bdfbfb 100644 --- a/pkg-r/inst/examples-shiny/02-sidebar-app/app.R +++ b/pkg-r/inst/examples-shiny/02-sidebar-app/app.R @@ -4,28 +4,37 @@ library(querychat) library(palmerpenguins) # Define a custom greeting for the penguins app -greeting <- " +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 -" +- 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 = "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 = "When showing results, always explain what the data represents and highlight any interesting patterns you observe." + 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", From fa1c28345ece02fcbf452be0852309b1d6ef9297 Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 25 Nov 2025 14:17:17 -0600 Subject: [PATCH 09/25] chore: rename file --- pkg-r/R/{QueryChat-class.R => QueryChat.R} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pkg-r/R/{QueryChat-class.R => QueryChat.R} (100%) diff --git a/pkg-r/R/QueryChat-class.R b/pkg-r/R/QueryChat.R similarity index 100% rename from pkg-r/R/QueryChat-class.R rename to pkg-r/R/QueryChat.R From ade1a77611a3c89b077552ee206e86f9a5b5bf4d Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Tue, 25 Nov 2025 14:20:10 -0600 Subject: [PATCH 10/25] Apply suggestions from code review Co-authored-by: Garrick Aden-Buie --- pkg-r/R/QueryChat.R | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index a4395739..910cb7a8 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -238,13 +238,10 @@ QueryChat <- R6::R6Class( #' library(querychat) #' #' qc <- QueryChat$new(mtcars, "mtcars") - #' app <- qc$app() + #' qc$app() #' - #' # Run the app - #' shiny::runApp(app) - #' - #' # Or return from a script - #' app + #' # Or explicitly run the app + #' shiny::runApp(qc$app()) #' } app = function(bookmark_store = "url") { rlang::check_installed("DT") @@ -254,10 +251,9 @@ QueryChat <- R6::R6Class( ui <- function(req) { bslib::page_sidebar( - title = shiny::HTML(paste0( - "querychat with ", - table_name, - "" + title = shiny::HTML(sprintf( + "querychat with %s", + table_name )), class = "bslib-page-dashboard", sidebar = self$sidebar(), From a13172d5fd93f561d060e8ae7af036742548a55c Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 25 Nov 2025 14:23:39 -0600 Subject: [PATCH 11/25] chore: take renaming suggestions --- pkg-py/src/querychat/_querychat.py | 4 +-- pkg-r/NAMESPACE | 6 ++-- pkg-r/NEWS.md | 2 +- pkg-r/R/QueryChat.R | 4 +-- pkg-r/R/data_source.R | 8 +++--- pkg-r/R/querychat-package.R | 2 +- pkg-r/R/utils-ellmer.R | 2 +- pkg-r/man/create_data_source.Rd | 6 ++-- pkg-r/man/querychat-package.Rd | 2 +- pkg-r/tests/testthat/test-data-source.R | 30 ++++++++++---------- pkg-r/tests/testthat/test-db-type.R | 8 +++--- pkg-r/tests/testthat/test-querychat-server.R | 2 +- pkg-r/tests/testthat/test-shiny-app.R | 2 +- pkg-r/tests/testthat/test-sql-comments.R | 10 +++---- pkg-r/tests/testthat/test-test-query.R | 6 ++-- 15 files changed, 47 insertions(+), 47 deletions(-) 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/NAMESPACE b/pkg-r/NAMESPACE index bea335a3..be0784cd 100644 --- a/pkg-r/NAMESPACE +++ b/pkg-r/NAMESPACE @@ -1,8 +1,8 @@ # Generated by roxygen2: do not edit by hand S3method(cleanup_source,dbi_source) -S3method(create_data_source,DBIConnection) -S3method(create_data_source,data.frame) +S3method(as_querychat_data_source,DBIConnection) +S3method(as_querychat_data_source,data.frame) S3method(create_system_prompt,querychat_data_source) S3method(execute_query,dbi_source) S3method(get_db_type,data_frame_source) @@ -12,7 +12,7 @@ S3method(get_schema,dbi_source) S3method(test_query,dbi_source) export(QueryChat) export(cleanup_source) -export(create_data_source) +export(as_querychat_data_source) export(create_system_prompt) export(execute_query) export(get_db_type) diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md index aae5bfca..5a91e2ef 100644 --- a/pkg-r/NEWS.md +++ b/pkg-r/NEWS.md @@ -1,7 +1,7 @@ # 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 `create_data_source()`, and remains exported for a developer extension point, but users no longer have to explicitly create a data source. (#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) diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index 910cb7a8..1919cd03 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -202,7 +202,7 @@ QueryChat <- R6::R6Class( ) # Fork and empty chat now so the per-session forks are fast - client <- normalize_client(client) + client <- as_querychat_client(client) private$.client <- client$clone() private$.client$set_turns(list()) private$.client$set_system_prompt(prompt) @@ -652,6 +652,6 @@ normalize_data_source <- function(data_source, table_name) { if (is_data_source(data_source)) { return(data_source) } else { - create_data_source(data_source, table_name) + 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 80fa4a67..73684c55 100644 --- a/pkg-r/R/data_source.R +++ b/pkg-r/R/data_source.R @@ -11,12 +11,12 @@ #' @return A querychat_data_source object #' @keywords internal #' @export -create_data_source <- function(x, table_name = NULL, ...) { - UseMethod("create_data_source") +as_querychat_data_source <- function(x, table_name = NULL, ...) { + UseMethod("as_querychat_data_source") } #' @export -create_data_source.data.frame <- function(x, table_name = NULL, ...) { +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,7 +47,7 @@ create_data_source.data.frame <- function(x, table_name = NULL, ...) { } #' @export -create_data_source.DBIConnection <- function(x, table_name, ...) { +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 diff --git a/pkg-r/R/querychat-package.R b/pkg-r/R/querychat-package.R index f5132ace..2e0c613f 100644 --- a/pkg-r/R/querychat-package.R +++ b/pkg-r/R/querychat-package.R @@ -41,7 +41,7 @@ #' #' @section Main Components: #' - [QueryChat]: The main R6 class for creating chat interfaces -#' - [create_data_source()]: (Advanced) Create custom data source objects +#' - [as_querychat_data_source()]: (Advanced) Create custom data source objects #' #' @section Examples: #' See the package examples directory for complete working apps: diff --git a/pkg-r/R/utils-ellmer.R b/pkg-r/R/utils-ellmer.R index ac0299e3..51002f8d 100644 --- a/pkg-r/R/utils-ellmer.R +++ b/pkg-r/R/utils-ellmer.R @@ -15,7 +15,7 @@ interpolate_package <- function(path, ..., .envir = parent.frame()) { } -normalize_client <- function(client = NULL) { +as_querychat_client <- function(client = NULL) { if (is.null(client)) { client <- querychat_client_option() } diff --git a/pkg-r/man/create_data_source.Rd b/pkg-r/man/create_data_source.Rd index 07acddc2..aca764ac 100644 --- a/pkg-r/man/create_data_source.Rd +++ b/pkg-r/man/create_data_source.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/data_source.R -\name{create_data_source} -\alias{create_data_source} +\name{as_querychat_data_source} +\alias{as_querychat_data_source} \title{Create a data source for querychat} \usage{ -create_data_source(x, table_name = NULL, ...) +as_querychat_data_source(x, table_name = NULL, ...) } \arguments{ \item{x}{A data frame or DBI connection} diff --git a/pkg-r/man/querychat-package.Rd b/pkg-r/man/querychat-package.Rd index 2bf6f0cb..c7c4e817 100644 --- a/pkg-r/man/querychat-package.Rd +++ b/pkg-r/man/querychat-package.Rd @@ -53,7 +53,7 @@ shinyApp(ui, server) \itemize{ \item \link{QueryChat}: The main R6 class for creating chat interfaces -\item \code{\link[=create_data_source]{create_data_source()}}: (Advanced) Create custom data source objects +\item \code{\link[=as_querychat_data_source]{as_querychat_data_source()}}: (Advanced) Create custom data source objects } } diff --git a/pkg-r/tests/testthat/test-data-source.R b/pkg-r/tests/testthat/test-data-source.R index 7d62bc3e..343f336a 100644 --- a/pkg-r/tests/testthat/test-data-source.R +++ b/pkg-r/tests/testthat/test-data-source.R @@ -3,7 +3,7 @@ library(DBI) library(RSQLite) library(querychat) -test_that("create_data_source.data.frame creates proper S3 object", { +test_that("as_querychat_data_source.data.frame creates proper S3 object", { # Create a simple data frame test_df <- data.frame( id = 1:5, @@ -13,7 +13,7 @@ test_that("create_data_source.data.frame creates proper S3 object", { ) # Test with explicit table name - source <- create_data_source(test_df, table_name = "test_table") + source <- as_querychat_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(source)) expect_s3_class(source, "data_frame_source") @@ -22,7 +22,7 @@ test_that("create_data_source.data.frame creates proper S3 object", { expect_true(inherits(source$conn, "DBIConnection")) }) -test_that("create_data_source.DBIConnection creates proper S3 object", { +test_that("as_querychat_data_source.DBIConnection creates proper S3 object", { # Create temporary SQLite database temp_db <- withr::local_tempfile(fileext = ".db") conn <- dbConnect(RSQLite::SQLite(), temp_db) @@ -39,7 +39,7 @@ test_that("create_data_source.DBIConnection creates proper S3 object", { dbWriteTable(conn, "users", test_data, overwrite = TRUE) # Test DBI source creation - db_source <- create_data_source(conn, "users") + db_source <- as_querychat_data_source(conn, "users") expect_s3_class(db_source, "dbi_source") expect_s3_class(db_source, "querychat_data_source") expect_equal(db_source$table_name, "users") @@ -54,7 +54,7 @@ test_that("get_schema methods return proper schema", { stringsAsFactors = FALSE ) - df_source <- create_data_source(test_df, table_name = "test_table") + df_source <- as_querychat_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) schema <- get_schema(df_source) @@ -75,7 +75,7 @@ test_that("get_schema methods return proper schema", { dbWriteTable(conn, "test_table", test_df, overwrite = TRUE) - dbi_source <- create_data_source(conn, "test_table") + dbi_source <- as_querychat_data_source(conn, "test_table") schema <- get_schema(dbi_source) expect_type(schema, "character") expect_match(schema, "Table: `test_table`") @@ -94,7 +94,7 @@ test_that("execute_query works for both source types", { stringsAsFactors = FALSE ) - df_source <- create_data_source(test_df, table_name = "test_table") + df_source <- as_querychat_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) result <- execute_query( df_source, @@ -109,7 +109,7 @@ test_that("execute_query works for both source types", { withr::defer(dbDisconnect(conn)) dbWriteTable(conn, "test_table", test_df, overwrite = TRUE) - dbi_source <- create_data_source(conn, "test_table") + dbi_source <- as_querychat_data_source(conn, "test_table") result <- execute_query( dbi_source, "SELECT * FROM test_table WHERE value > 25" @@ -126,7 +126,7 @@ test_that("execute_query works with empty/null queries", { stringsAsFactors = FALSE ) - df_source <- create_data_source(test_df, table_name = "test_table") + df_source <- as_querychat_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) # Test with NULL query @@ -148,7 +148,7 @@ test_that("execute_query works with empty/null queries", { dbWriteTable(conn, "test_table", test_df, overwrite = TRUE) - dbi_source <- create_data_source(conn, "test_table") + dbi_source <- as_querychat_data_source(conn, "test_table") # Test with NULL query result_null <- execute_query(dbi_source, NULL) @@ -173,7 +173,7 @@ test_that("get_schema correctly reports min/max values for numeric columns", { stringsAsFactors = FALSE ) - df_source <- create_data_source(test_df, table_name = "test_metrics") + df_source <- as_querychat_data_source(test_df, table_name = "test_metrics") withr::defer(cleanup_source(df_source)) schema <- get_schema(df_source) @@ -191,7 +191,7 @@ test_that("create_system_prompt generates appropriate system prompt", { stringsAsFactors = FALSE ) - df_source <- create_data_source(test_df, table_name = "test_table") + df_source <- as_querychat_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) prompt <- create_system_prompt( @@ -220,7 +220,7 @@ test_that("QueryChat$new() automatically handles data.frame inputs", { expect_s3_class(qc$data_source, "data_frame_source") # Should work with proper data source too - df_source <- create_data_source(test_df, table_name = "test_table") + df_source <- as_querychat_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) qc2 <- QueryChat$new( @@ -240,7 +240,7 @@ test_that("QueryChat$new() works with both source types", { ) # Create data source and test with QueryChat$new() - df_source <- create_data_source(test_df, table_name = "test_source") + df_source <- as_querychat_data_source(test_df, table_name = "test_source") withr::defer(cleanup_source(df_source)) qc <- QueryChat$new( @@ -259,7 +259,7 @@ test_that("QueryChat$new() works with both source types", { dbWriteTable(conn, "test_table", test_df, overwrite = TRUE) - dbi_source <- create_data_source(conn, "test_table") + dbi_source <- as_querychat_data_source(conn, "test_table") qc2 <- QueryChat$new( data_source = dbi_source, table_name = "test_table", diff --git a/pkg-r/tests/testthat/test-db-type.R b/pkg-r/tests/testthat/test-db-type.R index 217f7054..7894d6e8 100644 --- a/pkg-r/tests/testthat/test-db-type.R +++ b/pkg-r/tests/testthat/test-db-type.R @@ -3,7 +3,7 @@ library(testthat) test_that("get_db_type returns correct type for data_frame_source", { # Create a simple data frame source df <- data.frame(x = 1:5, y = letters[1:5]) - df_source <- create_data_source(df, "test_table") + df_source <- as_querychat_data_source(df, "test_table") # Test that get_db_type returns "DuckDB" expect_equal(get_db_type(df_source), "DuckDB") @@ -17,7 +17,7 @@ test_that("get_db_type returns correct type for dbi_source with SQLite", { conn <- DBI::dbConnect(RSQLite::SQLite(), temp_db) withr::defer(DBI::dbDisconnect(conn)) DBI::dbWriteTable(conn, "test_table", data.frame(x = 1:5, y = letters[1:5])) - db_source <- create_data_source(conn, "test_table") + db_source <- as_querychat_data_source(conn, "test_table") # Test that get_db_type returns the correct database type expect_equal(get_db_type(db_source), "SQLite") @@ -26,7 +26,7 @@ test_that("get_db_type returns correct type for dbi_source with SQLite", { test_that("get_db_type is correctly used in create_system_prompt", { # Create a simple data frame source df <- data.frame(x = 1:5, y = letters[1:5]) - df_source <- create_data_source(df, "test_table") + df_source <- as_querychat_data_source(df, "test_table") # Generate system prompt sys_prompt <- create_system_prompt(df_source) @@ -38,7 +38,7 @@ test_that("get_db_type is correctly used in create_system_prompt", { test_that("get_db_type is used to customize prompt template", { # Create a simple data frame source df <- data.frame(x = 1:5, y = letters[1:5]) - df_source <- create_data_source(df, "test_table") + df_source <- as_querychat_data_source(df, "test_table") # Get the db_type db_type <- get_db_type(df_source) diff --git a/pkg-r/tests/testthat/test-querychat-server.R b/pkg-r/tests/testthat/test-querychat-server.R index 6b30409f..503620d0 100644 --- a/pkg-r/tests/testthat/test-querychat-server.R +++ b/pkg-r/tests/testthat/test-querychat-server.R @@ -20,7 +20,7 @@ test_that("database source query functionality", { dbWriteTable(conn, "users", test_data, overwrite = TRUE) # Create database source - db_source <- create_data_source(conn, "users") + db_source <- as_querychat_data_source(conn, "users") # Test that we can execute queries result <- execute_query(db_source, "SELECT * FROM users WHERE age > 30") diff --git a/pkg-r/tests/testthat/test-shiny-app.R b/pkg-r/tests/testthat/test-shiny-app.R index e6c4c5c9..b4b7cf3c 100644 --- a/pkg-r/tests/testthat/test-shiny-app.R +++ b/pkg-r/tests/testthat/test-shiny-app.R @@ -35,7 +35,7 @@ test_that("database reactive functionality works correctly", { db_conn <- dbConnect(RSQLite::SQLite(), temp_db) withr::defer(dbDisconnect(db_conn)) - iris_source <- create_data_source(db_conn, "iris") + iris_source <- as_querychat_data_source(db_conn, "iris") # Mock chat function mock_client <- ellmer::chat_openai(api_key = "boop") diff --git a/pkg-r/tests/testthat/test-sql-comments.R b/pkg-r/tests/testthat/test-sql-comments.R index 992a5160..737093ca 100644 --- a/pkg-r/tests/testthat/test-sql-comments.R +++ b/pkg-r/tests/testthat/test-sql-comments.R @@ -12,7 +12,7 @@ test_that("execute_query handles SQL with inline comments", { ) # Create data source - df_source <- create_data_source(test_df, table_name = "test_table") + df_source <- as_querychat_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) # Test with inline comments @@ -51,7 +51,7 @@ test_that("execute_query handles SQL with multiline comments", { ) # Create data source - df_source <- create_data_source(test_df, table_name = "test_table") + df_source <- as_querychat_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) # Test with multiline comments @@ -95,7 +95,7 @@ test_that("execute_query handles SQL with trailing semicolons", { ) # Create data source - df_source <- create_data_source(test_df, table_name = "test_table") + df_source <- as_querychat_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) # Test with trailing semicolon @@ -132,7 +132,7 @@ test_that("execute_query handles SQL with mixed comments and semicolons", { ) # Create data source - df_source <- create_data_source(test_df, table_name = "test_table") + df_source <- as_querychat_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) # Test with a mix of comment styles and semicolons @@ -180,7 +180,7 @@ test_that("execute_query handles SQL with unusual whitespace patterns", { ) # Create data source - df_source <- create_data_source(test_df, table_name = "test_table") + df_source <- as_querychat_data_source(test_df, table_name = "test_table") withr::defer(cleanup_source(df_source)) # Test with unusual whitespace patterns (which LLMs might generate) diff --git a/pkg-r/tests/testthat/test-test-query.R b/pkg-r/tests/testthat/test-test-query.R index 4c15b70d..afdb0f54 100644 --- a/pkg-r/tests/testthat/test-test-query.R +++ b/pkg-r/tests/testthat/test-test-query.R @@ -18,7 +18,7 @@ test_that("test_query.dbi_source correctly retrieves one row of data", { dbWriteTable(conn, "test_table", test_df, overwrite = TRUE) - dbi_source <- create_data_source(conn, "test_table") + dbi_source <- as_querychat_data_source(conn, "test_table") withr::defer(cleanup_source(dbi_source)) # Test basic query - should only return one row @@ -62,7 +62,7 @@ test_that("test_query.dbi_source handles errors correctly", { ) dbWriteTable(conn, "test_table", test_df, overwrite = TRUE) - dbi_source <- create_data_source(conn, "test_table") + dbi_source <- as_querychat_data_source(conn, "test_table") withr::defer(cleanup_source(dbi_source), priority = "last") # Test with invalid SQL @@ -96,7 +96,7 @@ test_that("test_query.dbi_source works with different data types", { dbWriteTable(conn, "types_table", test_df, overwrite = TRUE) - dbi_source <- create_data_source(conn, "types_table") + dbi_source <- as_querychat_data_source(conn, "types_table") withr::defer(cleanup_source(dbi_source), priority = "last") # Test query with different column types From 5c10ad31cb07016ee39b8b99613d7ebf94dc0d9f Mon Sep 17 00:00:00 2001 From: cpsievert Date: Tue, 25 Nov 2025 20:27:38 +0000 Subject: [PATCH 12/25] `devtools::document()` (GitHub Actions) --- pkg-r/NAMESPACE | 4 ++-- pkg-r/man/QueryChat.Rd | 20 +++++++------------ ..._source.Rd => as_querychat_data_source.Rd} | 0 3 files changed, 9 insertions(+), 15 deletions(-) rename pkg-r/man/{create_data_source.Rd => as_querychat_data_source.Rd} (100%) diff --git a/pkg-r/NAMESPACE b/pkg-r/NAMESPACE index be0784cd..f6bec95c 100644 --- a/pkg-r/NAMESPACE +++ b/pkg-r/NAMESPACE @@ -1,8 +1,8 @@ # Generated by roxygen2: do not edit by hand -S3method(cleanup_source,dbi_source) 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) S3method(get_db_type,data_frame_source) @@ -11,8 +11,8 @@ S3method(get_db_type,default) S3method(get_schema,dbi_source) S3method(test_query,dbi_source) export(QueryChat) -export(cleanup_source) export(as_querychat_data_source) +export(cleanup_source) export(create_system_prompt) export(execute_query) export(get_db_type) diff --git a/pkg-r/man/QueryChat.Rd b/pkg-r/man/QueryChat.Rd index f6e53b1a..00780af9 100644 --- a/pkg-r/man/QueryChat.Rd +++ b/pkg-r/man/QueryChat.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/QueryChat-class.R +% Please edit documentation in R/QueryChat.R \name{QueryChat} \alias{QueryChat} \title{QueryChat: Interactive Data Querying with Natural Language} @@ -121,13 +121,10 @@ qc <- QueryChat$new(conn, "mtcars") library(querychat) qc <- QueryChat$new(mtcars, "mtcars") -app <- qc$app() - -# Run the app -shiny::runApp(app) +qc$app() -# Or return from a script -app +# Or explicitly run the app +shiny::runApp(qc$app()) } ## ------------------------------------------------ @@ -418,13 +415,10 @@ passed to \code{\link[shiny:shinyApp]{shiny::shinyApp()}}. library(querychat) qc <- QueryChat$new(mtcars, "mtcars") -app <- qc$app() - -# Run the app -shiny::runApp(app) +qc$app() -# Or return from a script -app +# Or explicitly run the app +shiny::runApp(qc$app()) } } \if{html}{\out{
}} diff --git a/pkg-r/man/create_data_source.Rd b/pkg-r/man/as_querychat_data_source.Rd similarity index 100% rename from pkg-r/man/create_data_source.Rd rename to pkg-r/man/as_querychat_data_source.Rd From be0ddda90478ba22fcdd427b5fde9bc266c8288d Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 25 Nov 2025 15:29:11 -0600 Subject: [PATCH 13/25] fix: pass along ... correctly --- pkg-r/R/querychat_module.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg-r/R/querychat_module.R b/pkg-r/R/querychat_module.R index 07e872bf..f21d831c 100644 --- a/pkg-r/R/querychat_module.R +++ b/pkg-r/R/querychat_module.R @@ -1,5 +1,5 @@ # Main module UI function -mod_ui <- function(id) { +mod_ui <- function(id, ...) { ns <- shiny::NS(id) htmltools::tagList( htmltools::htmlDependency( @@ -13,8 +13,8 @@ mod_ui <- function(id) { shinychat::chat_ui( ns("chat"), height = "100%", - fill = TRUE, - class = "querychat" + class = "querychat", + ... ) ) } From 1b32ab6af0e3dab59d591a0ad44fc83cbe7c2d5e Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 25 Nov 2025 15:33:06 -0600 Subject: [PATCH 14/25] fix: don't cleanup automatically when QueryChat gets initialized outside of active session --- pkg-r/R/QueryChat.R | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index 1919cd03..a9c669c7 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -129,8 +129,10 @@ QueryChat <- R6::R6Class( #' @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 auto_cleanup Logical indicating whether to automatically close the - #' data source when the Shiny app stops. Default is `TRUE`. + #' @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. #' @@ -165,7 +167,7 @@ QueryChat <- R6::R6Class( categorical_threshold = 20, extra_instructions = NULL, prompt_template = NULL, - auto_cleanup = TRUE + cleanup = NA ) { rlang::check_dots_empty() @@ -207,9 +209,12 @@ QueryChat <- R6::R6Class( private$.client$set_turns(list()) private$.client$set_system_prompt(prompt) - if (auto_cleanup) { - # 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) + # 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() From 4e9255e7e03817d4e7b6f5699598fe2bf9208f9e Mon Sep 17 00:00:00 2001 From: cpsievert Date: Tue, 25 Nov 2025 21:39:30 +0000 Subject: [PATCH 15/25] `devtools::document()` (GitHub Actions) --- pkg-r/man/QueryChat.Rd | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg-r/man/QueryChat.Rd b/pkg-r/man/QueryChat.Rd index 00780af9..0702c714 100644 --- a/pkg-r/man/QueryChat.Rd +++ b/pkg-r/man/QueryChat.Rd @@ -294,7 +294,7 @@ Create a new QueryChat object. categorical_threshold = 20, extra_instructions = NULL, prompt_template = NULL, - auto_cleanup = TRUE + cleanup = NA )}\if{html}{\out{}} } @@ -344,8 +344,10 @@ model in plain text or Markdown. Can be a string or a file path.} template file. If not provided, the default querychat template will be used. See the package prompts directory for the default template format.} -\item{\code{auto_cleanup}}{Logical indicating whether to automatically close the -data source when the Shiny app stops. Default is \code{TRUE}.} +\item{\code{cleanup}}{Whether or not to automatically run \verb{$cleanup()} when the +Shiny session/app stops. By default, cleanup only occurs if \code{QueryChat} +gets created within a Shiny session. Set to \code{TRUE} to always clean up, +or \code{FALSE} to never clean up automatically.} } \if{html}{\out{}} } From c37cba2a356d3b3b7509ed4ab68c847e3e5d5785 Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 25 Nov 2025 15:47:51 -0600 Subject: [PATCH 16/25] fix: mention shiny::runExample() --- pkg-r/R/querychat-package.R | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg-r/R/querychat-package.R b/pkg-r/R/querychat-package.R index 2e0c613f..921875ed 100644 --- a/pkg-r/R/querychat-package.R +++ b/pkg-r/R/querychat-package.R @@ -44,10 +44,19 @@ #' - [as_querychat_data_source()]: (Advanced) Create custom data source objects #' #' @section Examples: -#' See the package examples directory for complete working apps: -#' - `01-hello-app/`: Minimal example using `$app()` -#' - `02-sidebar-app/`: Custom layout with additional UI elements -#' - `sqlite/`: Database connection example +#' 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" From 2b5d6eb0e27ce88dc2606b6720c78d9fe7e0f152 Mon Sep 17 00:00:00 2001 From: cpsievert Date: Tue, 25 Nov 2025 21:53:40 +0000 Subject: [PATCH 17/25] `air format` (GitHub Actions) --- pkg-r/R/querychat-package.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg-r/R/querychat-package.R b/pkg-r/R/querychat-package.R index 921875ed..e19e1636 100644 --- a/pkg-r/R/querychat-package.R +++ b/pkg-r/R/querychat-package.R @@ -45,7 +45,7 @@ #' #' @section Examples: #' To see examples included with the package, run: -#' +#' #' ```r #' shiny::runExample(package = "querychat") #' ``` From abcdefb4ca8f9b8206f1f9e0ed05168356501133 Mon Sep 17 00:00:00 2001 From: cpsievert Date: Tue, 25 Nov 2025 21:53:43 +0000 Subject: [PATCH 18/25] `devtools::document()` (GitHub Actions) --- pkg-r/man/querychat-package.Rd | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pkg-r/man/querychat-package.Rd b/pkg-r/man/querychat-package.Rd index c7c4e817..f9346e7e 100644 --- a/pkg-r/man/querychat-package.Rd +++ b/pkg-r/man/querychat-package.Rd @@ -59,12 +59,16 @@ shinyApp(ui, server) \section{Examples}{ -See the package examples directory for complete working apps: -\itemize{ -\item \verb{01-hello-app/}: Minimal example using \verb{$app()} -\item \verb{02-sidebar-app/}: Custom layout with additional UI elements -\item \verb{sqlite/}: Database connection example -} +To see examples included with the package, run: + +\if{html}{\out{
}}\preformatted{shiny::runExample(package = "querychat") +}\if{html}{\out{
}} + +This provides a list of available examples. To run a specific example, like +'01-hello-app', use: + +\if{html}{\out{
}}\preformatted{shiny::runExample("01-hello-app", package = "querychat") +}\if{html}{\out{
}} } \seealso{ From bc157252c2e000b35a9828d7abc3d2d0ec66a35b Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 25 Nov 2025 16:44:51 -0600 Subject: [PATCH 19/25] feat: Add querychat() and querychat_app() convenience functions --- pkg-r/R/QueryChat.R | 161 ++++++++++++++++++++++++++++++++++++++++++- pkg-r/R/deprecated.R | 20 ------ 2 files changed, 160 insertions(+), 21 deletions(-) diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index a9c669c7..ca0246ef 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -653,9 +653,168 @@ QueryChat <- R6::R6Class( ) +#' 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 = TRUE +) { + 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 +#' @examples +#' \dontrun{ +#' # Quick start - create and run app in one line +#' querychat_app(mtcars, "mtcars") +#' +#' # With options +#' querychat_app( +#' mtcars, +#' "mtcars", +#' greeting = "Welcome to the mtcars explorer!", +#' client = "openai/gpt-4o" +#' ) +#' +#' # With database +#' library(DBI) +#' conn <- dbConnect(RSQLite::SQLite(), ":memory:") +#' dbWriteTable(conn, "mtcars", mtcars) +#' querychat_app(conn, "mtcars") +#' } +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)) { - return(data_source) + data_source } else { as_querychat_data_source(data_source, table_name) } diff --git a/pkg-r/R/deprecated.R b/pkg-r/R/deprecated.R index e9875f51..c5721e5f 100644 --- a/pkg-r/R/deprecated.R +++ b/pkg-r/R/deprecated.R @@ -36,26 +36,6 @@ querychat_init <- function(...) { ) } -#' @rdname deprecated -#' @export -querychat_app <- function(...) { - lifecycle::deprecate_stop( - when = "0.1.0", - what = "querychat_app()", - with = "QueryChat$app()", - details = c( - "Old code:", - " querychat_app(config)", - "", - "New code:", - " qc <- QueryChat$new(data, 'table_name')", - " qc$app()", - "", - "See ?QueryChat for more information." - ) - ) -} - #' @rdname deprecated #' @export querychat_sidebar <- function(...) { From a24ae94002671ac0f5871409b3738e41d7f6f48d Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 25 Nov 2025 16:49:40 -0600 Subject: [PATCH 20/25] fix: cleanup default value --- pkg-r/R/QueryChat.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index ca0246ef..07f9ded3 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -736,7 +736,7 @@ querychat <- function( categorical_threshold = 20, extra_instructions = NULL, prompt_template = NULL, - cleanup = TRUE + cleanup = NA ) { QueryChat$new( data_source = data_source, From c1b0534a2aeea4a66c578823f5a95923d3c5e58f Mon Sep 17 00:00:00 2001 From: cpsievert Date: Tue, 25 Nov 2025 22:54:49 +0000 Subject: [PATCH 21/25] `devtools::document()` (GitHub Actions) --- pkg-r/NAMESPACE | 1 + pkg-r/man/deprecated.Rd | 3 - pkg-r/man/querychat-convenience.Rd | 143 +++++++++++++++++++++++++++++ pkg-r/man/querychat-package.Rd | 1 - 4 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 pkg-r/man/querychat-convenience.Rd diff --git a/pkg-r/NAMESPACE b/pkg-r/NAMESPACE index f6bec95c..fa321d5b 100644 --- a/pkg-r/NAMESPACE +++ b/pkg-r/NAMESPACE @@ -17,6 +17,7 @@ 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) diff --git a/pkg-r/man/deprecated.Rd b/pkg-r/man/deprecated.Rd index 11f1186d..7cf475a1 100644 --- a/pkg-r/man/deprecated.Rd +++ b/pkg-r/man/deprecated.Rd @@ -3,7 +3,6 @@ \name{deprecated} \alias{deprecated} \alias{querychat_init} -\alias{querychat_app} \alias{querychat_sidebar} \alias{querychat_ui} \alias{querychat_server} @@ -13,8 +12,6 @@ \usage{ querychat_init(...) -querychat_app(...) - querychat_sidebar(...) querychat_ui(...) diff --git a/pkg-r/man/querychat-convenience.Rd b/pkg-r/man/querychat-convenience.Rd new file mode 100644 index 00000000..aacf2c26 --- /dev/null +++ b/pkg-r/man/querychat-convenience.Rd @@ -0,0 +1,143 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/QueryChat.R +\name{querychat} +\alias{querychat} +\alias{querychat_app} +\title{QueryChat convenience functions} +\usage{ +querychat( + 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_app( + 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" +) +} +\arguments{ +\item{data_source}{Either a data.frame or a database connection (e.g., DBI +connection).} + +\item{table_name}{A string specifying the table name to use in SQL queries. +If \code{data_source} is a data.frame, this is the name to refer to it by in +queries (typically the variable name). If \code{data_source} is a database +connection, this is the name of the table in the database.} + +\item{...}{Additional arguments (currently unused).} + +\item{id}{Optional module ID for the QueryChat instance. If not provided, +will be auto-generated from \code{table_name}. The ID is used to namespace +the Shiny module.} + +\item{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 \verb{$generate_greeting()} to create +a greeting to save and reuse.} + +\item{client}{Optional chat client. Can be: +\itemize{ +\item An \link[ellmer:Chat]{ellmer::Chat} object +\item A string to pass to \code{\link[ellmer:chat-any]{ellmer::chat()}} (e.g., \code{"openai/gpt-4o"}) +\item \code{NULL} (default): Uses the \code{querychat.client} option, the +\code{QUERYCHAT_CLIENT} environment variable, or defaults to +\code{\link[ellmer:chat_openai]{ellmer::chat_openai()}} +}} + +\item{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.} + +\item{categorical_threshold}{For text columns, the maximum number of unique +values to consider as a categorical variable. Default is 20.} + +\item{extra_instructions}{Optional additional instructions for the chat +model in plain text or Markdown. Can be a string or a file path.} + +\item{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.} + +\item{cleanup}{Whether or not to automatically run \verb{$cleanup()} when the +Shiny session/app stops. By default, cleanup only occurs if \code{QueryChat} +gets created within a Shiny session. Set to \code{TRUE} to always clean up, +or \code{FALSE} to never clean up automatically.} + +\item{bookmark_store}{The bookmarking storage method. Passed to +\code{\link[shiny:enableBookmarking]{shiny::enableBookmarking()}}. If \code{"url"} or \code{"server"}, the chat state +(including current query) will be bookmarked. Default is \code{"url"}.} +} +\value{ +A \code{QueryChat} object. See \link{QueryChat} for available methods. + +Invisibly returns the chat object after the app stops. +} +\description{ +Convenience functions for wrapping \link{QueryChat} creation (i.e., \code{querychat()}) +and app launching (i.e., \code{querychat_app()}). +} +\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() + +} +\dontrun{ +# Quick start - create and run app in one line +querychat_app(mtcars, "mtcars") + +# With options +querychat_app( + mtcars, + "mtcars", + greeting = "Welcome to the mtcars explorer!", + client = "openai/gpt-4o" +) + +# With database +library(DBI) +conn <- dbConnect(RSQLite::SQLite(), ":memory:") +dbWriteTable(conn, "mtcars", mtcars) +querychat_app(conn, "mtcars") +} +} diff --git a/pkg-r/man/querychat-package.Rd b/pkg-r/man/querychat-package.Rd index f9346e7e..5a74574b 100644 --- a/pkg-r/man/querychat-package.Rd +++ b/pkg-r/man/querychat-package.Rd @@ -2,7 +2,6 @@ % Please edit documentation in R/querychat-package.R \docType{package} \name{querychat-package} -\alias{querychat} \alias{querychat-package} \title{querychat: Chat with Your Data Using Natural Language} \description{ From 54cc5743a530f98cb52297e30b856f38604c58ce Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 25 Nov 2025 17:05:28 -0600 Subject: [PATCH 22/25] feat: Add app_obj() method --- pkg-r/R/QueryChat.R | 58 +++++++++++++++++-------- pkg-r/man/QueryChat.Rd | 98 ++++++++++++++++++++++++++++-------------- 2 files changed, 107 insertions(+), 49 deletions(-) diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index 07f9ded3..3878e2ce 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -223,20 +223,19 @@ QueryChat <- R6::R6Class( }, #' @description - #' Create a complete Shiny app with sensible defaults. + #' Create and run a Shiny gadget for chatting with data #' - #' This is a convenience method that creates a full Shiny application 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 + #' 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 A Shiny app object that can be run with [shiny::runApp()] or - #' passed to [shiny::shinyApp()]. + #' @return Invisibly returns the chat object after the app stops. #' #' @examples #' \dontrun{ @@ -244,13 +243,43 @@ QueryChat <- R6::R6Class( #' #' qc <- QueryChat$new(mtcars, "mtcars") #' qc$app() + #' } + #' + app = function(..., bookmark_store = "url") { + app <- self$app_obj(..., bookmark_store = bookmark_store) + tryCatch(shiny::runGadget(app), interrupt = function(cnd) NULL) + invisible(private$server_values$chat) + }, + + #' @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) #' - #' # Or explicitly run the app - #' shiny::runApp(qc$app()) + #' qc <- QueryChat$new(mtcars, "mtcars") + #' app <- qc$app_obj() + #' shiny::runApp(app) #' } - app = function(bookmark_store = "url") { + #' + 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 @@ -295,11 +324,8 @@ QueryChat <- R6::R6Class( ) } - chat <- NULL - server <- function(input, output, session) { self$server() - chat <<- private$server_values$chat output$query_title <- shiny::renderText({ if (shiny::isTruthy(self$title())) { @@ -354,9 +380,7 @@ QueryChat <- R6::R6Class( }) } - app <- shiny::shinyApp(ui, server, enableBookmarking = bookmark_store) - tryCatch(shiny::runGadget(app), interrupt = function(cnd) NULL) - invisible(chat) + shiny::shinyApp(ui, server, enableBookmarking = bookmark_store) }, #' @description diff --git a/pkg-r/man/QueryChat.Rd b/pkg-r/man/QueryChat.Rd index 0702c714..f8974f11 100644 --- a/pkg-r/man/QueryChat.Rd +++ b/pkg-r/man/QueryChat.Rd @@ -48,27 +48,6 @@ shinyApp(ui, server) }\if{html}{\out{}} } -\section{Constructor}{ - -\code{QueryChat$new(data_source, table_name, id = NULL, greeting = NULL, client = NULL, data_description = NULL, extra_instructions = NULL, prompt_template = NULL)} -} - -\section{Methods}{ - -\describe{ -\item{\verb{$new(...)}}{Create a new QueryChat object.} -\item{\verb{$sidebar(...)}}{Create a sidebar UI with chat interface.} -\item{\verb{$ui(...)}}{Create the chat UI component.} -\item{\verb{$server()}}{Initialize server logic (call within server function).} -\item{\verb{$df()}}{Get the current filtered data frame (reactive).} -\item{\verb{$sql(query)}}{Get or set the current SQL query (reactive).} -\item{\verb{$title(value)}}{Get or set the current title (reactive).} -\item{\verb{$app()}}{Create a complete Shiny app with sensible defaults.} -\item{\verb{$generate_greeting(echo)}}{Generate a greeting message using the LLM.} -\item{\verb{$cleanup()}}{Clean up data source resources.} -} -} - \examples{ \dontrun{ # Basic usage with a data frame @@ -122,11 +101,22 @@ library(querychat) qc <- QueryChat$new(mtcars, "mtcars") qc$app() +} + + +## ------------------------------------------------ +## Method `QueryChat$app_obj` +## ------------------------------------------------ -# Or explicitly run the app -shiny::runApp(qc$app()) +\dontrun{ +library(querychat) + +qc <- QueryChat$new(mtcars, "mtcars") +app <- qc$app_obj() +shiny::runApp(app) } + ## ------------------------------------------------ ## Method `QueryChat$sidebar` ## ------------------------------------------------ @@ -266,6 +256,7 @@ qc2 <- QueryChat$new(mtcars, "mtcars", greeting = "mtcars_greeting.md") \itemize{ \item \href{#method-QueryChat-new}{\code{QueryChat$new()}} \item \href{#method-QueryChat-app}{\code{QueryChat$app()}} +\item \href{#method-QueryChat-app_obj}{\code{QueryChat$app_obj()}} \item \href{#method-QueryChat-sidebar}{\code{QueryChat$sidebar()}} \item \href{#method-QueryChat-ui}{\code{QueryChat$ui()}} \item \href{#method-QueryChat-server}{\code{QueryChat$server()}} @@ -385,9 +376,52 @@ qc <- QueryChat$new(conn, "mtcars") \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-QueryChat-app}{}}} \subsection{Method \code{app()}}{ -Create a complete Shiny app with sensible defaults. +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 \verb{$app_obj()}. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{QueryChat$app(..., bookmark_store = "url")}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{...}}{Arguments passed to \verb{$app_obj()}.} + +\item{\code{bookmark_store}}{The bookmarking storage method. Passed to +\code{\link[shiny:enableBookmarking]{shiny::enableBookmarking()}}. If \code{"url"} or \code{"server"}, the chat state +(including current query) will be bookmarked. Default is \code{"url"}.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +Invisibly returns the chat object after the app stops. +} +\subsection{Examples}{ +\if{html}{\out{
}} +\preformatted{\dontrun{ +library(querychat) + +qc <- QueryChat$new(mtcars, "mtcars") +qc$app() +} + +} +\if{html}{\out{
}} + +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-QueryChat-app_obj}{}}} +\subsection{Method \code{app_obj()}}{ +A streamlined Shiny app for chatting with data -This is a convenience method that creates a full Shiny application with: +Creates a Shiny app designed for chatting with data, with: \itemize{ \item A sidebar containing the chat interface \item A card displaying the current SQL query @@ -395,12 +429,14 @@ This is a convenience method that creates a full Shiny application with: \item A reset button to clear the query } \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{QueryChat$app(bookmark_store = "url")}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{QueryChat$app_obj(..., bookmark_store = "url")}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ +\item{\code{...}}{Additional arguments (currently unused).} + \item{\code{bookmark_store}}{The bookmarking storage method. Passed to \code{\link[shiny:enableBookmarking]{shiny::enableBookmarking()}}. If \code{"url"} or \code{"server"}, the chat state (including current query) will be bookmarked. Default is \code{"url"}.} @@ -408,8 +444,7 @@ This is a convenience method that creates a full Shiny application with: \if{html}{\out{
}} } \subsection{Returns}{ -A Shiny app object that can be run with \code{\link[shiny:runApp]{shiny::runApp()}} or -passed to \code{\link[shiny:shinyApp]{shiny::shinyApp()}}. +A Shiny app object that can be run with \code{shiny::runApp()}. } \subsection{Examples}{ \if{html}{\out{
}} @@ -417,11 +452,10 @@ passed to \code{\link[shiny:shinyApp]{shiny::shinyApp()}}. library(querychat) qc <- QueryChat$new(mtcars, "mtcars") -qc$app() - -# Or explicitly run the app -shiny::runApp(qc$app()) +app <- qc$app_obj() +shiny::runApp(app) } + } \if{html}{\out{
}} From cb3946dbd1cad6ebb90cd6e3c02eed05e333a4b2 Mon Sep 17 00:00:00 2001 From: cpsievert Date: Tue, 25 Nov 2025 23:10:21 +0000 Subject: [PATCH 23/25] `devtools::document()` (GitHub Actions) --- pkg-r/man/QueryChat.Rd | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pkg-r/man/QueryChat.Rd b/pkg-r/man/QueryChat.Rd index f8974f11..42f22e91 100644 --- a/pkg-r/man/QueryChat.Rd +++ b/pkg-r/man/QueryChat.Rd @@ -48,6 +48,27 @@ shinyApp(ui, server) }\if{html}{\out{}} } +\section{Constructor}{ + +\code{QueryChat$new(data_source, table_name, id = NULL, greeting = NULL, client = NULL, data_description = NULL, extra_instructions = NULL, prompt_template = NULL)} +} + +\section{Methods}{ + +\describe{ +\item{\verb{$new(...)}}{Create a new QueryChat object.} +\item{\verb{$sidebar(...)}}{Create a sidebar UI with chat interface.} +\item{\verb{$ui(...)}}{Create the chat UI component.} +\item{\verb{$server()}}{Initialize server logic (call within server function).} +\item{\verb{$df()}}{Get the current filtered data frame (reactive).} +\item{\verb{$sql(query)}}{Get or set the current SQL query (reactive).} +\item{\verb{$title(value)}}{Get or set the current title (reactive).} +\item{\verb{$app()}}{Create a complete Shiny app with sensible defaults.} +\item{\verb{$generate_greeting(echo)}}{Generate a greeting message using the LLM.} +\item{\verb{$cleanup()}}{Clean up data source resources.} +} +} + \examples{ \dontrun{ # Basic usage with a data frame From a5c13baa41555c4be9229a5ab7b6243ed09c5de9 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 26 Nov 2025 11:44:23 -0600 Subject: [PATCH 24/25] fix: enforce session-specific server values by accessing through (); handle fallout and address feedback --- pkg-r/R/QueryChat.R | 235 +++------------ pkg-r/R/querychat_module.R | 2 +- pkg-r/README.md | 8 +- .../inst/examples-shiny/02-sidebar-app/app.R | 6 +- pkg-r/inst/examples-shiny/sqlite/app.R | 6 +- pkg-r/man/QueryChat.Rd | 271 ++---------------- pkg-r/tests/testthat/apps/basic/app.R | 6 +- 7 files changed, 88 insertions(+), 446 deletions(-) diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index 3878e2ce..1c95f4c5 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -12,8 +12,8 @@ #' 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()`) -#' - Reactively read SQL results in your Shiny app (e.g., `$df()`) -#' - Programmatically get/set the current query and title (e.g., `$sql()`, `$title()`) +#' - 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 @@ -33,37 +33,15 @@ #' ) #' #' server <- function(input, output, session) { -#' qc$server() +#' qc_vals <- qc$server() #' -#' output$sql <- renderText(qc$sql()) -#' output$data <- renderDataTable(qc$df()) +#' output$sql <- renderText(qc_vals$sql()) +#' output$data <- renderDataTable(qc_vals$df()) #' } #' #' shinyApp(ui, server) #' ``` #' -#' @section Constructor: -#' `QueryChat$new(data_source, table_name, id = NULL, greeting = NULL, -#' client = NULL, data_description = NULL, -#' extra_instructions = NULL, prompt_template = NULL)` -#' -#' @section Methods: -#' \describe{ -#' \item{`$new(...)`}{Create a new QueryChat object.} -#' \item{`$sidebar(...)`}{Create a sidebar UI with chat interface.} -#' \item{`$ui(...)`}{Create the chat UI component.} -#' \item{`$server()`}{Initialize server logic (call within server function).} -#' \item{`$df()`}{Get the current filtered data frame (reactive).} -#' \item{`$sql(query)`}{Get or set the current SQL query (reactive).} -#' \item{`$title(value)`}{Get or set the current title (reactive).} -#' \item{`$app()`}{Create a complete Shiny app with sensible defaults.} -#' \item{`$generate_greeting(echo)`}{Generate a greeting message using the LLM.} -#' \item{`$cleanup()`}{Clean up data source resources.} -#' } -#' -#' @field greeting The greeting message displayed to users. -#' @field id The module ID for namespacing. -#' #' @export #' @examples #' \dontrun{ @@ -92,7 +70,9 @@ QueryChat <- R6::R6Class( .client = NULL ), public = list( + #' @field greeting The greeting message displayed to users. greeting = NULL, + #' @field id The module ID for namespacing. id = NULL, #' @description @@ -235,7 +215,11 @@ QueryChat <- R6::R6Class( #' [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. + #' @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{ @@ -247,8 +231,8 @@ QueryChat <- R6::R6Class( #' app = function(..., bookmark_store = "url") { app <- self$app_obj(..., bookmark_store = bookmark_store) - tryCatch(shiny::runGadget(app), interrupt = function(cnd) NULL) - invisible(private$server_values$chat) + vals <- tryCatch(shiny::runGadget(app), interrupt = function(cnd) NULL) + invisible(vals) }, #' @description @@ -325,18 +309,18 @@ QueryChat <- R6::R6Class( } server <- function(input, output, session) { - self$server() + qc_vals <- self$server() output$query_title <- shiny::renderText({ - if (shiny::isTruthy(self$title())) { - self$title() + if (shiny::isTruthy(qc_vals$title())) { + qc_vals$title() } else { "SQL Query" } }) output$ui_reset <- shiny::renderUI({ - shiny::req(self$sql()) + shiny::req(qc_vals$sql()) shiny::actionButton( "reset_query", @@ -346,21 +330,21 @@ QueryChat <- R6::R6Class( }) shiny::observeEvent(input$reset_query, label = "on_reset_query", { - self$sql("") - self$title(NULL) + qc_vals$sql("") + qc_vals$title(NULL) }) output$dt <- DT::renderDT({ DT::datatable( - self$df(), + qc_vals$df(), fillContainer = TRUE, options = list(pageLength = 25, scrollX = TRUE) ) }) output$sql_output <- shiny::renderUI({ - sql <- if (shiny::isTruthy(self$sql())) { - self$sql() + sql <- if (shiny::isTruthy(qc_vals$sql())) { + qc_vals$sql() } else { paste("SELECT * FROM", table_name) } @@ -376,7 +360,12 @@ QueryChat <- R6::R6Class( }) shiny::observeEvent(input$close_btn, label = "on_close_btn", { - shiny::stopApp() + shiny::stopApp(list( + df = qc_vals$df(), + sql = qc_vals$sql(), + title = qc_vals$title(), + client = qc_vals$client + )) }) } @@ -440,23 +429,28 @@ QueryChat <- R6::R6Class( #' 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 populates the internal - #' server values that are accessed via `$df()`, `$sql()`, and `$title()`. + #' the reactive logic for the chat interface and returns session-specific + #' reactive values. #' #' @param session The Shiny session object. #' - #' @return Invisibly returns `NULL`. Access reactive values via `$df()`, - #' `$sql()`, and `$title()` methods. + #' @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$server() + #' qc_vals <- qc$server() #' - #' output$data <- renderDataTable(qc$df()) - #' output$query <- renderText(qc$sql()) + #' 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()) { @@ -466,147 +460,12 @@ QueryChat <- R6::R6Class( ) } - private$server_values <- mod_server( + mod_server( self$id, data_source = private$.data_source, greeting = self$greeting, client = private$.client ) - - invisible(NULL) - }, - - #' @description - #' Get the current filtered data frame. - #' - #' This is a reactive expression that returns the data after applying the - #' current SQL query. If no query has been set, returns the unfiltered data. - #' - #' @return A data.frame with the filtered/transformed data. - #' - #' @examples - #' \dontrun{ - #' qc <- QueryChat$new(mtcars, "mtcars") - #' - #' server <- function(input, output, session) { - #' qc$server() - #' - #' output$table <- renderDataTable({ - #' qc$df() # Reactive - will update when query changes - #' }) - #' } - #' } - df = function() { - vals <- private$server_values - if (is.null(vals)) { - rlang::abort( - "Must call $server() before accessing $df(). Make sure to call server() within your Shiny server function." - ) - } - vals$df() - }, - - #' @description - #' Get or set the current SQL query. - #' - #' This method provides both getter and setter functionality for the SQL - #' query. When called without arguments, it returns the current query. - #' When called with a query string, it sets the query and returns whether - #' the query changed. - #' - #' @param query Optional SQL query string. If provided, sets the current - #' query to this value. If `NULL` (default), returns the current query. - #' - #' @return - #' - When `query = NULL` (getter): Returns the current SQL query as a string - #' (may be an empty string `""` if no query has been set). - #' - When `query` is provided (setter): Returns `TRUE` if the query was - #' changed to a new value, `FALSE` if it was the same as the current value. - #' - #' @examples - #' \dontrun{ - #' qc <- QueryChat$new(mtcars, "mtcars") - #' - #' server <- function(input, output, session) { - #' qc$server() - #' - #' # Get current query - #' output$current_query <- renderText({ - #' qc$sql() - #' }) - #' - #' # Set query programmatically - #' observeEvent(input$filter_button, { - #' qc$sql("SELECT * FROM mtcars WHERE cyl = 6") - #' }) - #' } - #' } - sql = function(query = NULL) { - vals <- private$server_values - if (is.null(vals)) { - rlang::abort( - "Must call $server() before accessing $sql(). Make sure to call $server() within your Shiny server function." - ) - } - - if (is.null(query)) { - vals$sql() - } else { - old_query <- shiny::isolate(vals$sql()) - vals$sql(query) - return(!identical(old_query, query)) - } - }, - - #' @description - #' Get or set the current title. - #' - #' The title is a short description of the current query that the LLM - #' provides whenever it generates a new SQL query. It can be used as a - #' status string for the data dashboard. - #' - #' @param value Optional title string. If provided, sets the current title - #' to this value. If `NULL` (default), returns the current title. - #' - #' @return - #' - When `value = NULL` (getter): Returns the current title as a string, - #' or `NULL` if no title has been set (because no SQL query has been set). - #' - When `value` is provided (setter): Returns `TRUE` if the title was - #' changed to a new value, `FALSE` if it was the same as the current value. - #' - #' @examples - #' \dontrun{ - #' qc <- QueryChat$new(mtcars, "mtcars") - #' - #' server <- function(input, output, session) { - #' qc$server() - #' - #' # Get current title - #' output$title <- renderText({ - #' qc$title() %||% "No Query" - #' }) - #' - #' # Set title programmatically - #' observeEvent(input$set_title, { - #' qc$title("Filtered Cars") - #' }) - #' } - #' } - title = function(value = NULL) { - vals <- private$server_values - if (is.null(vals)) { - rlang::abort( - "Must call $server() before accessing $title(). Make sure to call $server() within your Shiny server function." - ) - } - - if (is.null(value)) { - vals$title() - } else { - old_value <- shiny::isolate(vals$title()) - vals$title(value) - return(!identical(old_value, value)) - } }, #' @description @@ -660,13 +519,9 @@ QueryChat <- R6::R6Class( } ), active = list( - #' @field client Get the (session-specific) chat client. - client = function() { - vals <- private$server_values - if (is.null(vals)) { - rlang::abort("Must call $server() before accessing $client") - } - vals$chat + #' @field system_prompt Get the system prompt. + system_prompt = function() { + private$.client$get_system_prompt() }, #' @field data_source Get the current data source. diff --git a/pkg-r/R/querychat_module.R b/pkg-r/R/querychat_module.R index f21d831c..8e3dc20c 100644 --- a/pkg-r/R/querychat_module.R +++ b/pkg-r/R/querychat_module.R @@ -95,7 +95,7 @@ mod_server <- function(id, data_source, greeting, client) { }) list( - chat = chat, + client = chat, sql = current_query, title = current_title, df = filtered_df diff --git a/pkg-r/README.md b/pkg-r/README.md index 8ed52cb8..23a67b46 100644 --- a/pkg-r/README.md +++ b/pkg-r/README.md @@ -54,12 +54,12 @@ ui <- page_sidebar( ) server <- function(input, output, session) { - # 3. Initialize the QueryChat server - qc$server() + # 3. Initialize the QueryChat server (returns session-specific reactive values) + qc_vals <- qc$server() output$dt <- DT::renderDT({ - # 4. Use the filtered/sorted data frame anywhere you wish, via qc$df() - DT::datatable(qc$df()) + # 4. Use the filtered/sorted data frame anywhere you wish, via qc_vals$df() + DT::datatable(qc_vals$df()) }) } diff --git a/pkg-r/inst/examples-shiny/02-sidebar-app/app.R b/pkg-r/inst/examples-shiny/02-sidebar-app/app.R index a0bdfbfb..450ff6f1 100644 --- a/pkg-r/inst/examples-shiny/02-sidebar-app/app.R +++ b/pkg-r/inst/examples-shiny/02-sidebar-app/app.R @@ -71,12 +71,12 @@ ui <- page_sidebar( # Define server logic server <- function(input, output, session) { # Initialize QueryChat server - qc$server() + qc_vals <- qc$server() # Render the data table output$data_table <- DT::renderDT( { - qc$df() + qc_vals$df() }, fillContainer = TRUE, options = list(pageLength = 25, scrollX = TRUE) @@ -84,7 +84,7 @@ server <- function(input, output, session) { # Render the SQL query output$sql_query <- renderText({ - query <- qc$sql() + query <- qc_vals$sql() if (query == "") { "No filter applied - showing all data" } else { diff --git a/pkg-r/inst/examples-shiny/sqlite/app.R b/pkg-r/inst/examples-shiny/sqlite/app.R index c1d5bcee..63dcbd4a 100644 --- a/pkg-r/inst/examples-shiny/sqlite/app.R +++ b/pkg-r/inst/examples-shiny/sqlite/app.R @@ -75,18 +75,18 @@ ui <- page_sidebar( ) server <- function(input, output, session) { - qc$server() + qc_vals <- qc$server() output$data_table <- DT::renderDT( { - qc$df() + qc_vals$df() }, fillContainer = TRUE, options = list(pageLength = 10, scrollX = TRUE) ) output$sql_query <- renderText({ - query <- qc$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 index 42f22e91..4e221d87 100644 --- a/pkg-r/man/QueryChat.Rd +++ b/pkg-r/man/QueryChat.Rd @@ -15,8 +15,8 @@ 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 Reactively read SQL results in your Shiny app (e.g., \verb{$df()}) -\item Programmatically get/set the current query and title (e.g., \verb{$sql()}, \verb{$title()}) +\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}{ @@ -38,37 +38,16 @@ ui <- page_sidebar( ) server <- function(input, output, session) \{ - qc$server() + qc_vals <- qc$server() - output$sql <- renderText(qc$sql()) - output$data <- renderDataTable(qc$df()) + output$sql <- renderText(qc_vals$sql()) + output$data <- renderDataTable(qc_vals$df()) \} shinyApp(ui, server) }\if{html}{\out{}} } -\section{Constructor}{ - -\code{QueryChat$new(data_source, table_name, id = NULL, greeting = NULL, client = NULL, data_description = NULL, extra_instructions = NULL, prompt_template = NULL)} -} - -\section{Methods}{ - -\describe{ -\item{\verb{$new(...)}}{Create a new QueryChat object.} -\item{\verb{$sidebar(...)}}{Create a sidebar UI with chat interface.} -\item{\verb{$ui(...)}}{Create the chat UI component.} -\item{\verb{$server()}}{Initialize server logic (call within server function).} -\item{\verb{$df()}}{Get the current filtered data frame (reactive).} -\item{\verb{$sql(query)}}{Get or set the current SQL query (reactive).} -\item{\verb{$title(value)}}{Get or set the current title (reactive).} -\item{\verb{$app()}}{Create a complete Shiny app with sensible defaults.} -\item{\verb{$generate_greeting(echo)}}{Generate a greeting message using the LLM.} -\item{\verb{$cleanup()}}{Clean up data source resources.} -} -} - \examples{ \dontrun{ # Basic usage with a data frame @@ -171,70 +150,11 @@ ui <- fluidPage( qc <- QueryChat$new(mtcars, "mtcars") server <- function(input, output, session) { - qc$server() - - output$data <- renderDataTable(qc$df()) - output$query <- renderText(qc$sql()) -} -} - -## ------------------------------------------------ -## Method `QueryChat$df` -## ------------------------------------------------ - -\dontrun{ -qc <- QueryChat$new(mtcars, "mtcars") - -server <- function(input, output, session) { - qc$server() - - output$table <- renderDataTable({ - qc$df() # Reactive - will update when query changes - }) -} -} - -## ------------------------------------------------ -## Method `QueryChat$sql` -## ------------------------------------------------ - -\dontrun{ -qc <- QueryChat$new(mtcars, "mtcars") - -server <- function(input, output, session) { - qc$server() - - # Get current query - output$current_query <- renderText({ - qc$sql() - }) + qc_vals <- qc$server() - # Set query programmatically - observeEvent(input$filter_button, { - qc$sql("SELECT * FROM mtcars WHERE cyl = 6") - }) -} -} - -## ------------------------------------------------ -## Method `QueryChat$title` -## ------------------------------------------------ - -\dontrun{ -qc <- QueryChat$new(mtcars, "mtcars") - -server <- function(input, output, session) { - qc$server() - - # Get current title - output$title <- renderText({ - qc$title() \%||\% "No Query" - }) - - # Set title programmatically - observeEvent(input$set_title, { - qc$title("Filtered Cars") - }) + output$data <- renderDataTable(qc_vals$df()) + output$query <- renderText(qc_vals$sql()) + output$title <- renderText(qc_vals$title() \%||\% "No Query") } } @@ -266,7 +186,7 @@ qc2 <- QueryChat$new(mtcars, "mtcars", greeting = "mtcars_greeting.md") \section{Active bindings}{ \if{html}{\out{
}} \describe{ -\item{\code{client}}{Get the (session-specific) chat client.} +\item{\code{system_prompt}}{Get the system prompt.} \item{\code{data_source}}{Get the current data source.} } @@ -281,9 +201,6 @@ qc2 <- QueryChat$new(mtcars, "mtcars", greeting = "mtcars_greeting.md") \item \href{#method-QueryChat-sidebar}{\code{QueryChat$sidebar()}} \item \href{#method-QueryChat-ui}{\code{QueryChat$ui()}} \item \href{#method-QueryChat-server}{\code{QueryChat$server()}} -\item \href{#method-QueryChat-df}{\code{QueryChat$df()}} -\item \href{#method-QueryChat-sql}{\code{QueryChat$sql()}} -\item \href{#method-QueryChat-title}{\code{QueryChat$title()}} \item \href{#method-QueryChat-generate_greeting}{\code{QueryChat$generate_greeting()}} \item \href{#method-QueryChat-cleanup}{\code{QueryChat$cleanup()}} \item \href{#method-QueryChat-clone}{\code{QueryChat$clone()}} @@ -419,7 +336,13 @@ see \verb{$app_obj()}. \if{html}{\out{
}} } \subsection{Returns}{ -Invisibly returns the chat object after the app stops. +Invisibly returns a list of session-specific values: +\itemize{ +\item \code{df}: The final filtered data frame +\item \code{sql}: The final SQL query string +\item \code{title}: The final title +\item \code{client}: The session-specific chat client instance +} } \subsection{Examples}{ \if{html}{\out{
}} @@ -569,8 +492,8 @@ ui <- fluidPage( 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 populates the internal -server values that are accessed via \verb{$df()}, \verb{$sql()}, and \verb{$title()}. +the reactive logic for the chat interface and returns session-specific +reactive values. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{QueryChat$server(session = shiny::getDefaultReactiveDomain())}\if{html}{\out{
}} } @@ -583,89 +506,13 @@ server values that are accessed via \verb{$df()}, \verb{$sql()}, and \verb{$titl \if{html}{\out{
}} } \subsection{Returns}{ -Invisibly returns \code{NULL}. Access reactive values via \verb{$df()}, -\verb{$sql()}, and \verb{$title()} methods. -} -\subsection{Examples}{ -\if{html}{\out{
}} -\preformatted{\dontrun{ -qc <- QueryChat$new(mtcars, "mtcars") - -server <- function(input, output, session) { - qc$server() - - output$data <- renderDataTable(qc$df()) - output$query <- renderText(qc$sql()) -} -} -} -\if{html}{\out{
}} - -} - -} -\if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-QueryChat-df}{}}} -\subsection{Method \code{df()}}{ -Get the current filtered data frame. - -This is a reactive expression that returns the data after applying the -current SQL query. If no query has been set, returns the unfiltered data. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{QueryChat$df()}\if{html}{\out{
}} -} - -\subsection{Returns}{ -A data.frame with the filtered/transformed data. -} -\subsection{Examples}{ -\if{html}{\out{
}} -\preformatted{\dontrun{ -qc <- QueryChat$new(mtcars, "mtcars") - -server <- function(input, output, session) { - qc$server() - - output$table <- renderDataTable({ - qc$df() # Reactive - will update when query changes - }) -} -} -} -\if{html}{\out{
}} - -} - -} -\if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-QueryChat-sql}{}}} -\subsection{Method \code{sql()}}{ -Get or set the current SQL query. - -This method provides both getter and setter functionality for the SQL -query. When called without arguments, it returns the current query. -When called with a query string, it sets the query and returns whether -the query changed. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{QueryChat$sql(query = NULL)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{query}}{Optional SQL query string. If provided, sets the current -query to this value. If \code{NULL} (default), returns the current query.} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ +A list containing session-specific reactive values and the chat +client with the following elements: \itemize{ -\item When \code{query = NULL} (getter): Returns the current SQL query as a string -(may be an empty string \code{""} if no query has been set). -\item When \code{query} is provided (setter): Returns \code{TRUE} if the query was -changed to a new value, \code{FALSE} if it was the same as the current value. +\item \code{df}: Reactive expression returning the current filtered data frame +\item \code{sql}: Reactive value for the current SQL query string +\item \code{title}: Reactive value for the current title +\item \code{client}: The session-specific chat client instance } } \subsection{Examples}{ @@ -674,71 +521,11 @@ changed to a new value, \code{FALSE} if it was the same as the current value. qc <- QueryChat$new(mtcars, "mtcars") server <- function(input, output, session) { - qc$server() - - # Get current query - output$current_query <- renderText({ - qc$sql() - }) - - # Set query programmatically - observeEvent(input$filter_button, { - qc$sql("SELECT * FROM mtcars WHERE cyl = 6") - }) -} -} -} -\if{html}{\out{}} - -} - -} -\if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-QueryChat-title}{}}} -\subsection{Method \code{title()}}{ -Get or set the current title. - -The title is a short description of the current query that the LLM -provides whenever it generates a new SQL query. It can be used as a -status string for the data dashboard. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{QueryChat$title(value = NULL)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{value}}{Optional title string. If provided, sets the current title -to this value. If \code{NULL} (default), returns the current title.} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -\itemize{ -\item When \code{value = NULL} (getter): Returns the current title as a string, -or \code{NULL} if no title has been set (because no SQL query has been set). -\item When \code{value} is provided (setter): Returns \code{TRUE} if the title was -changed to a new value, \code{FALSE} if it was the same as the current value. -} -} -\subsection{Examples}{ -\if{html}{\out{
}} -\preformatted{\dontrun{ -qc <- QueryChat$new(mtcars, "mtcars") - -server <- function(input, output, session) { - qc$server() - - # Get current title - output$title <- renderText({ - qc$title() \%||\% "No Query" - }) + qc_vals <- qc$server() - # Set title programmatically - observeEvent(input$set_title, { - qc$title("Filtered Cars") - }) + output$data <- renderDataTable(qc_vals$df()) + output$query <- renderText(qc_vals$sql()) + output$title <- renderText(qc_vals$title() \%||\% "No Query") } } } diff --git a/pkg-r/tests/testthat/apps/basic/app.R b/pkg-r/tests/testthat/apps/basic/app.R index fa449163..35a55797 100644 --- a/pkg-r/tests/testthat/apps/basic/app.R +++ b/pkg-r/tests/testthat/apps/basic/app.R @@ -42,17 +42,17 @@ ui <- page_sidebar( ) server <- function(input, output, session) { - qc$server() + qc_vals <- qc$server() output$data_table <- DT::renderDT( { - qc$df() + qc_vals$df() }, options = list(pageLength = 5) ) output$sql_query <- renderText({ - query <- qc$sql() + query <- qc_vals$sql() if (query == "") "No filter applied" else query }) From 0f40c0d344af40bbf389d33cd06887f47b9b90a6 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 26 Nov 2025 13:20:31 -0600 Subject: [PATCH 25/25] Address feedback --- pkg-r/R/QueryChat.R | 23 ++--------------------- pkg-r/man/QueryChat.Rd | 6 +++--- pkg-r/man/querychat-convenience.Rd | 18 ------------------ 3 files changed, 5 insertions(+), 42 deletions(-) diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index 1c95f4c5..3c214b2d 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -477,7 +477,7 @@ QueryChat <- R6::R6Class( #' 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 `"text"` (print to console). + #' `"none"` (default, no output) or `"output"` (print to console). #' #' @return The greeting string in Markdown format. #' @@ -487,7 +487,7 @@ QueryChat <- R6::R6Class( #' qc <- QueryChat$new(mtcars, "mtcars") #' #' # Generate a greeting and save it - #' greeting <- qc$generate_greeting(echo = "text") + #' greeting <- qc$generate_greeting() #' writeLines(greeting, "mtcars_greeting.md") #' #' # Later, use the saved greeting @@ -640,25 +640,6 @@ querychat <- function( #' @return Invisibly returns the chat object after the app stops. #' #' @export -#' @examples -#' \dontrun{ -#' # Quick start - create and run app in one line -#' querychat_app(mtcars, "mtcars") -#' -#' # With options -#' querychat_app( -#' mtcars, -#' "mtcars", -#' greeting = "Welcome to the mtcars explorer!", -#' client = "openai/gpt-4o" -#' ) -#' -#' # With database -#' library(DBI) -#' conn <- dbConnect(RSQLite::SQLite(), ":memory:") -#' dbWriteTable(conn, "mtcars", mtcars) -#' querychat_app(conn, "mtcars") -#' } querychat_app <- function( data_source, table_name, diff --git a/pkg-r/man/QueryChat.Rd b/pkg-r/man/QueryChat.Rd index 4e221d87..6612e235 100644 --- a/pkg-r/man/QueryChat.Rd +++ b/pkg-r/man/QueryChat.Rd @@ -167,7 +167,7 @@ server <- function(input, output, session) { qc <- QueryChat$new(mtcars, "mtcars") # Generate a greeting and save it -greeting <- qc$generate_greeting(echo = "text") +greeting <- qc$generate_greeting() writeLines(greeting, "mtcars_greeting.md") # Later, use the saved greeting @@ -552,7 +552,7 @@ generate a greeting once and save it for reuse. \if{html}{\out{
}} \describe{ \item{\code{echo}}{Whether to print the greeting to the console. Options are -\code{"none"} (default, no output) or \code{"text"} (print to console).} +\code{"none"} (default, no output) or \code{"output"} (print to console).} } \if{html}{\out{
}} } @@ -566,7 +566,7 @@ The greeting string in Markdown format. qc <- QueryChat$new(mtcars, "mtcars") # Generate a greeting and save it -greeting <- qc$generate_greeting(echo = "text") +greeting <- qc$generate_greeting() writeLines(greeting, "mtcars_greeting.md") # Later, use the saved greeting diff --git a/pkg-r/man/querychat-convenience.Rd b/pkg-r/man/querychat-convenience.Rd index aacf2c26..14902139 100644 --- a/pkg-r/man/querychat-convenience.Rd +++ b/pkg-r/man/querychat-convenience.Rd @@ -121,23 +121,5 @@ qc <- querychat(mtcars, "mtcars") # Run the app later qc$app() -} -\dontrun{ -# Quick start - create and run app in one line -querychat_app(mtcars, "mtcars") - -# With options -querychat_app( - mtcars, - "mtcars", - greeting = "Welcome to the mtcars explorer!", - client = "openai/gpt-4o" -) - -# With database -library(DBI) -conn <- dbConnect(RSQLite::SQLite(), ":memory:") -dbWriteTable(conn, "mtcars", mtcars) -querychat_app(conn, "mtcars") } }