}}
\describe{
+ \item{\code{greeter}}{The QueryChatGreeter controlling greeting generation;
+access its \verb{$tables} and \verb{$prompt}.}
+
\item{\code{system_prompt}}{Get the system prompt.}
\item{\code{data_source}}{Removed. Use \verb{$add_table()} and \verb{$remove_table()} to manage tables.}
@@ -227,7 +230,12 @@ or \code{FALSE} to never clean up automatically.}
Add a table to this QueryChat instance.
\subsection{Usage}{
\if{html}{\out{
}}
- \preformatted{QueryChat$add_table(data_source, table_name, replace = FALSE)}
+ \preformatted{QueryChat$add_table(
+ data_source,
+ table_name,
+ replace = FALSE,
+ include_in_greeting = FALSE
+)}
\if{html}{\out{
}}
}
\subsection{Arguments}{
@@ -237,6 +245,8 @@ or \code{FALSE} to never clean up automatically.}
\item{\code{table_name}}{The SQL table name for this data source.}
\item{\code{replace}}{Whether to replace an existing table with this name.
Default is \code{FALSE}.}
+ \item{\code{include_in_greeting}}{Whether to include this table in the greeting
+context. Default is \code{FALSE}.}
}
\if{html}{\out{
}}
}
@@ -256,7 +266,12 @@ system prompt exactly once after all tables have been staged, avoiding
N-1 spurious intermediate rebuilds.
\subsection{Usage}{
\if{html}{\out{}}
- \preformatted{QueryChat$add_tables(conn, tables = NULL, replace = FALSE)}
+ \preformatted{QueryChat$add_tables(
+ conn,
+ tables = NULL,
+ replace = FALSE,
+ include_in_greeting = FALSE
+)}
\if{html}{\out{
}}
}
\subsection{Arguments}{
@@ -268,6 +283,10 @@ individual data frames or other sources via \verb{$add_table()}.}
by \code{DBI::dbListTables(conn)} are used.}
\item{\code{replace}}{Whether to replace existing tables with the same name.
Default is \code{FALSE}.}
+ \item{\code{include_in_greeting}}{Whether to include added tables in the greeting
+context. \code{TRUE} includes all tables; \code{FALSE} (default) includes none;
+a character vector includes only those named tables (intersected with
+the tables being added). Any other type raises an error.}
}
\if{html}{\out{}}
}
diff --git a/pkg-r/tests/testthat/test-QueryChat.R b/pkg-r/tests/testthat/test-QueryChat.R
index 99313737..ec6922df 100644
--- a/pkg-r/tests/testthat/test-QueryChat.R
+++ b/pkg-r/tests/testthat/test-QueryChat.R
@@ -1039,3 +1039,276 @@ describe("QueryChat$add_tables()", {
expect_length(multi_table_warns, 1L)
})
})
+
+describe("QueryChatGreeter", {
+ skip_if_no_dataframe_engine()
+
+ local_multi_table_conn_greeter <- function(env = parent.frame()) {
+ skip_if_not_installed("RSQLite")
+ conn <- DBI::dbConnect(RSQLite::SQLite(), ":memory:")
+ withr::defer(DBI::dbDisconnect(conn), envir = env)
+ DBI::dbWriteTable(
+ conn,
+ "orders",
+ data.frame(id = 1:2, amount = c(9.99, 4.50))
+ )
+ DBI::dbWriteTable(
+ conn,
+ "customers",
+ data.frame(id = 1:2, name = c("Alice", "Bob"))
+ )
+ conn
+ }
+
+ it("constructor table is always present in greeter$tables", {
+ qc <- local_querychat(new_test_df(), "test_table", greeting = "hi")
+ expect_true("test_table" %in% qc$greeter$tables)
+ })
+
+ it("user-supplied greeting survives construction and mutation of greeter fields", {
+ qc <- local_querychat(
+ new_test_df(),
+ "test_table",
+ greeting = "Preset greeting"
+ )
+ expect_equal(qc$greeting, "Preset greeting")
+
+ qc$greeter$tables <- c("test_table", "other")
+ expect_equal(qc$greeting, "Preset greeting")
+
+ qc$greeter$prompt <- "New prompt text"
+ expect_equal(qc$greeting, "Preset greeting")
+ })
+
+ it("add_table with include_in_greeting = TRUE adds to greeter$tables", {
+ qc <- local_querychat(new_test_df(), "base_table", greeting = "hi")
+ extra <- new_test_df()
+ qc$add_table(extra, "extra_table", include_in_greeting = TRUE)
+ expect_true("extra_table" %in% qc$greeter$tables)
+ })
+
+ it("add_table with default include_in_greeting does NOT add to greeter$tables", {
+ qc <- local_querychat(new_test_df(), "base_table", greeting = "hi")
+ extra <- new_test_df()
+ qc$add_table(extra, "hidden_table")
+ expect_false("hidden_table" %in% qc$greeter$tables)
+ })
+
+ it("add_table with non-logical include_in_greeting errors", {
+ qc <- local_querychat(new_test_df(), "base_table", greeting = "hi")
+ extra <- new_test_df()
+ expect_error(
+ qc$add_table(extra, "extra_table", include_in_greeting = "yes"),
+ "include_in_greeting"
+ )
+ })
+
+ it("add_tables with include_in_greeting = TRUE adds all tables", {
+ conn <- local_multi_table_conn_greeter()
+ qc <- QueryChat$new(NULL, "placeholder", greeting = "hi")
+ suppressWarnings(qc$add_tables(conn, include_in_greeting = TRUE))
+ expect_true("orders" %in% qc$greeter$tables)
+ expect_true("customers" %in% qc$greeter$tables)
+ })
+
+ it("add_tables with include_in_greeting = FALSE adds no tables to greeter", {
+ conn <- local_multi_table_conn_greeter()
+ qc <- QueryChat$new(NULL, "placeholder", greeting = "hi")
+ suppressWarnings(qc$add_tables(conn, include_in_greeting = FALSE))
+ expect_false("orders" %in% qc$greeter$tables)
+ expect_false("customers" %in% qc$greeter$tables)
+ })
+
+ it("add_tables with include_in_greeting as character includes only named subset", {
+ conn <- local_multi_table_conn_greeter()
+ qc <- QueryChat$new(NULL, "placeholder", greeting = "hi")
+ suppressWarnings(qc$add_tables(conn, include_in_greeting = "orders"))
+ expect_true("orders" %in% qc$greeter$tables)
+ expect_false("customers" %in% qc$greeter$tables)
+ })
+
+ it("add_tables with non-logical, non-character include_in_greeting errors", {
+ conn <- local_multi_table_conn_greeter()
+ qc <- QueryChat$new(NULL, "placeholder", greeting = "hi")
+ expect_error(
+ suppressWarnings(qc$add_tables(conn, include_in_greeting = 1)),
+ "include_in_greeting"
+ )
+ })
+
+ it("add_tables leaves state unchanged when include_in_greeting is invalid", {
+ conn <- local_multi_table_conn_greeter()
+ qc <- QueryChat$new(NULL, "placeholder", greeting = "hi")
+ expect_error(
+ suppressWarnings(qc$add_tables(conn, include_in_greeting = 1)),
+ "include_in_greeting"
+ )
+ expect_false("orders" %in% qc$greeter$tables)
+ expect_false("customers" %in% qc$greeter$tables)
+ suppressWarnings(qc$add_tables(conn, include_in_greeting = TRUE))
+ expect_true("orders" %in% qc$greeter$tables)
+ expect_true("customers" %in% qc$greeter$tables)
+ })
+
+ it("greeting prompt omits dicts that describe only excluded tables", {
+ conn <- local_multi_table_conn_greeter()
+ orders_yaml <- withr::local_tempfile(fileext = ".yaml")
+ writeLines(
+ c(
+ "name: orders_dict",
+ "description: ORDERS_DICT_DESC",
+ "tables:",
+ " orders:",
+ " description: Orders info"
+ ),
+ orders_yaml
+ )
+ customers_yaml <- withr::local_tempfile(fileext = ".yaml")
+ writeLines(
+ c(
+ "name: customers_dict",
+ "description: CUSTOMERS_DICT_DESC",
+ "tables:",
+ " customers:",
+ " description: Customers info"
+ ),
+ customers_yaml
+ )
+
+ qc <- QueryChat$new(
+ NULL,
+ "placeholder",
+ greeting = "hi",
+ data_dict = list(orders_yaml, customers_yaml)
+ )
+ qc$add_tables(conn, include_in_greeting = "orders")
+
+ prompt <- qc$.__enclos_env__$private$build_greeting_client()$get_system_prompt()
+ expect_true(grepl("ORDERS_DICT_DESC", prompt))
+ expect_false(grepl("CUSTOMERS_DICT_DESC", prompt))
+ })
+
+ it("greeting prompt keeps a global dict description but drops relationships/glossary", {
+ conn <- local_multi_table_conn_greeter()
+ global_yaml <- withr::local_tempfile(fileext = ".yaml")
+ writeLines(
+ c(
+ "name: domain",
+ "description: GLOBAL_DOMAIN_DESC",
+ "glossary:",
+ " ARR: GLOSSARY_ARR_DEF"
+ ),
+ global_yaml
+ )
+ orders_yaml <- withr::local_tempfile(fileext = ".yaml")
+ writeLines(
+ c(
+ "name: orders_dict",
+ "description: ORDERS_DICT_DESC",
+ "tables:",
+ " orders:",
+ " description: Orders info",
+ "relationships:",
+ " - join: orders.id = customers.id",
+ " description: REL_DESC"
+ ),
+ orders_yaml
+ )
+
+ qc <- QueryChat$new(
+ NULL,
+ "placeholder",
+ greeting = "hi",
+ data_dict = list(global_yaml, orders_yaml)
+ )
+ qc$add_tables(conn, include_in_greeting = "orders")
+
+ prompt <- qc$.__enclos_env__$private$build_greeting_client()$get_system_prompt()
+ expect_true(grepl("GLOBAL_DOMAIN_DESC", prompt))
+ expect_true(grepl("ORDERS_DICT_DESC", prompt))
+ expect_false(grepl("GLOSSARY_ARR_DEF", prompt))
+ expect_false(grepl("REL_DESC", prompt))
+ })
+
+ it("generate_greeting() uses greeting system prompt, writes to qc$greeting, returns text", {
+ client <- mock_ellmer_chat_client(
+ public = list(
+ chat = function(message, ...) {
+ expect_equal(message, GREETING_PROMPT)
+ "Hello from greeting mock!"
+ }
+ )
+ )
+
+ qc <- QueryChat$new(new_test_df(), "test_table", client = client)
+ withr::defer(qc$cleanup())
+
+ greeting_client <- qc$.__enclos_env__$private$build_greeting_client()
+ greeting_system_prompt <- greeting_client$get_system_prompt()
+ expect_false(grepl("querychat_get_schema", greeting_system_prompt))
+ expect_true(grepl("test_table", greeting_system_prompt))
+
+ result <- qc$generate_greeting()
+
+ expect_equal(result, "Hello from greeting mock!")
+ expect_equal(qc$greeting, "Hello from greeting mock!")
+ })
+
+ it("generate_greeting() with empty greeter$tables succeeds without error", {
+ client <- mock_ellmer_chat_client(
+ public = list(
+ chat = function(message, ...) "Generic greeting with no tables."
+ )
+ )
+
+ qc <- QueryChat$new(new_test_df(), "test_table", client = client)
+ withr::defer(qc$cleanup())
+
+ qc$greeter$tables <- character()
+
+ prompt <- qc$.__enclos_env__$private$build_greeting_client()$get_system_prompt()
+ expect_false(grepl("following tables", prompt))
+ expect_false(grepl("SQL SQL", prompt))
+
+ expect_no_error(qc$generate_greeting())
+ expect_equal(qc$greeting, "Generic greeting with no tables.")
+ })
+
+ it("greeting prompt keeps a global dict description with no tables included", {
+ global_yaml <- withr::local_tempfile(fileext = ".yaml")
+ writeLines(
+ c(
+ "name: domain",
+ "description: GLOBAL_DOMAIN_DESC",
+ "glossary:",
+ " ARR: GLOSSARY_ARR_DEF"
+ ),
+ global_yaml
+ )
+
+ qc <- QueryChat$new(
+ new_test_df(),
+ "test_table",
+ greeting = "hi",
+ data_dict = list(global_yaml)
+ )
+ withr::defer(qc$cleanup())
+ qc$greeter$tables <- character()
+
+ prompt <- qc$.__enclos_env__$private$build_greeting_client()$get_system_prompt()
+ expect_true(grepl("GLOBAL_DOMAIN_DESC", prompt))
+ expect_false(grepl("GLOSSARY_ARR_DEF", prompt))
+ expect_false(grepl("following tables", prompt))
+ })
+
+ it("remove_table prunes the table from greeter$tables", {
+ conn <- local_multi_table_conn_greeter()
+ qc <- QueryChat$new(NULL, "placeholder", greeting = "hi")
+ suppressWarnings(qc$add_tables(conn, include_in_greeting = TRUE))
+ expect_true("orders" %in% qc$greeter$tables)
+
+ qc$remove_table("orders")
+ expect_false("orders" %in% qc$greeter$tables)
+ expect_true("customers" %in% qc$greeter$tables)
+ })
+})
diff --git a/pkg-r/vignettes/greet.Rmd b/pkg-r/vignettes/greet.Rmd
index 35649dc5..42a7f575 100644
--- a/pkg-r/vignettes/greet.Rmd
+++ b/pkg-r/vignettes/greet.Rmd
@@ -77,3 +77,37 @@ querychat_app(
greeting = "penguins_greeting.md"
)
```
+
+## Greetings with multiple tables
+
+The generated greeting is *schema-aware*: querychat shares the schema of the
+relevant tables with the model so the opening message can describe the data
+it's about to help you explore. Tables passed to `QueryChat$new()` are included
+in the greeting automatically.
+
+Tables added later with `$add_table()` or `$add_tables()` are **not** included
+by default — pass `include_in_greeting = TRUE` to opt them in:
+
+```{r}
+qc <- QueryChat$new(orders, "orders") # included automatically
+qc$add_table(customers, "customers") # not included by default
+qc$add_table(products, "products", include_in_greeting = TRUE) # opted in
+
+qc$greeter$tables
+#> [1] "orders" "products"
+```
+
+For `$add_tables()`, `include_in_greeting` can also be a character vector
+naming which of the added tables to include:
+
+```{r}
+qc$add_tables(con, include_in_greeting = c("orders", "customers"))
+```
+
+You can also set the included tables directly, or swap in a custom greeting
+template, through `qc$greeter`:
+
+```{r}
+qc$greeter$tables <- c("orders", "customers")
+qc$greeter$prompt <- "my-greeting-template.md"
+```