From d1dd68abead006ec74d361392a812aae51fccd5b Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 24 Jun 2026 10:21:24 -0400 Subject: [PATCH 01/24] feat(pkg-r): add QueryChatGreeter for schema-aware greetings Introduce a semi-internal QueryChatGreeter (R6) accessed via `qc$greeter`, which generates the opening greeting from a separate, leaner greeting system prompt (inst/prompts/greeting.md) rendered through the existing QueryChatSystemPrompt infrastructure, scoped to `greeter$tables`. - `qc$greeter$tables` / `$prompt` invalidate the cached greeting on set - constructor tables are always included; `add_table(include_in_greeting=)` and `add_tables(include_in_greeting=)` opt additional tables in - `$generate_greeting()` now delegates to `$greeter$generate()` - mod_server streams the greeting via the shared build_greeting_client() Fixes the schema-blind greeting regression from multi-table support (#195). Reuses the regression insight and test infra from #260; the greeting is generated on a separate client, so #260's GREETING_MARKER sentinel and history filtering are intentionally not carried over. --- pkg-r/R/QueryChat.R | 84 ++++++++++++++++++++++++++++++++-- pkg-r/R/QueryChatGreeter.R | 60 ++++++++++++++++++++++++ pkg-r/R/querychat_module.R | 7 ++- pkg-r/inst/prompts/greeting.md | 62 +++++++++++++++++++++++++ pkg-r/man/QueryChat.Rd | 23 +++++++++- 5 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 pkg-r/R/QueryChatGreeter.R create mode 100644 pkg-r/inst/prompts/greeting.md diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index 576d9abc..2220fa37 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -103,6 +103,30 @@ QueryChat <- R6::R6Class( .extra_instructions = NULL, .categorical_threshold = NULL, .data_dicts = list(), + .greeter = NULL, + + build_greeting_client = function(client_spec = NULL) { + data_sources <- private$.data_sources + tbls <- intersect(self$greeter$tables, names(data_sources)) + if (length(tbls) == 0) { + tbls <- names(data_sources) + } + sources <- data_sources[tbls] + greeting_prompt_obj <- QueryChatSystemPrompt$new( + prompt_template = self$greeter$prompt, + data_sources = sources, + data_description = private$.data_description, + extra_instructions = NULL, + categorical_threshold = private$.categorical_threshold, + data_dicts = private$.data_dicts + ) + spec <- client_spec %||% private$.client_spec + chat <- as_querychat_client(spec) + chat <- chat$clone() + chat$set_turns(list()) + chat$set_system_prompt(greeting_prompt_obj$render(tools = NULL)) + chat + }, require_initialized = function(method_name) { if (length(private$.data_sources) == 0) { @@ -365,6 +389,7 @@ QueryChat <- R6::R6Class( private$.data_sources[[normalized$table_name]] <- normalized private$auto_fill_data_description() private$build_system_prompt() + self$greeter$tables <- c(self$greeter$tables, normalized$table_name) self$id <- id %||% sprintf("querychat_%s", normalized$table_name) } else { # Deferred pattern: data_source is NULL @@ -397,9 +422,16 @@ QueryChat <- R6::R6Class( #' @param table_name The SQL table name for this data source. #' @param replace Whether to replace an existing table with this name. #' Default is `FALSE`. + #' @param include_in_greeting Whether to include this table in the greeting + #' context. Default is `FALSE`. #' #' @return Invisibly returns `self` for chaining. - add_table = function(data_source, table_name, replace = FALSE) { + add_table = function( + data_source, + table_name, + replace = FALSE, + include_in_greeting = FALSE + ) { if (private$.server_initialized) { cli::cli_abort("Cannot add tables after server initialization.") } @@ -447,6 +479,10 @@ QueryChat <- R6::R6Class( self$id <- sprintf("querychat_%s", table_name) } + if (isTRUE(include_in_greeting)) { + self$greeter$tables <- c(self$greeter$tables, table_name) + } + invisible(self) }, @@ -463,9 +499,18 @@ QueryChat <- R6::R6Class( #' by `DBI::dbListTables(conn)` are used. #' @param replace Whether to replace existing tables with the same name. #' Default is `FALSE`. + #' @param include_in_greeting Whether to include added tables in the greeting + #' context. `TRUE` includes all tables; `FALSE` (default) includes none; + #' a character vector includes only those named tables (intersected with + #' the tables being added). #' #' @return Invisibly returns `self` for chaining. - add_tables = function(conn, tables = NULL, replace = FALSE) { + add_tables = function( + conn, + tables = NULL, + replace = FALSE, + include_in_greeting = FALSE + ) { if (private$.server_initialized) { cli::cli_abort("Cannot add tables after server initialization.") } @@ -532,6 +577,19 @@ QueryChat <- R6::R6Class( private$.query_executor <- NULL } + greeting_tbls <- if (isTRUE(include_in_greeting)) { + tables + } else if (identical(include_in_greeting, FALSE)) { + character() + } else if (is.character(include_in_greeting)) { + intersect(include_in_greeting, tables) + } else { + character() + } + if (length(greeting_tbls) > 0) { + self$greeter$tables <- c(self$greeter$tables, greeting_tbls) + } + invisible(self) }, @@ -893,6 +951,10 @@ QueryChat <- R6::R6Class( ) } + greeting_client_fn <- function() { + private$build_greeting_client(client_spec = resolved_client_spec) + } + result <- mod_server( id %||% self$id, data_sources = private$.data_sources, @@ -900,6 +962,7 @@ QueryChat <- R6::R6Class( greeting = self$greeting, client = create_session_client, tools = self$tools, + greeting_client_fn = greeting_client_fn, enable_bookmarking = enable_bookmarking ) result @@ -913,8 +976,7 @@ QueryChat <- R6::R6Class( #' @return The greeting string in Markdown format. generate_greeting = function(echo = c("none", "output")) { private$require_initialized("$generate_greeting") - chat <- private$create_session_client() - as.character(chat$chat(GREETING_PROMPT, echo = echo)) + self$greeter$generate(echo = echo) }, #' @description @@ -932,6 +994,20 @@ QueryChat <- R6::R6Class( } ), active = list( + #' @field greeter The QueryChatGreeter controlling greeting generation; + #' access its `$tables` and `$prompt`. + greeter = function(value) { + if (!missing(value)) { + # The greeter is read-only. Sub-field assignments like + # `qc$greeter$tables <- x` mutate the greeter by reference and + # trigger a write-back of the (unchanged) binding, which we ignore. + return(invisible(value)) + } + private$.greeter <- private$.greeter %||% + QueryChatGreeter$new(parent = self) + private$.greeter + }, + #' @field system_prompt Get the system prompt. system_prompt = function() { private$require_initialized("$system_prompt") diff --git a/pkg-r/R/QueryChatGreeter.R b/pkg-r/R/QueryChatGreeter.R new file mode 100644 index 00000000..cb052bd0 --- /dev/null +++ b/pkg-r/R/QueryChatGreeter.R @@ -0,0 +1,60 @@ +#' QueryChatGreeter +#' +#' @description +#' Controls greeting generation for a [QueryChat] instance. Access via +#' `qc$greeter`. +#' +#' @noRd +QueryChatGreeter <- R6::R6Class( + "QueryChatGreeter", + private = list( + .parent = NULL, + .tables = NULL, + .prompt = NULL + ), + public = list( + #' @description Create a new QueryChatGreeter. + #' @param parent The owning QueryChat instance. + initialize = function(parent) { + private$.parent <- parent + private$.tables <- character() + private$.prompt <- system.file( + "prompts", + "greeting.md", + package = "querychat" + ) + }, + + #' @description Generate a greeting using the greeting system prompt. + #' @param echo Whether to echo the output (`"none"` or `"output"`). + #' @return The greeting string. + generate = function(echo = c("none", "output")) { + echo <- rlang::arg_match(echo) + chat <- private$.parent$.__enclos_env__$private$build_greeting_client() + txt <- as.character(chat$chat(GREETING_PROMPT, echo = echo)) + private$.parent$greeting <- txt + txt + } + ), + active = list( + #' @field tables Character vector of table names whose context to include in + #' the greeting. Set to invalidate the cached greeting. + tables = function(value) { + if (missing(value)) { + return(private$.tables) + } + private$.tables <- value + private$.parent$greeting <- NULL + }, + + #' @field prompt The greeting template (string or file path). Set to + #' invalidate the cached greeting. + prompt = function(value) { + if (missing(value)) { + return(private$.prompt) + } + private$.prompt <- value + private$.parent$greeting <- NULL + } + ) +) diff --git a/pkg-r/R/querychat_module.R b/pkg-r/R/querychat_module.R index cb1c7ff8..592ebcad 100644 --- a/pkg-r/R/querychat_module.R +++ b/pkg-r/R/querychat_module.R @@ -43,6 +43,7 @@ mod_server <- function( greeting, client, tools, + greeting_client_fn = NULL, enable_bookmarking = FALSE ) { shiny::moduleServer(id, function(input, output, session) { @@ -155,7 +156,11 @@ mod_server <- function( "i" = "For faster startup, lower cost, and determinism, consider providing a {.arg greeting} to {.fn QueryChat}.", "i" = "You can use your {.help querychat::QueryChat} object's {.fn $generate_greeting} method to generate a greeting." )) - greeting_client <- client(tools = NULL) + greeting_client <- if (!is.null(greeting_client_fn)) { + greeting_client_fn() + } else { + client(tools = NULL) + } stream <- greeting_client$stream_async(GREETING_PROMPT) p <- shinychat::chat_set_greeting( "chat", diff --git a/pkg-r/inst/prompts/greeting.md b/pkg-r/inst/prompts/greeting.md new file mode 100644 index 00000000..6db55866 --- /dev/null +++ b/pkg-r/inst/prompts/greeting.md @@ -0,0 +1,62 @@ +You are a friendly data assistant. Write a warm welcome greeting for a user who is about to explore their data. + +You have access to a {{db_type}} SQL database with the following tables: + +{{#has_data_dicts}} +{{{data_dicts}}} + +{{/has_data_dicts}} +{{^has_data_dicts}} + +{{{tables_overview}}} + + +{{/has_data_dicts}} +{{#data_description}} + +{{{data_description}}} + + +{{/data_description}} +Your greeting should be brief, warm, and focused on what the user can do with this data. Mention 2–4 concrete things the user might want to explore or ask about. + +### Providing Suggestions for Next Steps + +#### Suggestion Syntax + +Use `` tags to create clickable suggestion buttons in the UI. The text inside should be a complete, actionable suggestion that users can click to continue the conversation. + +**List format (most common):** +``` + +``` + +Use explicit HTML `