From c9206b7633feea0f2302b9e3253df8ca372c9ff5 Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 16 Dec 2025 17:23:48 -0600 Subject: [PATCH 1/8] fix(pkg-r): better support for modules. Closes #169 --- pkg-r/R/QueryChat.R | 52 +++++++++++++++++++++++++++++++++++++----- pkg-r/man/QueryChat.Rd | 20 ++++++++++++---- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index 09199c95..3023339a 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -74,7 +74,7 @@ QueryChat <- R6::R6Class( public = list( #' @field greeting The greeting message displayed to users. greeting = NULL, - #' @field id The module ID for namespacing. + #' @field id ID for the QueryChat instance. id = NULL, #' @field tools The allowed tools for the chat client. tools = c("update", "query"), @@ -470,10 +470,12 @@ QueryChat <- R6::R6Class( #' This method generates a [bslib::sidebar()] component containing the chat #' interface, suitable for use with [bslib::page_sidebar()] or similar layouts. #' + #' @param ... Additional arguments passed to [bslib::sidebar()]. #' @param width Width of the sidebar in pixels. Default is 400. #' @param height Height of the sidebar. Default is "100%". #' @param fillable Whether the sidebar should be fillable. Default is `TRUE`. - #' @param ... Additional arguments passed to [bslib::sidebar()]. + #' @param ns A Shiny namespacing (i.e., [shiny::NS()]) function. + #' Only needed when calling this method within a module UI function. #' #' @return A [bslib::sidebar()] UI component. #' @@ -486,14 +488,20 @@ QueryChat <- R6::R6Class( #' # Main content here #' ) #' } - sidebar = function(width = 400, height = "100%", fillable = TRUE, ...) { + sidebar = function( + ..., + width = 400, + height = "100%", + fillable = TRUE, + ns = NULL + ) { bslib::sidebar( width = width, height = height, fillable = fillable, class = "querychat-sidebar", ..., - self$ui() + self$ui(ns = ns) ) }, @@ -504,6 +512,8 @@ QueryChat <- R6::R6Class( #' `$sidebar()` instead, which wraps this in a sidebar layout. #' #' @param ... Additional arguments passed to [shinychat::chat_ui()]. + #' @param ns A Shiny namespacing (i.e., [shiny::NS()]) function. + #' Only needed when calling this method within a module UI function. #' #' @return A UI component containing the chat interface. #' @@ -515,8 +525,29 @@ QueryChat <- R6::R6Class( #' qc$ui() #' ) #' } - ui = function(...) { - mod_ui(self$id, ...) + ui = function(..., ns = NULL) { + check_function(ns, allow_null = TRUE) + + # If called within another module, the UI id needs to be namespaced + # by that "parent" module. If called in a module *server* context, we + # can infer the namespace from the session, but if not, the user + # will need to provide it. + # NOTE: this isn't a problem for Python since id namespacing is handled implicitly + # by UI functions like shinychat.chat_ui(). + id <- self$id + id <- if (is.null(ns)) namespaced_id(id) else ns(id) + + # Provide a helpful error if the user tries to set id directly + if ("id" %in% names(list2(...))) { + cli::cli_abort( + c( + "Not allowed to set {.arg id} to {.fn $ui()} (or {.fn $sidebar()}).", + "i" = "Use the {.arg ns} argument instead to namespace the UI id." + ) + ) + } + + mod_ui(id, ...) }, #' @description @@ -826,3 +857,12 @@ normalize_data_source <- function(data_source, table_name) { "{.arg data_source} must be a {.cls DataSource}, {.cls data.frame}, or {.cls DBIConnection}, not {.obj_type_friendly {data_source}}." ) } + + +namespaced_id <- function(id, session = shiny::getDefaultReactiveDomain()) { + if (is.null(session)) { + id + } else { + session$ns(id) + } +} \ No newline at end of file diff --git a/pkg-r/man/QueryChat.Rd b/pkg-r/man/QueryChat.Rd index 58236f15..083e214f 100644 --- a/pkg-r/man/QueryChat.Rd +++ b/pkg-r/man/QueryChat.Rd @@ -178,7 +178,7 @@ qc2 <- QueryChat$new(mtcars, greeting = "mtcars_greeting.md") \describe{ \item{\code{greeting}}{The greeting message displayed to users.} -\item{\code{id}}{The module ID for namespacing.} +\item{\code{id}}{ID for the QueryChat instance.} \item{\code{tools}}{The allowed tools for the chat client.} } @@ -484,19 +484,28 @@ 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\%", fillable = TRUE, ...)}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{QueryChat$sidebar( + ..., + width = 400, + height = "100\%", + fillable = TRUE, + ns = NULL +)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ +\item{\code{...}}{Additional arguments passed to \code{\link[bslib:sidebar]{bslib::sidebar()}}.} + \item{\code{width}}{Width of the sidebar in pixels. Default is 400.} \item{\code{height}}{Height of the sidebar. Default is "100\%".} \item{\code{fillable}}{Whether the sidebar should be fillable. Default is \code{TRUE}.} -\item{\code{...}}{Additional arguments passed to \code{\link[bslib:sidebar]{bslib::sidebar()}}.} +\item{\code{ns}}{A Shiny namespacing (i.e., \code{\link[shiny:NS]{shiny::NS()}}) function. +Only needed when calling this method within a module UI function.} } \if{html}{\out{
}} } @@ -528,13 +537,16 @@ 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{
}} +\if{html}{\out{
}}\preformatted{QueryChat$ui(..., ns = NULL)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{...}}{Additional arguments passed to \code{\link[shinychat:chat_ui]{shinychat::chat_ui()}}.} + +\item{\code{ns}}{A Shiny namespacing (i.e., \code{\link[shiny:NS]{shiny::NS()}}) function. +Only needed when calling this method within a module UI function.} } \if{html}{\out{
}} } From fe79fd13201e6c8c36a3cdeeced750bd97d844de Mon Sep 17 00:00:00 2001 From: cpsievert Date: Tue, 16 Dec 2025 23:38:32 +0000 Subject: [PATCH 2/8] `air format` (GitHub Actions) --- 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 3023339a..fe4a2f9f 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -865,4 +865,4 @@ namespaced_id <- function(id, session = shiny::getDefaultReactiveDomain()) { } else { session$ns(id) } -} \ No newline at end of file +} From c34db6971ff9c97044288dbd22bcf25c360b8c61 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 17 Dec 2025 21:35:24 -0500 Subject: [PATCH 3/8] fix(pkg-r): Directly expose `id` rather than ask for `ns()` function (#173) * refactor: Allow directly providing `id` * chore: Also `$sidebar()` and update docstrings --- pkg-r/R/QueryChat.R | 50 +++++++++++++++++++++++------------------- pkg-r/man/QueryChat.Rd | 25 ++++++++++++++++----- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index fe4a2f9f..863e4bd1 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -474,8 +474,10 @@ QueryChat <- R6::R6Class( #' @param width Width of the sidebar in pixels. Default is 400. #' @param height Height of the sidebar. Default is "100%". #' @param fillable Whether the sidebar should be fillable. Default is `TRUE`. - #' @param ns A Shiny namespacing (i.e., [shiny::NS()]) function. - #' Only needed when calling this method within a module UI function. + #' @param id Optional ID for the QueryChat instance. If not provided, + #' will use the ID provided at initialization. If using `$sidebar()` in + #' a Shiny module, you'll need to provide `id = ns("your_id")` where `ns` + #' is the namespacing function from [shiny::NS()]. #' #' @return A [bslib::sidebar()] UI component. #' @@ -493,7 +495,7 @@ QueryChat <- R6::R6Class( width = 400, height = "100%", fillable = TRUE, - ns = NULL + id = NULL ) { bslib::sidebar( width = width, @@ -501,7 +503,7 @@ QueryChat <- R6::R6Class( fillable = fillable, class = "querychat-sidebar", ..., - self$ui(ns = ns) + self$ui(id = id) ) }, @@ -512,8 +514,10 @@ QueryChat <- R6::R6Class( #' `$sidebar()` instead, which wraps this in a sidebar layout. #' #' @param ... Additional arguments passed to [shinychat::chat_ui()]. - #' @param ns A Shiny namespacing (i.e., [shiny::NS()]) function. - #' Only needed when calling this method within a module UI function. + #' @param id Optional ID for the QueryChat instance. If not provided, + #' will use the ID provided at initialization. If using `$ui()` in a Shiny + #' module, you'll need to provide `id = ns("your_id")` where `ns` is the + #' namespacing function from [shiny::NS()]. #' #' @return A UI component containing the chat interface. #' @@ -525,27 +529,16 @@ QueryChat <- R6::R6Class( #' qc$ui() #' ) #' } - ui = function(..., ns = NULL) { - check_function(ns, allow_null = TRUE) + ui = function(..., id = NULL) { + check_string(id, allow_null = TRUE, allow_empty = FALSE) # If called within another module, the UI id needs to be namespaced # by that "parent" module. If called in a module *server* context, we # can infer the namespace from the session, but if not, the user # will need to provide it. - # NOTE: this isn't a problem for Python since id namespacing is handled implicitly - # by UI functions like shinychat.chat_ui(). - id <- self$id - id <- if (is.null(ns)) namespaced_id(id) else ns(id) - - # Provide a helpful error if the user tries to set id directly - if ("id" %in% names(list2(...))) { - cli::cli_abort( - c( - "Not allowed to set {.arg id} to {.fn $ui()} (or {.fn $sidebar()}).", - "i" = "Use the {.arg ns} argument instead to namespace the UI id." - ) - ) - } + # NOTE: this isn't a problem for Python since id namespacing is handled + # implicitly by UI functions like shinychat.chat_ui(). + id <- id %||% namespaced_id(self$id) mod_ui(id, ...) }, @@ -563,6 +556,12 @@ QueryChat <- R6::R6Class( #' with Shiny bookmarks. This requires that the Shiny app has bookmarking #' enabled via `shiny::enableBookmarking()` or the `enableBookmarking` #' parameter of `shiny::shinyApp()`. + #' @param ... Ignored. + #' @param id Optional module ID for the QueryChat instance. If not provided, + #' will use the ID provided at initialization. When used in Shiny modules, + #' this `id` should match the `id` used in the corresponding UI function + #' (i.e., `qc$ui(id = ns("your_id"))` pairs with `qc$server(id = + #' "your_id")`). #' @param session The Shiny session object. #' #' @return A list containing session-specific reactive values and the chat @@ -586,8 +585,13 @@ QueryChat <- R6::R6Class( #' } server = function( enable_bookmarking = FALSE, + ..., + id = NULL, session = shiny::getDefaultReactiveDomain() ) { + check_string(id, allow_null = TRUE, allow_empty = FALSE) + check_dots_empty() + if (is.null(session)) { cli::cli_abort( "{.fn $server} must be called within a Shiny server function" @@ -595,7 +599,7 @@ QueryChat <- R6::R6Class( } mod_server( - self$id, + id %||% self$id, data_source = private$.data_source, greeting = self$greeting, client = self$client, diff --git a/pkg-r/man/QueryChat.Rd b/pkg-r/man/QueryChat.Rd index 083e214f..64f15b0a 100644 --- a/pkg-r/man/QueryChat.Rd +++ b/pkg-r/man/QueryChat.Rd @@ -489,7 +489,7 @@ interface, suitable for use with \code{\link[bslib:page_sidebar]{bslib::page_sid width = 400, height = "100\%", fillable = TRUE, - ns = NULL + id = NULL )}\if{html}{\out{}} } @@ -504,8 +504,10 @@ interface, suitable for use with \code{\link[bslib:page_sidebar]{bslib::page_sid \item{\code{fillable}}{Whether the sidebar should be fillable. Default is \code{TRUE}.} -\item{\code{ns}}{A Shiny namespacing (i.e., \code{\link[shiny:NS]{shiny::NS()}}) function. -Only needed when calling this method within a module UI function.} +\item{\code{id}}{Optional ID for the QueryChat instance. If not provided, +will use the ID provided at initialization. If using \verb{$sidebar()} in +a Shiny module, you'll need to provide \code{id = ns("your_id")} where \code{ns} +is the namespacing function from \code{\link[shiny:NS]{shiny::NS()}}.} } \if{html}{\out{}} } @@ -537,7 +539,7 @@ 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(..., ns = NULL)}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{QueryChat$ui(..., id = NULL)}\if{html}{\out{
}} } \subsection{Arguments}{ @@ -545,8 +547,10 @@ This method generates the chat UI component. Typically you'll use \describe{ \item{\code{...}}{Additional arguments passed to \code{\link[shinychat:chat_ui]{shinychat::chat_ui()}}.} -\item{\code{ns}}{A Shiny namespacing (i.e., \code{\link[shiny:NS]{shiny::NS()}}) function. -Only needed when calling this method within a module UI function.} +\item{\code{id}}{Optional ID for the QueryChat instance. If not provided, +will use the ID provided at initialization. If using \verb{$ui()} in a Shiny +module, you'll need to provide \code{id = ns("your_id")} where \code{ns} is the +namespacing function from \code{\link[shiny:NS]{shiny::NS()}}.} } \if{html}{\out{}} } @@ -580,6 +584,8 @@ reactive values. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{QueryChat$server( enable_bookmarking = FALSE, + ..., + id = NULL, session = shiny::getDefaultReactiveDomain() )}\if{html}{\out{
}} } @@ -594,6 +600,13 @@ with Shiny bookmarks. This requires that the Shiny app has bookmarking enabled via \code{shiny::enableBookmarking()} or the \code{enableBookmarking} parameter of \code{shiny::shinyApp()}.} +\item{\code{...}}{Ignored.} + +\item{\code{id}}{Optional module ID for the QueryChat instance. If not provided, +will use the ID provided at initialization. When used in Shiny modules, +this \code{id} should match the \code{id} used in the corresponding UI function +(i.e., \code{qc$ui(id = ns("your_id"))} pairs with \code{qc$server(id = "your_id")}).} + \item{\code{session}}{The Shiny session object.} } \if{html}{\out{}} From 5e5f8798b56f527dd2ad270c7f3458d411191631 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 18 Dec 2025 11:52:30 -0500 Subject: [PATCH 4/8] chore(pkg-r): Add example app with a module --- .../examples-shiny/03-module-app/README.md | 60 ++++++++++ pkg-r/inst/examples-shiny/03-module-app/app.R | 110 ++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 pkg-r/inst/examples-shiny/03-module-app/README.md create mode 100644 pkg-r/inst/examples-shiny/03-module-app/app.R diff --git a/pkg-r/inst/examples-shiny/03-module-app/README.md b/pkg-r/inst/examples-shiny/03-module-app/README.md new file mode 100644 index 00000000..a30243b5 --- /dev/null +++ b/pkg-r/inst/examples-shiny/03-module-app/README.md @@ -0,0 +1,60 @@ +# QueryChat Modules Example + +This example demonstrates how to use QueryChat within Shiny modules, following standard Shiny module patterns. + +## Key Concepts + +### Module UI Function + +In a Shiny module UI function, you wrap the QueryChat ID with the namespace function `ns()`: + +```r +module_ui <- function(id) { + ns <- NS(id) + card( + qc$sidebar(id = ns("qc-ui")) # Wrap ID with ns() + ) +} +``` + +### Module Server Function + +In the corresponding server function, you pass the **unwrapped** ID to `qc$server()`: + +```r +module_server <- function(id) { + moduleServer(id, function(input, output, session) { + qc_vals <- qc$server(id = "qc-ui") # Use unwrapped ID + # ... rest of server logic + }) +} +``` + +## Why This Pattern? + +This follows the established Shiny module pattern where: + +1. **UI functions** namespace all IDs using `ns()` to avoid conflicts when multiple instances exist +2. **Server functions** receive the unwrapped ID and use it to connect to the corresponding UI + +This is the same pattern used for any Shiny component in a module, and QueryChat now supports it seamlessly. + +## Benefits + +- **Multiple instances**: You can have multiple QueryChat explorers in the same app +- **Familiar pattern**: Uses standard Shiny module conventions +- **Clean isolation**: Each module instance has its own reactive state + +## Running the Example + +From the R console: + +```r +shiny::runApp(system.file("examples-shiny/03-module-app", package = "querychat")) +``` + +Or navigate to this directory and run: + +```bash +Rscript app.R +``` diff --git a/pkg-r/inst/examples-shiny/03-module-app/app.R b/pkg-r/inst/examples-shiny/03-module-app/app.R new file mode 100644 index 00000000..45019335 --- /dev/null +++ b/pkg-r/inst/examples-shiny/03-module-app/app.R @@ -0,0 +1,110 @@ +library(shiny) +library(bslib) +library(querychat) +library(palmerpenguins) + +# Define a custom greeting for the penguins app +greeting <- r"( +# Welcome to the Palmer Penguins Explorer! 🐧 + +I can help you explore and analyze the Palmer Penguins dataset. Ask me questions +about the penguins, and I'll generate SQL queries to get the answers. + +Try asking: +- Show me the first 10 rows of the penguins dataset +- What's the average bill length by species? +- Which species has the largest body mass? +)" + +# Create QueryChat object with custom options +qc <- QueryChat$new( + penguins, + greeting = greeting, + data_description = paste( + "The Palmer Penguins dataset contains measurements of bill", + "dimensions, flipper length, body mass, sex, and species", + "(Adelie, Chinstrap, and Gentoo) collected from three islands in", + "the Palmer Archipelago, Antarctica." + ) +) + +# Module UI function +# This demonstrates the standard Shiny module pattern where: +# - The module ID is wrapped with ns() in the UI function +# - The same ID (unwrapped) is used in the corresponding server function +module_ui <- function(id) { + ns <- NS(id) + layout_sidebar( + sidebar = qc$sidebar(id = ns("qc-ui")), # Pass namespaced ID to QueryChat + padding = 0, + navset_card_tab( + title = "Data Explorer", + nav_panel( + "Data View", + DT::DTOutput(ns("data_table")) + ), + nav_panel( + "SQL Query", + verbatimTextOutput(ns("sql_query")) + ) + ) + ) +} + +# Module server function +module_server <- function(id) { + moduleServer(id, function(input, output, session) { + # Initialize QueryChat server with the same ID (unwrapped) + # This connects to the UI initialized with id = ns("qc-ui") + qc_vals <- qc$server(id = "qc-ui") + + # Render the data table + output$data_table <- DT::renderDT( + { + qc_vals$df() + }, + fillContainer = TRUE, + options = list(pageLength = 25, scrollX = TRUE) + ) + + # Render the SQL query + output$sql_query <- renderText({ + query <- qc_vals$sql() + if (is.null(query) || !nzchar(query)) { + "No filter applied - showing all data" + } else { + query + } + }) + }) +} + +# Define UI with multiple module instances +ui <- page_sidebar( + title = "QueryChat Modules Example", + sidebar = sidebar( + "This example demonstrates using QueryChat within Shiny modules.", + markdown( + "Each module instance has its own QueryChat sidebar and data explorer. + + **UI:** `qc$sidebar(id = ns(\"qc-ui\"))` wraps the ID with the namespace function + + **Server:** `qc$server(id = \"qc-ui\")` uses the unwrapped ID" + ) + ), + class = "p-0", + navset_card_underline( + wrapper = \(...) card_body(..., padding = 0, border_radius = 0), + nav_panel("Explorer 1", module_ui("module1")), + nav_panel("Explorer 2", module_ui("module2")) + ) +) + +# Define server logic +server <- function(input, output, session) { + # Initialize both module instances + module_server("module1") + module_server("module2") +} + +shinyApp(ui = ui, server = server) From c5728b33f3bf4277c52ef474592738debc5e960f Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 18 Dec 2025 12:03:28 -0500 Subject: [PATCH 5/8] feat: Prepend "querychat_" to automatic `id` --- pkg-py/src/querychat/_querychat.py | 2 +- pkg-py/tests/test_client_console.py | 2 +- pkg-py/tests/test_querychat.py | 2 +- pkg-r/R/QueryChat.R | 2 +- pkg-r/tests/testthat/test-QueryChat.R | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg-py/src/querychat/_querychat.py b/pkg-py/src/querychat/_querychat.py index 8a7469e4..b599cb2b 100644 --- a/pkg-py/src/querychat/_querychat.py +++ b/pkg-py/src/querychat/_querychat.py @@ -58,7 +58,7 @@ def __init__( "Table name must begin with a letter and contain only letters, numbers, and underscores", ) - self.id = id or table_name + self.id = id or f"querychat_{table_name}" self.tools = normalize_tools(tools, default=("update", "query")) self.greeting = greeting.read_text() if isinstance(greeting, Path) else greeting diff --git a/pkg-py/tests/test_client_console.py b/pkg-py/tests/test_client_console.py index 06eaa895..a2948366 100644 --- a/pkg-py/tests/test_client_console.py +++ b/pkg-py/tests/test_client_console.py @@ -291,5 +291,5 @@ def test_existing_initialization_still_works(self, sample_df): ) assert qc is not None - assert qc.id == "test_table" + assert qc.id == "querychat_test_table" assert qc.tools == ("update", "query") diff --git a/pkg-py/tests/test_querychat.py b/pkg-py/tests/test_querychat.py index ebf5ec57..22dab6d7 100644 --- a/pkg-py/tests/test_querychat.py +++ b/pkg-py/tests/test_querychat.py @@ -39,7 +39,7 @@ def test_querychat_init(sample_df): # Verify basic attributes are set assert qc is not None - assert qc.id == "test_table" + assert qc.id == "querychat_test_table" # Even without server initialization, we should be able to query the data source result = qc.data_source.execute_query( diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index 863e4bd1..cf8c7483 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -178,7 +178,7 @@ QueryChat <- R6::R6Class( private$.data_source <- normalize_data_source(data_source, table_name) - self$id <- id %||% table_name + self$id <- id %||% sprintf("querychat_%s", table_name) self$tools <- tools if (!is.null(greeting) && file.exists(greeting)) { diff --git a/pkg-r/tests/testthat/test-QueryChat.R b/pkg-r/tests/testthat/test-QueryChat.R index 9db979de..5c0f39c5 100644 --- a/pkg-r/tests/testthat/test-QueryChat.R +++ b/pkg-r/tests/testthat/test-QueryChat.R @@ -44,7 +44,7 @@ describe("QueryChat$new()", { withr::defer(qc$cleanup()) expect_equal(qc$data_source$table_name, "my_data") - expect_equal(qc$id, "my_data") + expect_equal(qc$id, "querychat_my_data") }) it("loads greeting from file if file exists", { From 0b2452ff1eef74dca83c8528d6efa69a1505bfb8 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 18 Dec 2025 12:21:05 -0500 Subject: [PATCH 6/8] feat(pkg-py): Expose `id` in `.ui`, `.sidebar` and `.server` methods --- pkg-py/src/querychat/_querychat.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/pkg-py/src/querychat/_querychat.py b/pkg-py/src/querychat/_querychat.py index b599cb2b..3a83b090 100644 --- a/pkg-py/src/querychat/_querychat.py +++ b/pkg-py/src/querychat/_querychat.py @@ -188,6 +188,7 @@ def sidebar( width: int = 400, height: str = "100%", fillable: bool = True, + id: Optional[str] = None, **kwargs, ) -> ui.Sidebar: """ @@ -201,6 +202,9 @@ def sidebar( Height of the sidebar. fillable Whether the sidebar should be fillable. Default is `True`. + id + Optional ID for the QueryChat instance. If not provided, + will use the ID provided at initialization. **kwargs Additional arguments passed to `shiny.ui.sidebar()`. @@ -211,7 +215,7 @@ def sidebar( """ return ui.sidebar( - self.ui(), + self.ui(id=id), width=width, height=height, fillable=fillable, @@ -219,12 +223,15 @@ def sidebar( **kwargs, ) - def ui(self, **kwargs): + def ui(self, *, id: Optional[str] = None, **kwargs): """ Create the UI for the querychat component. Parameters ---------- + id + Optional ID for the QueryChat instance. If not provided, + will use the ID provided at initialization. **kwargs Additional arguments to pass to `shinychat.chat_ui()`. @@ -234,7 +241,7 @@ def ui(self, **kwargs): A UI component. """ - return mod_ui(self.id, **kwargs) + return mod_ui(id or self.id, **kwargs) def generate_greeting(self, *, echo: Literal["none", "output"] = "none"): """ @@ -561,7 +568,9 @@ class QueryChat(QueryChatBase): """ - def server(self, *, enable_bookmarking: bool = False) -> ServerValues: + def server( + self, *, enable_bookmarking: bool = False, id: Optional[str] = None + ) -> ServerValues: """ Initialize Shiny server logic. @@ -574,6 +583,10 @@ def server(self, *, enable_bookmarking: bool = False) -> ServerValues: ---------- enable_bookmarking Whether to enable bookmarking for the querychat module. + id + Optional module ID for the QueryChat instance. If not provided, + will use the ID provided at initialization. This must match the ID + used in the `.ui()` or `.sidebar()` methods. Examples -------- @@ -629,7 +642,7 @@ def title(): ) return mod_server( - self.id, + id or self.id, data_source=self._data_source, greeting=self.greeting, client=self.client, From f8de8436be57599cbdb6601c890076f64642afd5 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 18 Dec 2025 12:25:12 -0500 Subject: [PATCH 7/8] chore: Add NEWS/CHANGELOG items --- pkg-py/CHANGELOG.md | 2 ++ pkg-r/NEWS.md | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg-py/CHANGELOG.md b/pkg-py/CHANGELOG.md index 9b600116..56a2fd92 100644 --- a/pkg-py/CHANGELOG.md +++ b/pkg-py/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New features +* `QueryChat.sidebar()`, `QueryChat.ui()`, and `QueryChat.server()` now support an optional `id` parameter to enable use within Shiny modules or to create multiple chat instances from a single `QueryChat` object. (#172) + * `QueryChat.client()` can now create standalone querychat-enabled chat clients with configurable tools and callbacks, enabling use outside of Shiny applications. (#168) * `QueryChat.console()` was added to launch interactive console-based chat sessions with your data source, with persistent conversation state across invocations. (#168) diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md index 64a8f6a3..144a3f29 100644 --- a/pkg-r/NEWS.md +++ b/pkg-r/NEWS.md @@ -1,11 +1,13 @@ # querychat (development version) +* `QueryChat$sidebar()`, `QueryChat$ui()`, and `QueryChat$server()` now support an optional `id` parameter to enable use within Shiny modules. When used in a module UI function, pass `id = ns("your_id")` where `ns` is the namespacing function from `shiny::NS()`. In the corresponding module server function, pass the unwrapped ID to `QueryChat$server(id = "your_id")`. This enables multiple independent QueryChat instances from the same QueryChat object. (#172) + * `QueryChat$client()` can now create standalone querychat-enabled chat clients with configurable tools and callbacks, enabling use outside of Shiny applications. (#168) * `QueryChat$console()` was added to launch interactive console-based chat sessions with your data source, with persistent conversation state across invocations. (#168) * The tools used in a `QueryChat` chatbot are now configurable. Use the new `tools` parameter of `querychat()` or `QueryChat$new()` to select either or both `"query"` or `"update"` tools. Choose `tools = "update"` if you only want QueryChat to be able to update the dashboard (useful when you want to be 100% certain that the LLM will not see _any_ raw data). (#168) - + * `querychat_app()` will now only automatically clean up the data source if QueryChat creates the data source internally from a data frame. (#164) * **Breaking change:** The `$sql()` method now returns `NULL` instead of `""` (empty string) when no query has been set, aligning with the behavior of `$title()` for consistency. Most code using `isTruthy()` or similar falsy checks will continue working without changes. Code that explicitly checks `sql() == ""` should be updated to use falsy checks (e.g., `!isTruthy(sql())`) or explicit null checks (`is.null(sql())`). (#146) From 1a3ad9d63d9fa64592dfbe819785dc4f28eb6d71 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 18 Dec 2025 13:20:02 -0500 Subject: [PATCH 8/8] Update pkg-py/CHANGELOG.md Co-authored-by: Carson Sievert --- pkg-py/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg-py/CHANGELOG.md b/pkg-py/CHANGELOG.md index 56a2fd92..4d6e38f4 100644 --- a/pkg-py/CHANGELOG.md +++ b/pkg-py/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New features -* `QueryChat.sidebar()`, `QueryChat.ui()`, and `QueryChat.server()` now support an optional `id` parameter to enable use within Shiny modules or to create multiple chat instances from a single `QueryChat` object. (#172) +* `QueryChat.sidebar()`, `QueryChat.ui()`, and `QueryChat.server()` now support an optional `id` parameter to create multiple chat instances from a single `QueryChat` object. (#172) * `QueryChat.client()` can now create standalone querychat-enabled chat clients with configurable tools and callbacks, enabling use outside of Shiny applications. (#168)