Skip to content
3 changes: 3 additions & 0 deletions pkg-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED]

### Improvements

* If a `QueryChat` instance is created inside a Shiny app context, the data source is automatically cleaned up when the app session ends. This can be configured manually via the `cleanup` parameter of the `QueryChat` constructor. (#164)


## [0.3.0] - 2025-12-10
Expand Down
24 changes: 24 additions & 0 deletions pkg-py/src/querychat/_querychat.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def __init__(
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
prompt_template: Optional[str | Path] = None,
cleanup: Optional[bool] = None,
):
self._data_source = normalize_data_source(data_source, table_name)

Expand All @@ -57,6 +58,11 @@ def __init__(
prompt_template=prompt_template,
)

# By default, only close the connection automatically if we're in a Shiny app session
if cleanup is None:
cleanup = get_current_session() is not None
self._cleanup = cleanup

# Fork and empty chat now so the per-session forks are fast
client = as_querychat_client(client)
self._client = copy.deepcopy(client)
Expand Down Expand Up @@ -345,6 +351,10 @@ class QueryChat(QueryChatBase):
`data_source.get_schema()`
- `{{data_description}}`: The optional data description provided
- `{{extra_instructions}}`: Any additional instructions provided
cleanup
Whether or not to automatically run `data_source.cleanup()` when the
Shiny session/app stops. By default, cleanup only occurs if the
`QueryChat` instance is created within a Shiny session.

"""

Expand Down Expand Up @@ -421,6 +431,7 @@ def title():
greeting=self.greeting,
client=self._client,
enable_bookmarking=enable_bookmarking,
cleanup=self._cleanup,
)


Expand Down Expand Up @@ -514,6 +525,10 @@ def data_table():
`data_source.get_schema()`
- `{{data_description}}`: The optional data description provided
- `{{extra_instructions}}`: Any additional instructions provided
cleanup
Whether or not to automatically run `data_source.cleanup()` when the
Shiny session/app stops. By default, cleanup only occurs if the
`QueryChat` instance is created within a Shiny session.

"""

Expand All @@ -530,6 +545,7 @@ def __init__(
extra_instructions: Optional[str | Path] = None,
prompt_template: Optional[str | Path] = None,
enable_bookmarking: Literal["auto", True, False] = "auto",
cleanup: Optional[bool] = None,
):
# Sanity check: Express should always have a (stub/real) session
session = get_current_session()
Expand Down Expand Up @@ -563,12 +579,20 @@ def __init__(
else:
enable = enable_bookmarking

if isinstance(session, ExpressStubSession):
cleanup = False
elif cleanup is None:
cleanup = True

self._cleanup = cleanup

self._vals = mod_server(
self.id,
data_source=self._data_source,
greeting=self.greeting,
client=self._client,
enable_bookmarking=enable,
cleanup=cleanup,
)

def df(self) -> pd.DataFrame:
Expand Down
8 changes: 8 additions & 0 deletions pkg-py/src/querychat/_querychat_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def mod_server(
greeting: str | None,
client: chatlas.Chat,
enable_bookmarking: bool,
cleanup: bool = False,
):
# Reactive values to store state
sql = ReactiveStringOrNone(None)
Expand Down Expand Up @@ -187,6 +188,13 @@ def _on_restore(x: RestoreState) -> None:
if "querychat_has_greeted" in vals:
has_greeted.set(vals["querychat_has_greeted"])

if cleanup:
# Clean up the data source when the session ends

@session.on_ended
def _cleanup() -> None:
data_source.cleanup()

return ServerValues(df=filtered_df, sql=sql, title=title, client=chat)


Expand Down
2 changes: 2 additions & 0 deletions pkg-r/NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# querychat (development version)

* `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)

* Tool detail cards can now be expanded or collapsed by default when querychat runs a query or updates the dashboard via the `querychat.tool_details` R option or the `QUERYCHAT_TOOL_DETAILS` environment variable. Valid values are `"expanded"`, `"collapsed"`, or `"default"`. (#137)
Expand Down
12 changes: 10 additions & 2 deletions pkg-r/R/QueryChat.R
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ QueryChat <- R6::R6Class(

# By default, only close automatically if a Shiny session is active
if (is.na(cleanup)) {
cleanup <- !is.null(shiny::getDefaultReactiveDomain())
cleanup <- in_shiny_session()
}

if (cleanup) {
Expand Down Expand Up @@ -668,13 +668,21 @@ querychat_app <- function(
categorical_threshold = 20,
extra_instructions = NULL,
prompt_template = NULL,
cleanup = TRUE,
cleanup = NA,
bookmark_store = "url"
) {
if (is_missing(table_name) && is.data.frame(data_source)) {
table_name <- deparse1(substitute(data_source))
}

check_bool(cleanup, allow_na = TRUE)
if (is.na(cleanup)) {
cleanup <-
is.data.frame(data_source) &&
!in_shiny_session() &&
is_interactive()
}

qc <- QueryChat$new(
data_source = data_source,
table_name = table_name,
Expand Down
3 changes: 3 additions & 0 deletions pkg-r/R/utils-shiny.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
in_shiny_session <- function() {
!is.null(shiny::getDefaultReactiveDomain()) # nocov
}
2 changes: 1 addition & 1 deletion pkg-r/man/querychat-convenience.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion pkg-r/man/querychat-package.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions pkg-r/tests/testthat/test-QueryChat.R
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,39 @@ describe("normalize_data_source()", {
})
})
})

test_that("querychat_app() only cleans up data frame sources on exit", {
local_mocked_r6_class(
QueryChat,
public = list(
initialize = function(..., cleanup) {
# have to use an option because the code is evaluated in a far-away env
options(.test_cleanup = cleanup)
},
app = function(...) {}
)
)
withr::local_options(rlang_interactive = TRUE)

withr::with_options(list(.test_cleanup = NULL), {
test_df <- new_test_df()
querychat_app(test_df)
cleanup_result <- getOption(".test_cleanup")
expect_true(cleanup_result)
})

withr::with_options(list(.test_cleanup = NULL), {
test_ds <- local_data_frame_source(new_test_df())
querychat_app(test_ds)
cleanup_result <- getOption(".test_cleanup")
expect_false(cleanup_result)
})

withr::with_options(list(.test_cleanup = NULL), {
con <- local_sqlite_connection(new_test_df())
test_ds <- DBISource$new(con$conn, "test_table")
querychat_app(test_ds)
cleanup_result <- getOption(".test_cleanup")
expect_false(cleanup_result)
})
})
Loading