diff --git a/pkg-py/src/querychat/_dash.py b/pkg-py/src/querychat/_dash.py index a4202690..c3052178 100644 --- a/pkg-py/src/querychat/_dash.py +++ b/pkg-py/src/querychat/_dash.py @@ -18,7 +18,7 @@ stream_response_async, ) from ._ui_assets import DASH_CSS, DASH_JS, SUGGESTION_CSS -from ._utils import as_narwhals +from ._utils import as_narwhals, maybe_truncate if TYPE_CHECKING: from collections.abc import Callable @@ -207,10 +207,17 @@ def store_id(self) -> str: """ return self._ids.store - def app(self) -> dash.Dash: + def app(self, *, max_rows: Optional[int] = 1000) -> dash.Dash: """ Create a complete Dash app. + Parameters + ---------- + max_rows + Maximum number of rows to display in the data table. This does not + affect the number of rows that the LLM can query against. Default + is 1000. Set to ``None`` to disable row limit. + Returns ------- dash.Dash @@ -237,6 +244,7 @@ def app(self) -> dash.Dash: self._ids, data_source.table_name, self._deserialize_state, + max_rows=max_rows, ) return app @@ -425,6 +433,8 @@ def register_app_callbacks( ids: IDs, table_name: str, deserialize_state: Callable[[AppStateDict], AppState], + *, + max_rows: int | None = None, ) -> None: """Register callbacks for SQL display, data table, and export.""" from dash.dcc.express import send_data_frame @@ -459,17 +469,16 @@ def update_display(state_data: AppStateDict, reset_clicks): sql_title = state.title or "SQL Query" sql_code = f"```sql\n{state.get_display_sql()}\n```" - nw_df = as_narwhals(state.get_current_data()) - nrow, ncol = nw_df.shape + result = maybe_truncate(state.get_current_data(), max_rows) - display_df = nw_df.to_pandas() + display_df = result.df.to_pandas() table_data = display_df.to_dict("records") table_columns = [{"field": col} for col in display_df.columns] data_info_parts = [] if state.error: data_info_parts.append(f"Warning: {state.error}") - data_info_parts.append(f"Data has {nrow} rows and {ncol} columns.") + data_info_parts.append(result.info_message) data_info = " ".join(data_info_parts) return ( diff --git a/pkg-py/src/querychat/_gradio.py b/pkg-py/src/querychat/_gradio.py index c0f3518d..b3b2a763 100644 --- a/pkg-py/src/querychat/_gradio.py +++ b/pkg-py/src/querychat/_gradio.py @@ -19,7 +19,7 @@ stream_response, ) from ._ui_assets import GRADIO_CSS, GRADIO_JS, SUGGESTION_CSS -from ._utils import as_narwhals +from ._utils import maybe_truncate if TYPE_CHECKING: from pathlib import Path @@ -317,10 +317,17 @@ def submit_message(message: str, state_dict: AppStateDict): return state_holder - def app(self) -> GradioBlocksWrapper: + def app(self, *, max_rows: Optional[int] = 1000) -> GradioBlocksWrapper: """ Create a complete Gradio app. + Parameters + ---------- + max_rows + Maximum number of rows to display in the data table. This does not + affect the number of rows that the LLM can query against. Default + is 1000. Set to ``None`` to disable row limit. + Returns ------- GradioBlocksWrapper @@ -379,14 +386,13 @@ def update_displays(state_dict: AppStateDict): ) df = self.df(state_dict) - nw_df = as_narwhals(df) - nrow, ncol = nw_df.shape - native_df = nw_df.to_native() + result = maybe_truncate(df, max_rows) + native_df = result.df.to_native() data_info_parts = [] if error: data_info_parts.append(f"⚠️ {error}") - data_info_parts.append(f"*Data has {nrow} rows and {ncol} columns.*") + data_info_parts.append(f"*{result.info_message}*") data_info_text = " ".join(data_info_parts) return sql_title_text, sql_code, native_df, data_info_text diff --git a/pkg-py/src/querychat/_shiny.py b/pkg-py/src/querychat/_shiny.py index f915e79f..9e3dd825 100644 --- a/pkg-py/src/querychat/_shiny.py +++ b/pkg-py/src/querychat/_shiny.py @@ -12,7 +12,7 @@ from ._icons import bs_icon from ._querychat_base import DEFAULT_TOOLS, TOOL_GROUPS, QueryChatBase from ._shiny_module import ServerValues, mod_server, mod_ui -from ._utils import MISSING, MISSING_TYPE, as_narwhals +from ._utils import MISSING, MISSING_TYPE, maybe_truncate from ._viz_utils import has_viz_tool if TYPE_CHECKING: @@ -242,7 +242,10 @@ def __init__( self.id = id or f"querychat_{table_name}" def app( - self, *, bookmark_store: Literal["url", "server", "disable"] = "url" + self, + *, + max_rows: Optional[int] = 1000, + bookmark_store: Literal["url", "server", "disable"] = "url", ) -> App: """ Quickly chat with a dataset. @@ -252,6 +255,10 @@ def app( Parameters ---------- + max_rows + Maximum number of rows to display in the data table. This does not + affect the number of rows that the LLM can query against. Default + is 1000. Set to ``None`` to disable row limit. bookmark_store The bookmarking store to use for the Shiny app. Options are: - `"url"`: Store bookmarks in the URL (default). @@ -290,6 +297,10 @@ def app_ui(request): ui.card( ui.card_header(bs_icon("table"), " Data"), ui.output_data_frame("dt"), + ui.card_footer( + ui.output_text("data_info"), + class_="text-muted small", + ), ), title=ui.span("querychat with ", ui.code(table_name)), class_="bslib-page-dashboard", @@ -325,10 +336,17 @@ def _(): vals.sql.set(None) vals.title.set(None) + @reactive.calc + def truncated(): + return maybe_truncate(vals.df(), max_rows) + @render.data_frame def dt(): - # Collect lazy sources (LazyFrame, Ibis Table) to eager DataFrame - return as_narwhals(vals.df()) + return truncated().df + + @render.text + def data_info(): + return truncated().info_message @render.ui def sql_output(): diff --git a/pkg-py/src/querychat/_streamlit.py b/pkg-py/src/querychat/_streamlit.py index 484b8f4a..3b8e78d5 100644 --- a/pkg-py/src/querychat/_streamlit.py +++ b/pkg-py/src/querychat/_streamlit.py @@ -14,7 +14,7 @@ stream_response, ) from ._ui_assets import STREAMLIT_JS, SUGGESTION_CSS -from ._utils import as_narwhals +from ._utils import maybe_truncate if TYPE_CHECKING: from pathlib import Path @@ -175,12 +175,20 @@ def _get_state(self) -> AppState: ) return st.session_state[self._state_key] - def app(self) -> None: + def app(self, *, max_rows: Optional[int] = 1000) -> None: """ Render a complete Streamlit app. Configures the page, renders chat in sidebar, and displays SQL query and data table in the main area. + + Parameters + ---------- + max_rows + Maximum number of rows to display in the data table. This does not + affect the number of rows that the LLM can query against. Default + is 1000. Set to ``None`` to disable row limit. + """ data_source = self._require_data_source("app") import streamlit as st @@ -192,7 +200,7 @@ def app(self) -> None: ) self.sidebar() - self._render_main_content() + self._render_main_content(max_rows=max_rows) def sidebar(self) -> None: """Render the chat interface in the Streamlit sidebar.""" @@ -303,7 +311,7 @@ def reset(self) -> None: state.reset_dashboard() st.rerun() - def _render_main_content(self) -> None: + def _render_main_content(self, *, max_rows: Optional[int] = None) -> None: """Render the main content area (SQL + data table).""" data_source = self._require_data_source("_render_main_content") import streamlit as st @@ -324,10 +332,13 @@ def _render_main_content(self) -> None: st.rerun() st.subheader("Data view") - df = as_narwhals(state.get_current_data()) if state.error: st.error(state.error) + result = maybe_truncate(state.get_current_data(), max_rows) st.dataframe( - df.to_native(), use_container_width=True, height=400, hide_index=True + result.df.to_native(), + use_container_width=True, + height=400, + hide_index=True, ) - st.caption(f"Data has {df.shape[0]} rows and {df.shape[1]} columns.") + st.caption(result.info_message) diff --git a/pkg-py/src/querychat/_utils.py b/pkg-py/src/querychat/_utils.py index c08ed93e..1920eae0 100644 --- a/pkg-py/src/querychat/_utils.py +++ b/pkg-py/src/querychat/_utils.py @@ -4,6 +4,7 @@ import re import warnings from contextlib import contextmanager +from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, Optional, overload @@ -354,3 +355,68 @@ def read_prompt_template(filename: str, **kwargs: object) -> str: template_path = Path(__file__).parent / "prompts" / filename template = template_path.read_text() return chevron.render(template, kwargs) + + +@dataclass +class TruncationResult: + """Result of maybe_truncate(), holding the (possibly truncated) DataFrame and metadata.""" + + df: nw.DataFrame + total_rows: int + total_cols: int + truncated: bool + + @property + def info_message(self) -> str: + """User-facing message describing the data dimensions and any truncation.""" + if self.truncated: + return f"Showing first {len(self.df)} of {self.total_rows} rows ({self.total_cols} columns)." + return f"Data has {self.total_rows} rows and {self.total_cols} columns." + + +def maybe_truncate( + df: Any, + max_rows: int | None, + *, + warn: bool = True, +) -> TruncationResult: + """ + Collect and optionally truncate data for display. + + Accepts any type that :func:`as_narwhals` understands (native DataFrame, + narwhals frame, Polars LazyFrame, Ibis Table). For lazy sources, truncation + is applied before collection so the backend only transfers *max_rows* rows. + + Parameters + ---------- + df + Raw data from a data source. + max_rows + Maximum rows to display. ``None`` disables truncation. + warn + If True and truncation occurs, emit a warning for the developer. + + """ + if max_rows is None: + nw_df = as_narwhals(df) + total_rows, total_cols = nw_df.shape + else: + nw_lazy = as_narwhals(df, lazy=True) + total_rows = int(nw_lazy.select(nw.len()).collect().item()) + total_cols = len(nw_lazy.collect_schema()) + nw_df = nw_lazy.head(max_rows).collect() if total_rows > max_rows else nw_lazy.collect() + + truncated = max_rows is not None and total_rows > max_rows + if truncated and warn: + warnings.warn( + f"querychat: Displaying {max_rows} of {total_rows} rows. " + "Set `max_rows` to increase or `None` to disable.", + stacklevel=2, + ) + + return TruncationResult( + df=nw_df, + total_rows=int(total_rows), + total_cols=int(total_cols), + truncated=truncated, + ) diff --git a/pkg-py/tests/test_maybe_truncate.py b/pkg-py/tests/test_maybe_truncate.py new file mode 100644 index 00000000..52076c0e --- /dev/null +++ b/pkg-py/tests/test_maybe_truncate.py @@ -0,0 +1,121 @@ +"""Tests for maybe_truncate helper.""" + +import warnings + +import narwhals.stable.v1 as nw +import pandas as pd +import polars as pl +import pytest +from querychat._utils import maybe_truncate + + +@pytest.fixture +def large_df(): + return nw.from_native(pd.DataFrame({"x": range(200), "y": range(200)})) + + +@pytest.fixture +def small_df(): + return nw.from_native(pd.DataFrame({"x": range(5), "y": range(5)})) + + +class TestMaybeTruncateEager: + def test_truncates_when_exceeds_max(self, large_df): + result = maybe_truncate(large_df, max_rows=50) + assert len(result.df) == 50 + assert result.total_rows == 200 + assert result.total_cols == 2 + assert result.truncated is True + + def test_no_truncation_when_under_max(self, small_df): + result = maybe_truncate(small_df, max_rows=50) + assert len(result.df) == 5 + assert result.total_rows == 5 + assert result.truncated is False + + def test_no_truncation_when_max_is_none(self, large_df): + result = maybe_truncate(large_df, max_rows=None) + assert len(result.df) == 200 + assert result.truncated is False + + def test_no_truncation_when_exactly_at_max(self, large_df): + result = maybe_truncate(large_df, max_rows=200) + assert len(result.df) == 200 + assert result.truncated is False + + def test_info_message_when_truncated(self, large_df): + result = maybe_truncate(large_df, max_rows=50) + assert result.info_message == "Showing first 50 of 200 rows (2 columns)." + + def test_info_message_when_not_truncated(self, small_df): + result = maybe_truncate(small_df, max_rows=50) + assert result.info_message == "Data has 5 rows and 2 columns." + + def test_emits_warning_when_truncated(self, large_df): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + maybe_truncate(large_df, max_rows=50) + assert len(w) == 1 + assert "Displaying 50 of 200 rows" in str(w[0].message) + assert "max_rows" in str(w[0].message) + + def test_no_warning_when_not_truncated(self, small_df): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + maybe_truncate(small_df, max_rows=50) + assert len(w) == 0 + + def test_no_warning_when_warn_false(self, large_df): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + maybe_truncate(large_df, max_rows=50, warn=False) + assert len(w) == 0 + + def test_accepts_native_pandas_df(self): + df = pd.DataFrame({"x": range(100), "y": range(100)}) + result = maybe_truncate(df, max_rows=10) + assert len(result.df) == 10 + assert result.total_rows == 100 + assert result.truncated is True + + +class TestMaybeTruncateLazy: + """Test lazy-aware truncation with Polars LazyFrame.""" + + def test_truncates_lazyframe(self): + lf = pl.LazyFrame({"x": range(200), "y": range(200)}) + result = maybe_truncate(lf, max_rows=50) + assert len(result.df) == 50 + assert result.total_rows == 200 + assert result.total_cols == 2 + assert result.truncated is True + assert isinstance(result.df, nw.DataFrame) + + def test_no_truncation_lazyframe(self): + lf = pl.LazyFrame({"x": range(5), "y": range(5)}) + result = maybe_truncate(lf, max_rows=50) + assert len(result.df) == 5 + assert result.total_rows == 5 + assert result.truncated is False + assert isinstance(result.df, nw.DataFrame) + + def test_none_max_rows_lazyframe(self): + lf = pl.LazyFrame({"x": range(200), "y": range(200)}) + result = maybe_truncate(lf, max_rows=None) + assert len(result.df) == 200 + assert result.truncated is False + + def test_warning_with_lazyframe(self): + lf = pl.LazyFrame({"x": range(200), "y": range(200)}) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + maybe_truncate(lf, max_rows=50) + assert len(w) == 1 + assert "Displaying 50 of 200 rows" in str(w[0].message) + + def test_native_polars_eager_df(self): + df = pl.DataFrame({"x": range(100), "y": range(100)}) + result = maybe_truncate(df, max_rows=10) + assert len(result.df) == 10 + assert result.total_rows == 100 + assert result.truncated is True diff --git a/pkg-r/DESCRIPTION b/pkg-r/DESCRIPTION index 532a055e..7b37530d 100644 --- a/pkg-r/DESCRIPTION +++ b/pkg-r/DESCRIPTION @@ -51,8 +51,8 @@ Suggests: withr VignetteBuilder: knitr +Config/roxygen2/version: 8.0.0 Config/testthat/edition: 3 Config/testthat/parallel: true Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.3 diff --git a/pkg-r/R/QueryChat.R b/pkg-r/R/QueryChat.R index a471c4dd..489c404f 100644 --- a/pkg-r/R/QueryChat.R +++ b/pkg-r/R/QueryChat.R @@ -376,6 +376,9 @@ QueryChat <- R6::R6Class( #' ``` #' #' @param ... Arguments passed to `$app_obj()`. + #' @param max_rows Maximum number of rows to display in the data table. + #' This does not affect the number of rows that the LLM can query + #' against. Default is 1000. Set to `NULL` to disable row limit. #' @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"`. @@ -385,8 +388,12 @@ QueryChat <- R6::R6Class( #' - `sql`: The final SQL query string #' - `title`: The final title #' - `client`: The session-specific chat client instance - app = function(..., bookmark_store = "url") { - app <- self$app_obj(..., bookmark_store = bookmark_store) + app = function(..., max_rows = 1000L, bookmark_store = "url") { + app <- self$app_obj( + ..., + max_rows = max_rows, + bookmark_store = bookmark_store + ) vals <- tryCatch(shiny::runGadget(app), interrupt = function(cnd) NULL) invisible(vals) }, @@ -409,16 +416,20 @@ QueryChat <- R6::R6Class( #' ``` #' #' @param ... Additional arguments (currently unused). + #' @param max_rows Maximum number of rows to display in the data table. + #' This does not affect the number of rows that the LLM can query + #' against. Default is 1000. Set to `NULL` to disable row limit. #' @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()`. - app_obj = function(..., bookmark_store = "url") { + app_obj = function(..., max_rows = 1000L, bookmark_store = "url") { private$require_data_source("$app_obj") check_installed("DT") check_installed("bsicons") check_dots_empty() + check_number_whole(max_rows, min = 1, allow_null = TRUE) table_name <- private$.data_source$table_name @@ -454,7 +465,11 @@ QueryChat <- R6::R6Class( bslib::card( full_screen = TRUE, bslib::card_header(bsicons::bs_icon("table"), "Data"), - DT::DTOutput("dt") + DT::DTOutput("dt"), + bslib::card_footer( + class = "text-muted small", + shiny::textOutput("data_info") + ) ), shiny::actionButton( "close_btn", @@ -493,20 +508,22 @@ QueryChat <- R6::R6Class( qc_vals$title(NULL) }) - output$dt <- DT::renderDT({ - df <- qc_vals$df() - if (inherits(df, "tbl_sql")) { - # Materialize the query for DT, {dplyr} guaranteed by TblSqlSource - df <- dplyr::collect(df) - } + truncated_df <- shiny::reactive({ + maybe_truncate(qc_vals$df(), max_rows) + }) + output$dt <- DT::renderDT({ DT::datatable( - df, + truncated_df()$df, fillContainer = TRUE, options = list(pageLength = 25, scrollX = TRUE) ) }) + output$data_info <- shiny::renderText({ + truncation_info_message(truncated_df()) + }) + output$sql_output <- shiny::renderUI({ sql <- if (shiny::isTruthy(qc_vals$sql())) { qc_vals$sql() @@ -895,6 +912,9 @@ querychat <- function( } #' @rdname querychat-convenience +#' @param max_rows Maximum number of rows to display in the data table. +#' This does not affect the number of rows that the LLM can query +#' against. Default is 1000. Set to `NULL` to disable row limit. #' @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"`. @@ -914,6 +934,7 @@ querychat_app <- function( extra_instructions = NULL, prompt_template = NULL, cleanup = NA, + max_rows = 1000L, bookmark_store = "url" ) { if (shiny::isRunning()) { @@ -948,7 +969,7 @@ querychat_app <- function( cleanup = cleanup ) - qc$app(bookmark_store = bookmark_store) + qc$app(max_rows = max_rows, bookmark_store = bookmark_store) } normalize_data_source <- function(data_source, table_name) { diff --git a/pkg-r/R/utils-display.R b/pkg-r/R/utils-display.R new file mode 100644 index 00000000..62c3c88f --- /dev/null +++ b/pkg-r/R/utils-display.R @@ -0,0 +1,63 @@ +maybe_truncate <- function(df, max_rows) { + is_lazy <- inherits(df, "tbl_sql") + + if (is.null(max_rows)) { + if (is_lazy) { + df <- dplyr::collect(df) + } + total_rows <- nrow(df) + total_cols <- ncol(df) + } else if (is_lazy) { + total_rows <- dplyr::pull(dplyr::tally(df)) + total_cols <- ncol(df) + df <- if (total_rows > max_rows) { + dplyr::collect(head(df, max_rows)) + } else { + dplyr::collect(df) + } + } else { + total_rows <- nrow(df) + total_cols <- ncol(df) + if (total_rows > max_rows) { + df <- head(df, max_rows) + } + } + + truncated <- !is.null(max_rows) && total_rows > max_rows + + if (truncated) { + warning( + "querychat: Displaying ", + max_rows, + " of ", + total_rows, + " rows. ", + "Set `max_rows` to increase or `NULL` to disable.", + call. = FALSE + ) + } + + list( + df = df, + total_rows = total_rows, + total_cols = total_cols, + truncated = truncated + ) +} + +truncation_info_message <- function(result) { + if (result$truncated) { + sprintf( + "Showing first %d of %d rows (%d columns).", + nrow(result$df), + result$total_rows, + result$total_cols + ) + } else { + sprintf( + "Data has %d rows and %d columns.", + result$total_rows, + result$total_cols + ) + } +} diff --git a/pkg-r/man/DBISource.Rd b/pkg-r/man/DBISource.Rd index 75e61096..e801b395 100644 --- a/pkg-r/man/DBISource.Rd +++ b/pkg-r/man/DBISource.Rd @@ -4,11 +4,6 @@ \alias{DBISource} \title{DBI Source} \description{ -DBI Source - -DBI Source -} -\details{ A DataSource implementation for DBI database connections (SQLite, PostgreSQL, MySQL, etc.). This class wraps a DBI connection and provides SQL query execution against a single table in the database. @@ -34,176 +29,192 @@ db_source$cleanup() \dontshow{\}) # examplesIf} } \section{Super class}{ -\code{\link[querychat:DataSource]{querychat::DataSource}} -> \code{DBISource} +\code{\link[querychat:DataSource]{DataSource}} -> \code{DBISource} } \section{Methods}{ \subsection{Public methods}{ -\itemize{ -\item \href{#method-DBISource-new}{\code{DBISource$new()}} -\item \href{#method-DBISource-get_db_type}{\code{DBISource$get_db_type()}} -\item \href{#method-DBISource-get_schema}{\code{DBISource$get_schema()}} -\item \href{#method-DBISource-get_semantic_views_description}{\code{DBISource$get_semantic_views_description()}} -\item \href{#method-DBISource-execute_query}{\code{DBISource$execute_query()}} -\item \href{#method-DBISource-test_query}{\code{DBISource$test_query()}} -\item \href{#method-DBISource-get_data}{\code{DBISource$get_data()}} -\item \href{#method-DBISource-cleanup}{\code{DBISource$cleanup()}} -\item \href{#method-DBISource-clone}{\code{DBISource$clone()}} -} + \itemize{ + \item \href{#method-DBISource-initialize}{\code{DBISource$new()}} + \item \href{#method-DBISource-get_db_type}{\code{DBISource$get_db_type()}} + \item \href{#method-DBISource-get_schema}{\code{DBISource$get_schema()}} + \item \href{#method-DBISource-get_semantic_views_description}{\code{DBISource$get_semantic_views_description()}} + \item \href{#method-DBISource-execute_query}{\code{DBISource$execute_query()}} + \item \href{#method-DBISource-test_query}{\code{DBISource$test_query()}} + \item \href{#method-DBISource-get_data}{\code{DBISource$get_data()}} + \item \href{#method-DBISource-cleanup}{\code{DBISource$cleanup()}} + \item \href{#method-DBISource-clone}{\code{DBISource$clone()}} + } } \if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-DBISource-new}{}}} -\subsection{Method \code{new()}}{ -Create a new DBISource -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DBISource$new(conn, table_name)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{conn}}{A DBI connection object} - -\item{\code{table_name}}{Name of the table in the database. Can be a character +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-DBISource-initialize}{}}} +\subsection{\code{DBISource$new()}}{ + Create a new DBISource + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DBISource$new(conn, table_name)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{conn}}{A DBI connection object} + \item{\code{table_name}}{Name of the table in the database. Can be a character string or a \code{\link[DBI:Id]{DBI::Id()}} object for tables in catalogs/schemas} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A new DBISource object + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -A new DBISource object -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DBISource-get_db_type}{}}} -\subsection{Method \code{get_db_type()}}{ -Get the database type -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DBISource$get_db_type()}\if{html}{\out{
}} +\subsection{\code{DBISource$get_db_type()}}{ + Get the database type + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DBISource$get_db_type()} + \if{html}{\out{
}} + } + \subsection{Returns}{ + A string identifying the database type + } } -\subsection{Returns}{ -A string identifying the database type -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DBISource-get_schema}{}}} -\subsection{Method \code{get_schema()}}{ -Get schema information for the database table -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DBISource$get_schema(categorical_threshold = 20)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{categorical_threshold}}{Maximum number of unique values for a text +\subsection{\code{DBISource$get_schema()}}{ + Get schema information for the database table + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DBISource$get_schema(categorical_threshold = 20)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{categorical_threshold}}{Maximum number of unique values for a text column to be considered categorical (default: 20)} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A string describing the schema + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -A string describing the schema -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DBISource-get_semantic_views_description}{}}} -\subsection{Method \code{get_semantic_views_description()}}{ -Get information about semantic views (if any) for the system prompt. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DBISource$get_semantic_views_description()}\if{html}{\out{
}} +\subsection{\code{DBISource$get_semantic_views_description()}}{ + Get information about semantic views (if any) for the system prompt. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DBISource$get_semantic_views_description()} + \if{html}{\out{
}} + } + \subsection{Returns}{ + A string with semantic view information, or empty string if none + } } -\subsection{Returns}{ -A string with semantic view information, or empty string if none -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DBISource-execute_query}{}}} -\subsection{Method \code{execute_query()}}{ -Execute a SQL query -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DBISource$execute_query(query)}\if{html}{\out{
}} +\subsection{\code{DBISource$execute_query()}}{ + Execute a SQL query + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DBISource$execute_query(query)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{query}}{SQL query string. If NULL or empty, returns all data} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A data frame with query results + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{query}}{SQL query string. If NULL or empty, returns all data} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -A data frame with query results -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DBISource-test_query}{}}} -\subsection{Method \code{test_query()}}{ -Test a SQL query by fetching only one row -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DBISource$test_query(query, require_all_columns = FALSE)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{query}}{SQL query string} - -\item{\code{require_all_columns}}{If \code{TRUE}, validates that the result includes +\subsection{\code{DBISource$test_query()}}{ + Test a SQL query by fetching only one row + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DBISource$test_query(query, require_all_columns = FALSE)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{query}}{SQL query string} + \item{\code{require_all_columns}}{If \code{TRUE}, validates that the result includes all original table columns (default: \code{FALSE})} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A data frame with one row of results + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -A data frame with one row of results -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DBISource-get_data}{}}} -\subsection{Method \code{get_data()}}{ -Get all data from the table -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DBISource$get_data()}\if{html}{\out{
}} +\subsection{\code{DBISource$get_data()}}{ + Get all data from the table + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DBISource$get_data()} + \if{html}{\out{
}} + } + \subsection{Returns}{ + A data frame containing all data + } } -\subsection{Returns}{ -A data frame containing all data -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DBISource-cleanup}{}}} -\subsection{Method \code{cleanup()}}{ -Disconnect from the database -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DBISource$cleanup()}\if{html}{\out{
}} +\subsection{\code{DBISource$cleanup()}}{ + Disconnect from the database + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DBISource$cleanup()} + \if{html}{\out{
}} + } + \subsection{Returns}{ + NULL (invisibly) + } } -\subsection{Returns}{ -NULL (invisibly) -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DBISource-clone}{}}} -\subsection{Method \code{clone()}}{ -The objects of this class are cloneable with this method. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DBISource$clone(deep = FALSE)}\if{html}{\out{
}} +\subsection{\code{DBISource$clone()}}{ + The objects of this class are cloneable with this method. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DBISource$clone(deep = FALSE)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{deep}}{Whether to make a deep clone.} + } + \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/DataFrameSource.Rd b/pkg-r/man/DataFrameSource.Rd index c1d18815..f33c62e1 100644 --- a/pkg-r/man/DataFrameSource.Rd +++ b/pkg-r/man/DataFrameSource.Rd @@ -39,76 +39,76 @@ df_sqlite$cleanup() \dontshow{\}) # examplesIf} } \section{Super classes}{ -\code{\link[querychat:DataSource]{querychat::DataSource}} -> \code{\link[querychat:DBISource]{querychat::DBISource}} -> \code{DataFrameSource} +\code{\link[querychat:DataSource]{DataSource}} -> \code{\link[querychat:DBISource]{DBISource}} -> \code{DataFrameSource} } \section{Methods}{ \subsection{Public methods}{ -\itemize{ -\item \href{#method-DataFrameSource-new}{\code{DataFrameSource$new()}} -\item \href{#method-DataFrameSource-clone}{\code{DataFrameSource$clone()}} + \itemize{ + \item \href{#method-DataFrameSource-initialize}{\code{DataFrameSource$new()}} + \item \href{#method-DataFrameSource-clone}{\code{DataFrameSource$clone()}} + } } -} -\if{html}{\out{ -
Inherited methods +\if{html}{\out{
Inherited methods -
-}} +
}} \if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-DataFrameSource-new}{}}} -\subsection{Method \code{new()}}{ -Create a new DataFrameSource -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DataFrameSource$new( +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-DataFrameSource-initialize}{}}} +\subsection{\code{DataFrameSource$new()}}{ + Create a new DataFrameSource + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DataFrameSource$new( df, table_name, engine = getOption("querychat.DataFrameSource.engine", NULL) -)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{df}}{A data frame.} - -\item{\code{table_name}}{Name to use for the table in SQL queries. Must be a +)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{df}}{A data frame.} + \item{\code{table_name}}{Name to use for the table in SQL queries. Must be a valid table name (start with letter, contain only letters, numbers, and underscores)} - -\item{\code{engine}}{Database engine to use: "duckdb" or "sqlite". Set the + \item{\code{engine}}{Database engine to use: "duckdb" or "sqlite". Set the global option \code{querychat.DataFrameSource.engine} to specify the default engine for all instances. If NULL (default), uses the first available engine from duckdb or RSQLite (in that order).} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A new DataFrameSource object + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -A new DataFrameSource object -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DataFrameSource-clone}{}}} -\subsection{Method \code{clone()}}{ -The objects of this class are cloneable with this method. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DataFrameSource$clone(deep = FALSE)}\if{html}{\out{
}} +\subsection{\code{DataFrameSource$clone()}}{ + The objects of this class are cloneable with this method. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DataFrameSource$clone(deep = FALSE)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{deep}}{Whether to make a deep clone.} + } + \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/DataSource.Rd b/pkg-r/man/DataSource.Rd index 1dea82a3..b8577bda 100644 --- a/pkg-r/man/DataSource.Rd +++ b/pkg-r/man/DataSource.Rd @@ -23,143 +23,156 @@ MyDataSource <- R6::R6Class( } \section{Public fields}{ -\if{html}{\out{
}} -\describe{ -\item{\code{table_name}}{Name of the table to be used in SQL queries} -} -\if{html}{\out{
}} + \if{html}{\out{
}} + \describe{ + \item{\code{table_name}}{Name of the table to be used in SQL queries} + } + \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ -\itemize{ -\item \href{#method-DataSource-get_db_type}{\code{DataSource$get_db_type()}} -\item \href{#method-DataSource-get_schema}{\code{DataSource$get_schema()}} -\item \href{#method-DataSource-execute_query}{\code{DataSource$execute_query()}} -\item \href{#method-DataSource-test_query}{\code{DataSource$test_query()}} -\item \href{#method-DataSource-get_data}{\code{DataSource$get_data()}} -\item \href{#method-DataSource-cleanup}{\code{DataSource$cleanup()}} -\item \href{#method-DataSource-clone}{\code{DataSource$clone()}} -} + \itemize{ + \item \href{#method-DataSource-get_db_type}{\code{DataSource$get_db_type()}} + \item \href{#method-DataSource-get_schema}{\code{DataSource$get_schema()}} + \item \href{#method-DataSource-execute_query}{\code{DataSource$execute_query()}} + \item \href{#method-DataSource-test_query}{\code{DataSource$test_query()}} + \item \href{#method-DataSource-get_data}{\code{DataSource$get_data()}} + \item \href{#method-DataSource-cleanup}{\code{DataSource$cleanup()}} + \item \href{#method-DataSource-clone}{\code{DataSource$clone()}} + } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DataSource-get_db_type}{}}} -\subsection{Method \code{get_db_type()}}{ -Get the database type -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DataSource$get_db_type()}\if{html}{\out{
}} +\subsection{\code{DataSource$get_db_type()}}{ + Get the database type + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DataSource$get_db_type()} + \if{html}{\out{
}} + } + \subsection{Returns}{ + A string describing the database type (e.g., "DuckDB", "SQLite") + } } -\subsection{Returns}{ -A string describing the database type (e.g., "DuckDB", "SQLite") -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DataSource-get_schema}{}}} -\subsection{Method \code{get_schema()}}{ -Get schema information about the table -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DataSource$get_schema(categorical_threshold = 20)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{categorical_threshold}}{Maximum number of unique values for a text +\subsection{\code{DataSource$get_schema()}}{ + Get schema information about the table + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DataSource$get_schema(categorical_threshold = 20)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{categorical_threshold}}{Maximum number of unique values for a text column to be considered categorical} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A string containing schema information formatted for LLM prompts + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -A string containing schema information formatted for LLM prompts -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DataSource-execute_query}{}}} -\subsection{Method \code{execute_query()}}{ -Execute a SQL query and return results -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DataSource$execute_query(query)}\if{html}{\out{
}} +\subsection{\code{DataSource$execute_query()}}{ + Execute a SQL query and return results + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DataSource$execute_query(query)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{query}}{SQL query string to execute} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A data frame containing query results + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{query}}{SQL query string to execute} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -A data frame containing query results -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DataSource-test_query}{}}} -\subsection{Method \code{test_query()}}{ -Test a SQL query by fetching only one row -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DataSource$test_query(query, require_all_columns = FALSE)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{query}}{SQL query string to test} - -\item{\code{require_all_columns}}{If \code{TRUE}, validates that the result includes +\subsection{\code{DataSource$test_query()}}{ + Test a SQL query by fetching only one row + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DataSource$test_query(query, require_all_columns = FALSE)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{query}}{SQL query string to test} + \item{\code{require_all_columns}}{If \code{TRUE}, validates that the result includes all original table columns (default: \code{FALSE})} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -A data frame containing one row of results (or empty if no + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A data frame containing one row of results (or empty if no matches) + } } -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DataSource-get_data}{}}} -\subsection{Method \code{get_data()}}{ -Get the unfiltered data as a data frame -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DataSource$get_data()}\if{html}{\out{
}} +\subsection{\code{DataSource$get_data()}}{ + Get the unfiltered data as a data frame + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DataSource$get_data()} + \if{html}{\out{
}} + } + \subsection{Returns}{ + A data frame containing all data from the table + } } -\subsection{Returns}{ -A data frame containing all data from the table -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DataSource-cleanup}{}}} -\subsection{Method \code{cleanup()}}{ -Clean up resources (close connections, etc.) -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DataSource$cleanup()}\if{html}{\out{
}} +\subsection{\code{DataSource$cleanup()}}{ + Clean up resources (close connections, etc.) + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DataSource$cleanup()} + \if{html}{\out{
}} + } + \subsection{Returns}{ + NULL (invisibly) + } } -\subsection{Returns}{ -NULL (invisibly) -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-DataSource-clone}{}}} -\subsection{Method \code{clone()}}{ -The objects of this class are cloneable with this method. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{DataSource$clone(deep = FALSE)}\if{html}{\out{
}} +\subsection{\code{DataSource$clone()}}{ + The objects of this class are cloneable with this method. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{DataSource$clone(deep = FALSE)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{deep}}{Whether to make a deep clone.} + } + \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/QueryChat.Rd b/pkg-r/man/QueryChat.Rd index da67c32e..4d3c188d 100644 --- a/pkg-r/man/QueryChat.Rd +++ b/pkg-r/man/QueryChat.Rd @@ -94,49 +94,50 @@ qc <- QueryChat$new(con, "mtcars") \dontshow{\}) # examplesIf} } \section{Public fields}{ -\if{html}{\out{
}} -\describe{ -\item{\code{greeting}}{The greeting message displayed to users.} + \if{html}{\out{
}} + \describe{ + \item{\code{greeting}}{The greeting message displayed to users.} -\item{\code{id}}{ID for the QueryChat instance.} + \item{\code{id}}{ID for the QueryChat instance.} -\item{\code{tools}}{The allowed tools for the chat client.} -} -\if{html}{\out{
}} + \item{\code{tools}}{The allowed tools for the chat client.} + } + \if{html}{\out{
}} } \section{Active bindings}{ -\if{html}{\out{
}} -\describe{ -\item{\code{system_prompt}}{Get the system prompt.} + \if{html}{\out{
}} + \describe{ + \item{\code{system_prompt}}{Get the system prompt.} -\item{\code{data_source}}{Get or set the current data source. When setting, + \item{\code{data_source}}{Get or set the current data source. When setting, the value is normalized and the system prompt is rebuilt.} -} -\if{html}{\out{
}} + } + \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ -\itemize{ -\item \href{#method-QueryChat-new}{\code{QueryChat$new()}} -\item \href{#method-QueryChat-client}{\code{QueryChat$client()}} -\item \href{#method-QueryChat-console}{\code{QueryChat$console()}} -\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()}} -\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()}} -} + \itemize{ + \item \href{#method-QueryChat-initialize}{\code{QueryChat$new()}} + \item \href{#method-QueryChat-client}{\code{QueryChat$client()}} + \item \href{#method-QueryChat-console}{\code{QueryChat$console()}} + \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()}} + \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( +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-QueryChat-initialize}{}}} +\subsection{\code{QueryChat$new()}}{ + Create a new QueryChat object. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{QueryChat$new( data_source, table_name = missing_arg(), ..., @@ -149,140 +150,130 @@ Create a new QueryChat object. extra_instructions = NULL, prompt_template = NULL, cleanup = NA -)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{data_source}}{Either a data.frame, a database connection (e.g., DBI +)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{data_source}}{Either a data.frame, a database connection (e.g., DBI connection), or \code{NULL} to defer setting the data source until later. When \code{NULL}, the data source must be set via the \verb{$data_source} property or passed to \verb{$server()} before calling methods that require data access.} - -\item{\code{table_name}}{A string specifying the table name to use in SQL + \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 not provided, will be inferred from the variable name for data.frame inputs. For database connections or \code{NULL} data sources, this parameter is required.} - -\item{\code{...}}{Additional arguments (currently unused).} - -\item{\code{id}}{Optional module ID for the QueryChat instance. If not provided, + \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 + \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: + \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 A string to pass to \code{\link[ellmer:chat]{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{tools}}{Which querychat tools to include in the chat client, by + \item{\code{tools}}{Which querychat tools to include in the chat client, by default. \code{"update"} includes the tools for updating and resetting the dashboard and \code{"query"} includes the tool for executing SQL queries. Use \code{tools = "update"} when you only want the dashboard updating tools, or when you want to disable the querying tool entirely to prevent the LLM from seeing any of the data in your dataset.} - -\item{\code{data_description}}{Optional description of the data in plain text or + \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 + \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 + \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 + \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{cleanup}}{Whether or not to automatically run \verb{$cleanup()} when the + \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{
}} + } + \subsection{Returns}{ + A new \code{QueryChat} object. + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -A new \code{QueryChat} object. -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-QueryChat-client}{}}} -\subsection{Method \code{client()}}{ -Create a chat client, complete with registered tools, for the current +\subsection{\code{QueryChat$client()}}{ + Create a chat client, complete with registered tools, for the current data source. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{QueryChat$client( + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{QueryChat$client( tools = NA, update_dashboard = function(query, title) { }, reset_dashboard = function() { } -)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{tools}}{Which querychat tools to include in the chat client. +)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{tools}}{Which querychat tools to include in the chat client. \code{"update"} includes the tools for updating and resetting the dashboard and \code{"query"} includes the tool for executing SQL queries. By default, when \code{tools = NA}, the values provided at initialization are used.} - -\item{\code{update_dashboard}}{Optional function to call with the \code{query} and + \item{\code{update_dashboard}}{Optional function to call with the \code{query} and \code{title} generated by the LLM for the \code{update_dashboard} tool.} - -\item{\code{reset_dashboard}}{Optional function to call when the + \item{\code{reset_dashboard}}{Optional function to call when the \code{reset_dashboard} tool is called.} + } + \if{html}{\out{
}} + } } -\if{html}{\out{
}} -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-QueryChat-console}{}}} -\subsection{Method \code{console()}}{ -Launch a console-based chat interface with the data source. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{QueryChat$console(new = FALSE, ..., tools = "query")}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{new}}{Whether to create a new chat client instance or continue the +\subsection{\code{QueryChat$console()}}{ + Launch a console-based chat interface with the data source. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{QueryChat$console(new = FALSE, ..., tools = "query")} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{new}}{Whether to create a new chat client instance or continue the conversation from the last console chat session (the default).} - -\item{\code{...}}{Additional arguments passed to the \verb{$client()} method.} - -\item{\code{tools}}{Which querychat tools to include in the chat client. See + \item{\code{...}}{Additional arguments passed to the \verb{$client()} method.} + \item{\code{tools}}{Which querychat tools to include in the chat client. See \verb{$client()} for details. Ignored when not creating a new chat client. By default, only the \code{"query"} tool is included, regardless of the \code{tools} set at initialization.} + } + \if{html}{\out{
}} + } } -\if{html}{\out{
}} -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-QueryChat-app}{}}} -\subsection{Method \code{app()}}{ -Create and run a Shiny gadget for chatting with data +\subsection{\code{QueryChat$app()}}{ + 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 @@ -294,36 +285,40 @@ you're looking to deploy this app or run it through some other means, see qc <- QueryChat$new(mtcars) qc$app() }\if{html}{\out{}} -\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 + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{QueryChat$app(..., max_rows = 1000L, bookmark_store = "url")} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{...}}{Arguments passed to \verb{$app_obj()}.} + \item{\code{max_rows}}{Maximum number of rows to display in the data table. +This does not affect the number of rows that the LLM can query +against. Default is 1000. Set to \code{NULL} to disable row limit.} + \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 a list of session-specific values: + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + 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 } + } } -} + \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 +\subsection{\code{QueryChat$app_obj()}}{ + A streamlined Shiny app for chatting with data Creates a Shiny app designed for chatting with data, with: \itemize{ @@ -339,30 +334,34 @@ qc <- QueryChat$new(mtcars) app <- qc$app_obj() shiny::runApp(app) }\if{html}{\out{}} -\subsection{Usage}{ -\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 + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{QueryChat$app_obj(..., max_rows = 1000L, bookmark_store = "url")} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{...}}{Additional arguments (currently unused).} + \item{\code{max_rows}}{Maximum number of rows to display in the data table. +This does not affect the number of rows that the LLM can query +against. Default is 1000. Set to \code{NULL} to disable row limit.} + \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{shiny::runApp()}. + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -A Shiny app object that can be run with \code{shiny::runApp()}. -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-QueryChat-sidebar}{}}} -\subsection{Method \code{sidebar()}}{ -Create a sidebar containing the querychat UI. +\subsection{\code{QueryChat$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 @@ -375,44 +374,42 @@ ui <- page_sidebar( # Main content here ) }\if{html}{\out{}} -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{QueryChat$sidebar( + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{QueryChat$sidebar( ..., width = 400, height = "100\%", fillable = TRUE, id = 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 +)} + \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{id}}{Optional ID for the QueryChat instance. If not provided, will + \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{
}} + } + \subsection{Returns}{ + A \code{\link[bslib:sidebar]{bslib::sidebar()}} UI component. + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -A \code{\link[bslib:sidebar]{bslib::sidebar()}} UI component. -} -} + \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. +\subsection{\code{QueryChat$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. @@ -423,31 +420,32 @@ ui <- fluidPage( qc$ui() ) }\if{html}{\out{}} -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{QueryChat$ui(..., id = 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{id}}{Optional ID for the QueryChat instance. If not provided, + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{QueryChat$ui(..., id = 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{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{
}} + } + \subsection{Returns}{ + A UI component containing the chat interface. + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -A UI component containing the chat interface. -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-QueryChat-server}{}}} -\subsection{Method \code{server()}}{ -Initialize the querychat server logic. +\subsection{\code{QueryChat$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 returns session-specific @@ -463,52 +461,48 @@ server <- function(input, output, session) \{ output$title <- renderText(qc_vals$title() \%||\% "No Query") \} }\if{html}{\out{}} -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{QueryChat$server( + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{QueryChat$server( data_source = NULL, client = NULL, enable_bookmarking = FALSE, ..., id = NULL, session = shiny::getDefaultReactiveDomain() -)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{data_source}}{Optional data source to use. If provided, sets the +)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{data_source}}{Optional data source to use. If provided, sets the data_source property before initializing server logic. This is useful for the deferred pattern where data_source is not known at initialization time (e.g., when the data source depends on session- specific authentication).} - -\item{\code{client}}{Optional chat client override for this session. Can be an + \item{\code{client}}{Optional chat client override for this session. Can be an \link[ellmer:Chat]{ellmer::Chat} object or a string (e.g., \code{"openai/gpt-4o"}). If provided, overrides the client set at initialization for this session only — other sessions are unaffected. This is useful when the client must be created within a session scope (e.g., Posit Connect managed credentials).} - -\item{\code{enable_bookmarking}}{Whether to enable bookmarking for the chat + \item{\code{enable_bookmarking}}{Whether to enable bookmarking for the chat state. Default is \code{FALSE}. When enabled, the chat state (including current query, title, and chat history) will be saved and restored 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, + \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{
}} -} -\subsection{Returns}{ -A list containing session-specific reactive values and the chat + \item{\code{session}}{The Shiny session object.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A list containing session-specific reactive values and the chat client with the following elements: \itemize{ \item \code{df}: Reactive expression returning the current filtered data frame @@ -516,13 +510,14 @@ client with the following elements: \item \code{title}: Reactive value for the current title \item \code{client}: The session-specific chat client instance } + } } -} + \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. +\subsection{\code{QueryChat$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 @@ -539,27 +534,29 @@ writeLines(greeting, "mtcars_greeting.md") # Later, use the saved greeting qc2 <- QueryChat$new(mtcars, greeting = "mtcars_greeting.md") }\if{html}{\out{}} -\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 + \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{"output"} (print to console).} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + The greeting string in Markdown format. + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -The greeting string in Markdown format. -} -} + \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. +\subsection{\code{QueryChat$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 @@ -567,29 +564,33 @@ 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{Usage}{ + \if{html}{\out{
}} + \preformatted{QueryChat$cleanup()} + \if{html}{\out{
}} + } + \subsection{Returns}{ + Invisibly returns \code{NULL}. Resources are cleaned up internally. + } } -\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{\code{QueryChat$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{
}} + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{deep}}{Whether to make a deep clone.} -} -\if{html}{\out{
}} -} -} } diff --git a/pkg-r/man/TblSqlSource.Rd b/pkg-r/man/TblSqlSource.Rd index 4446cc77..f9345da3 100644 --- a/pkg-r/man/TblSqlSource.Rd +++ b/pkg-r/man/TblSqlSource.Rd @@ -31,197 +31,211 @@ mtcars_source$cleanup() \dontshow{\}) # examplesIf} } \section{Super classes}{ -\code{\link[querychat:DataSource]{querychat::DataSource}} -> \code{\link[querychat:DBISource]{querychat::DBISource}} -> \code{TblSqlSource} +\code{\link[querychat:DataSource]{DataSource}} -> \code{\link[querychat:DBISource]{DBISource}} -> \code{TblSqlSource} } \section{Public fields}{ -\if{html}{\out{
}} -\describe{ -\item{\code{table_name}}{Name of the table to be used in SQL queries} -} -\if{html}{\out{
}} + \if{html}{\out{
}} + \describe{ + \item{\code{table_name}}{Name of the table to be used in SQL queries} + } + \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ -\itemize{ -\item \href{#method-TblSqlSource-new}{\code{TblSqlSource$new()}} -\item \href{#method-TblSqlSource-get_db_type}{\code{TblSqlSource$get_db_type()}} -\item \href{#method-TblSqlSource-get_schema}{\code{TblSqlSource$get_schema()}} -\item \href{#method-TblSqlSource-execute_query}{\code{TblSqlSource$execute_query()}} -\item \href{#method-TblSqlSource-test_query}{\code{TblSqlSource$test_query()}} -\item \href{#method-TblSqlSource-prep_query}{\code{TblSqlSource$prep_query()}} -\item \href{#method-TblSqlSource-get_data}{\code{TblSqlSource$get_data()}} -\item \href{#method-TblSqlSource-cleanup}{\code{TblSqlSource$cleanup()}} -\item \href{#method-TblSqlSource-clone}{\code{TblSqlSource$clone()}} -} -} -\if{html}{\out{ -
Inherited methods + \itemize{ + \item \href{#method-TblSqlSource-initialize}{\code{TblSqlSource$new()}} + \item \href{#method-TblSqlSource-get_db_type}{\code{TblSqlSource$get_db_type()}} + \item \href{#method-TblSqlSource-get_schema}{\code{TblSqlSource$get_schema()}} + \item \href{#method-TblSqlSource-execute_query}{\code{TblSqlSource$execute_query()}} + \item \href{#method-TblSqlSource-test_query}{\code{TblSqlSource$test_query()}} + \item \href{#method-TblSqlSource-prep_query}{\code{TblSqlSource$prep_query()}} + \item \href{#method-TblSqlSource-get_data}{\code{TblSqlSource$get_data()}} + \item \href{#method-TblSqlSource-cleanup}{\code{TblSqlSource$cleanup()}} + \item \href{#method-TblSqlSource-clone}{\code{TblSqlSource$clone()}} + } +} +\if{html}{\out{
Inherited methods -
-}} +
}} \if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-TblSqlSource-new}{}}} -\subsection{Method \code{new()}}{ -Create a new TblSqlSource -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TblSqlSource$new(tbl, table_name = missing_arg())}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{tbl}}{A \code{\link[dbplyr:tbl_sql]{dbplyr::tbl_sql()}} (or SQL tibble via \code{\link[dplyr:tbl]{dplyr::tbl()}}).} - -\item{\code{table_name}}{Name of the table in the database. Can be a character +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-TblSqlSource-initialize}{}}} +\subsection{\code{TblSqlSource$new()}}{ + Create a new TblSqlSource + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TblSqlSource$new(tbl, table_name = missing_arg())} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{tbl}}{A \code{\link[dbplyr:tbl_sql]{dbplyr::tbl_sql()}} (or SQL tibble via \code{\link[dplyr:tbl]{dplyr::tbl()}}).} + \item{\code{table_name}}{Name of the table in the database. Can be a character string, or will be inferred from the \code{tbl} argument, if possible.} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A new TblSqlSource object + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -A new TblSqlSource object -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TblSqlSource-get_db_type}{}}} -\subsection{Method \code{get_db_type()}}{ -Get the database type -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TblSqlSource$get_db_type()}\if{html}{\out{
}} +\subsection{\code{TblSqlSource$get_db_type()}}{ + Get the database type + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TblSqlSource$get_db_type()} + \if{html}{\out{
}} + } + \subsection{Returns}{ + A string describing the database type (e.g., "DuckDB", "SQLite") + } } -\subsection{Returns}{ -A string describing the database type (e.g., "DuckDB", "SQLite") -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TblSqlSource-get_schema}{}}} -\subsection{Method \code{get_schema()}}{ -Get schema information about the table -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TblSqlSource$get_schema(categorical_threshold = 20)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{categorical_threshold}}{Maximum number of unique values for a text +\subsection{\code{TblSqlSource$get_schema()}}{ + Get schema information about the table + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TblSqlSource$get_schema(categorical_threshold = 20)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{categorical_threshold}}{Maximum number of unique values for a text column to be considered categorical} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A string containing schema information formatted for LLM prompts + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -A string containing schema information formatted for LLM prompts -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TblSqlSource-execute_query}{}}} -\subsection{Method \code{execute_query()}}{ -Execute a SQL query and return results -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TblSqlSource$execute_query(query)}\if{html}{\out{
}} +\subsection{\code{TblSqlSource$execute_query()}}{ + Execute a SQL query and return results + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TblSqlSource$execute_query(query)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{query}}{SQL query string to execute} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A data frame containing query results + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{query}}{SQL query string to execute} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -A data frame containing query results -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TblSqlSource-test_query}{}}} -\subsection{Method \code{test_query()}}{ -Test a SQL query by fetching only one row -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TblSqlSource$test_query(query, require_all_columns = FALSE)}\if{html}{\out{
}} -} - -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{query}}{SQL query string to test} - -\item{\code{require_all_columns}}{If \code{TRUE}, validates that the result includes +\subsection{\code{TblSqlSource$test_query()}}{ + Test a SQL query by fetching only one row + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TblSqlSource$test_query(query, require_all_columns = FALSE)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{query}}{SQL query string to test} + \item{\code{require_all_columns}}{If \code{TRUE}, validates that the result includes all original table columns (default: \code{FALSE})} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A data frame containing one row of results (or empty if no matches) + } } -\if{html}{\out{
}} -} -\subsection{Returns}{ -A data frame containing one row of results (or empty if no matches) -} -} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TblSqlSource-prep_query}{}}} -\subsection{Method \code{prep_query()}}{ -Prepare a generic \verb{SELECT * FROM ____} query to work with the SQL tibble -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TblSqlSource$prep_query(query)}\if{html}{\out{
}} +\subsection{\code{TblSqlSource$prep_query()}}{ + Prepare a generic \verb{SELECT * FROM ____} query to work with the SQL tibble + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TblSqlSource$prep_query(query)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{query}}{SQL query as a string} + } + \if{html}{\out{
}} + } + \subsection{Returns}{ + A complete SQL query string + } } -\subsection{Arguments}{ -\if{html}{\out{
}} -\describe{ -\item{\code{query}}{SQL query as a string} -} -\if{html}{\out{
}} -} -\subsection{Returns}{ -A complete SQL query string -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TblSqlSource-get_data}{}}} -\subsection{Method \code{get_data()}}{ -Get the unfiltered data as a SQL tibble -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TblSqlSource$get_data()}\if{html}{\out{
}} +\subsection{\code{TblSqlSource$get_data()}}{ + Get the unfiltered data as a SQL tibble + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TblSqlSource$get_data()} + \if{html}{\out{
}} + } + \subsection{Returns}{ + A \code{\link[dbplyr:tbl_sql]{dbplyr::tbl_sql()}} containing the original, unfiltered data + } } -\subsection{Returns}{ -A \code{\link[dbplyr:tbl_sql]{dbplyr::tbl_sql()}} containing the original, unfiltered data -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TblSqlSource-cleanup}{}}} -\subsection{Method \code{cleanup()}}{ -Clean up resources (close connections, etc.) -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TblSqlSource$cleanup()}\if{html}{\out{
}} +\subsection{\code{TblSqlSource$cleanup()}}{ + Clean up resources (close connections, etc.) + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TblSqlSource$cleanup()} + \if{html}{\out{
}} + } + \subsection{Returns}{ + NULL (invisibly) + } } -\subsection{Returns}{ -NULL (invisibly) -} -} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-TblSqlSource-clone}{}}} -\subsection{Method \code{clone()}}{ -The objects of this class are cloneable with this method. -\subsection{Usage}{ -\if{html}{\out{
}}\preformatted{TblSqlSource$clone(deep = FALSE)}\if{html}{\out{
}} +\subsection{\code{TblSqlSource$clone()}}{ + The objects of this class are cloneable with this method. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{TblSqlSource$clone(deep = FALSE)} + \if{html}{\out{
}} + } + \subsection{Arguments}{ + \if{html}{\out{
}} + \describe{ + \item{\code{deep}}{Whether to make a deep clone.} + } + \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/querychat-convenience.Rd b/pkg-r/man/querychat-convenience.Rd index c1e1391b..430aba6d 100644 --- a/pkg-r/man/querychat-convenience.Rd +++ b/pkg-r/man/querychat-convenience.Rd @@ -33,6 +33,7 @@ querychat_app( extra_instructions = NULL, prompt_template = NULL, cleanup = NA, + max_rows = 1000L, bookmark_store = "url" ) } @@ -61,7 +62,7 @@ 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 A string to pass to \code{\link[ellmer:chat]{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()}} @@ -96,6 +97,10 @@ is created within a Shiny app. Set to \code{TRUE} to always clean up, or In \code{querychat_app()}, in-memory databases created for data frames are always cleaned up.} +\item{max_rows}{Maximum number of rows to display in the data table. +This does not affect the number of rows that the LLM can query +against. Default is 1000. Set to \code{NULL} to disable row limit.} + \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"}.} diff --git a/pkg-r/man/querychat-package.Rd b/pkg-r/man/querychat-package.Rd index 8c82eb92..1e3ff88a 100644 --- a/pkg-r/man/querychat-package.Rd +++ b/pkg-r/man/querychat-package.Rd @@ -85,6 +85,7 @@ Useful links: Authors: \itemize{ + \item Garrick Aden-Buie \email{garrick@posit.co} (\href{https://orcid.org/0000-0002-7111-0077}{ORCID}) \item Joe Cheng \email{joe@posit.co} [conceptor] \item Carson Sievert \email{carson@posit.co} (\href{https://orcid.org/0000-0002-4958-2844}{ORCID}) } diff --git a/pkg-r/tests/testthat/test-utils-display.R b/pkg-r/tests/testthat/test-utils-display.R new file mode 100644 index 00000000..59067a2b --- /dev/null +++ b/pkg-r/tests/testthat/test-utils-display.R @@ -0,0 +1,86 @@ +describe("maybe_truncate()", { + large_df <- data.frame(x = seq_len(200), y = seq_len(200)) + small_df <- data.frame(x = seq_len(5), y = seq_len(5)) + + it("truncates when exceeds max_rows", { + result <- suppressWarnings(maybe_truncate(large_df, max_rows = 50)) + expect_equal(nrow(result$df), 50) + expect_equal(result$total_rows, 200) + expect_equal(result$total_cols, 2) + expect_true(result$truncated) + }) + + it("does not truncate when under max_rows", { + result <- maybe_truncate(small_df, max_rows = 50) + expect_equal(nrow(result$df), 5) + expect_equal(result$total_rows, 5) + expect_false(result$truncated) + }) + + it("does not truncate when max_rows is NULL", { + result <- maybe_truncate(large_df, max_rows = NULL) + expect_equal(nrow(result$df), 200) + expect_false(result$truncated) + }) + + it("does not truncate when exactly at max_rows", { + result <- maybe_truncate(large_df, max_rows = 200) + expect_equal(nrow(result$df), 200) + expect_false(result$truncated) + }) + + it("emits warning when truncated", { + expect_warning( + maybe_truncate(large_df, max_rows = 50), + "Displaying 50 of 200 rows" + ) + }) + + it("does not emit warning when not truncated", { + expect_no_warning(maybe_truncate(small_df, max_rows = 50)) + }) + + it("collects tbl_sql before truncating", { + skip_if_not_installed("duckdb") + skip_if_not_installed("dbplyr") + skip_if_not_installed("dplyr") + + conn <- DBI::dbConnect(duckdb::duckdb(), dbdir = ":memory:") + withr::defer(DBI::dbDisconnect(conn, shutdown = TRUE)) + + DBI::dbWriteTable(conn, "test_data", large_df) + tbl <- dplyr::tbl(conn, "test_data") + + result <- suppressWarnings(maybe_truncate(tbl, max_rows = 50)) + expect_true(is.data.frame(result$df)) + expect_equal(nrow(result$df), 50) + expect_equal(result$total_rows, 200) + expect_true(result$truncated) + }) +}) + +describe("truncation_info_message()", { + it("shows truncation message when truncated", { + result <- suppressWarnings( + maybe_truncate( + data.frame(x = seq_len(200), y = seq_len(200)), + max_rows = 50 + ) + ) + expect_equal( + truncation_info_message(result), + "Showing first 50 of 200 rows (2 columns)." + ) + }) + + it("shows full data message when not truncated", { + result <- maybe_truncate( + data.frame(x = seq_len(5), y = seq_len(5)), + max_rows = 50 + ) + expect_equal( + truncation_info_message(result), + "Data has 5 rows and 2 columns." + ) + }) +})