Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* querychat's system prompt and tool descriptions were rewritten for clarity and future extensibility. (#90)

* Tool detail cards can now be expanded or collapsed by default when querychat runs a query or updates the dashboard via the `QUERYCHAT_TOOL_DETAILS` environment variable. Valid values are `"expanded"`, `"collapsed"`, or `"default"`. (#137)

## [0.2.2] - 2025-09-04

* Fixed another issue with data sources that aren't already narwhals DataFrames (#83)
Expand Down
62 changes: 61 additions & 1 deletion pkg-py/src/querychat/_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from __future__ import annotations

import os
import warnings
from contextlib import contextmanager
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Literal, Optional

import narwhals.stable.v1 as nw

Expand Down Expand Up @@ -53,6 +54,65 @@ def temp_env_vars(env_vars: dict[str, Optional[str]]):
os.environ[key] = original_value


def get_tool_details_setting() -> Optional[Literal["expanded", "collapsed", "default"]]:
"""
Get and validate the tool details setting from environment variable.

Returns
-------
Optional[str]
The validated value of QUERYCHAT_TOOL_DETAILS environment variable
(one of 'expanded', 'collapsed', or 'default'), or None if not set
or invalid

"""
setting = os.environ.get("QUERYCHAT_TOOL_DETAILS")
if setting is None:
return None

setting_lower = setting.lower()
valid_settings = ("expanded", "collapsed", "default")

if setting_lower not in valid_settings:
warnings.warn(
f"Invalid value for QUERYCHAT_TOOL_DETAILS: {setting!r}. "
"Must be one of: 'expanded', 'collapsed', or 'default'",
UserWarning,
stacklevel=2,
)
return None

return setting_lower


def querychat_tool_starts_open(action: Literal["update", "query", "reset"]) -> bool:
"""
Determine whether a tool card should be open based on action and setting.

Parameters
----------
action : str
The action type ('update', 'query', or 'reset')

Returns
-------
bool
True if the tool card should be open, False otherwise

"""
setting = get_tool_details_setting()

if setting is None:
return action != "reset"

if setting == "expanded":
return True
elif setting == "collapsed":
return False
else: # setting == "default"
return action != "reset"


def df_to_html(df: IntoFrame, maxrows: int = 5) -> str:
"""
Convert a DataFrame to an HTML table for display in chat.
Expand Down
8 changes: 4 additions & 4 deletions pkg-py/src/querychat/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from shinychat.types import ToolResultDisplay

from ._icons import bs_icon
from ._utils import df_to_html
from ._utils import df_to_html, querychat_tool_starts_open

if TYPE_CHECKING:
from ._datasource import DataSource
Expand Down Expand Up @@ -65,7 +65,7 @@ def update_dashboard(query: str, title: str) -> ContentToolResult:
markdown=markdown + f"\n\n{button_html}",
title=title,
show_request=False,
open=True,
open=querychat_tool_starts_open("update"),
icon=bs_icon("funnel-fill"),
),
},
Expand Down Expand Up @@ -139,7 +139,7 @@ def reset_dashboard() -> ContentToolResult:
markdown=button_html,
title=None,
show_request=False,
open=False,
open=querychat_tool_starts_open("reset"),
icon=bs_icon("arrow-counterclockwise"),
),
},
Expand Down Expand Up @@ -208,7 +208,7 @@ def query(query: str, _intent: str = "") -> ContentToolResult:
"display": ToolResultDisplay(
markdown=markdown,
show_request=False,
open=True,
open=querychat_tool_starts_open("query"),
icon=bs_icon("table"),
),
},
Expand Down
66 changes: 66 additions & 0 deletions pkg-py/tests/test_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Tests for tool functions and utilities."""

import warnings

from querychat._utils import querychat_tool_starts_open


def test_querychat_tool_starts_open_default_behavior(monkeypatch):
"""Test default behavior when no setting is provided."""
monkeypatch.delenv("QUERYCHAT_TOOL_DETAILS", raising=False)

assert querychat_tool_starts_open("query") is True
assert querychat_tool_starts_open("update") is True
assert querychat_tool_starts_open("reset") is False


def test_querychat_tool_starts_open_expanded(monkeypatch):
"""Test 'expanded' setting."""
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "expanded")

assert querychat_tool_starts_open("query") is True
assert querychat_tool_starts_open("update") is True
assert querychat_tool_starts_open("reset") is True


def test_querychat_tool_starts_open_collapsed(monkeypatch):
"""Test 'collapsed' setting."""
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "collapsed")

assert querychat_tool_starts_open("query") is False
assert querychat_tool_starts_open("update") is False
assert querychat_tool_starts_open("reset") is False


def test_querychat_tool_starts_open_default_setting(monkeypatch):
"""Test 'default' setting."""
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "default")

assert querychat_tool_starts_open("query") is True
assert querychat_tool_starts_open("update") is True
assert querychat_tool_starts_open("reset") is False


def test_querychat_tool_starts_open_case_insensitive(monkeypatch):
"""Test that setting is case-insensitive."""
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "EXPANDED")
assert querychat_tool_starts_open("query") is True

monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "Collapsed")
assert querychat_tool_starts_open("query") is False

monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "DeFaUlT")
assert querychat_tool_starts_open("query") is True


def test_querychat_tool_starts_open_invalid_setting(monkeypatch):
"""Test warning on invalid setting."""
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "invalid")

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = querychat_tool_starts_open("query")

assert len(w) == 1
assert "Invalid value" in str(w[0].message)
assert result is True # Falls back to default behavior
2 changes: 2 additions & 0 deletions pkg-r/NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# querychat (development version)

* Tool detail cards can now be expanded or collapsed by default when querychat runs a query or updates the dashboard via the `querychat.tool_details` R option or the `QUERYCHAT_TOOL_DETAILS` environment variable. Valid values are `"expanded"`, `"collapsed"`, or `"default"`. (#137)

* Added bookmarking support to `QueryChat$server()` and `querychat_app()`. When bookmarking is enabled (via `bookmark_store = "url"` or `"server"` in `querychat_app()` or `$app_obj()`, or via `enable_bookmarking = TRUE` in `$server()`), the chat state (including current query, title, and chat history) will be saved and restored with Shiny bookmarks. (#107)

* Nearly the entire functional API (i.e., `querychat_init()`, `querychat_sidebar()`, `querychat_server()`, etc) has been hard deprecated in favor of a simpler OOP-based API. Namely, the new `QueryChat$new()` class is now the main entry point (instead of `querychat_init()`) and has methods to replace old functions (e.g., `$sidebar()`, `$server()`, etc). (#109)
Expand Down
44 changes: 43 additions & 1 deletion pkg-r/R/querychat_tools.R
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,48 @@ tool_query <- function(data_source) {
)
}

querychat_tool_details_option <- function() {
opt <- getOption("querychat.tool_details", NULL)
if (!is.null(opt)) {
setting <- opt
} else {
env <- Sys.getenv("QUERYCHAT_TOOL_DETAILS", "")
if (nzchar(env)) {
setting <- env
} else {
return(NULL)
}
}

setting <- tolower(setting)
valid_settings <- c("expanded", "collapsed", "default")

if (!setting %in% valid_settings) {
cli::cli_warn(c(
"Invalid value for {.code querychat.tool_details} or {.envvar QUERYCHAT_TOOL_DETAILS}: {.val {setting}}",
"i" = "Must be one of: {.or {.val {valid_settings}}}"
))
return(NULL)
}

setting
}

querychat_tool_starts_open <- function(action) {
setting <- querychat_tool_details_option()

if (is.null(setting)) {
return(action != "reset")
}

switch(
setting,
"expanded" = TRUE,
"collapsed" = FALSE,
action != "reset"
)
}

querychat_tool_result <- function(
data_source,
query,
Expand Down Expand Up @@ -182,7 +224,7 @@ querychat_tool_result <- function(
title = if (action == "update" && !is.null(title)) title,
show_request = is_error,
markdown = display_md,
open = !is_error && action != "reset"
open = querychat_tool_starts_open(action)
)
)
)
Expand Down
74 changes: 74 additions & 0 deletions pkg-r/tests/testthat/test-querychat_tools.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
test_that("querychat_tool_starts_open respects default behavior", {
withr::local_options(querychat.tool_details = NULL)
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = NA)

expect_true(querychat_tool_starts_open("query"))
expect_true(querychat_tool_starts_open("update"))
expect_false(querychat_tool_starts_open("reset"))
})

test_that("querychat_tool_starts_open respects 'expanded' setting via option", {
withr::local_options(querychat.tool_details = "expanded")

expect_true(querychat_tool_starts_open("query"))
expect_true(querychat_tool_starts_open("update"))
expect_true(querychat_tool_starts_open("reset"))
})

test_that("querychat_tool_starts_open respects 'collapsed' setting via option", {
withr::local_options(querychat.tool_details = "collapsed")

expect_false(querychat_tool_starts_open("query"))
expect_false(querychat_tool_starts_open("update"))
expect_false(querychat_tool_starts_open("reset"))
})

test_that("querychat_tool_starts_open respects 'default' setting via option", {
withr::local_options(querychat.tool_details = "default")

expect_true(querychat_tool_starts_open("query"))
expect_true(querychat_tool_starts_open("update"))
expect_false(querychat_tool_starts_open("reset"))
})

test_that("querychat_tool_starts_open respects 'expanded' setting via envvar", {
withr::local_options(querychat.tool_details = NULL)
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = "expanded")

expect_true(querychat_tool_starts_open("query"))
expect_true(querychat_tool_starts_open("update"))
expect_true(querychat_tool_starts_open("reset"))
})

test_that("querychat_tool_starts_open respects 'collapsed' setting via envvar", {
withr::local_options(querychat.tool_details = NULL)
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = "collapsed")

expect_false(querychat_tool_starts_open("query"))
expect_false(querychat_tool_starts_open("update"))
expect_false(querychat_tool_starts_open("reset"))
})

test_that("querychat_tool_starts_open is case-insensitive", {
withr::local_options(querychat.tool_details = "EXPANDED")
expect_true(querychat_tool_starts_open("query"))

withr::local_options(querychat.tool_details = "Collapsed")
expect_false(querychat_tool_starts_open("query"))
})

test_that("querychat_tool_starts_open warns on invalid setting", {
withr::local_options(querychat.tool_details = "invalid")

expect_warning(
querychat_tool_starts_open("query"),
"Invalid value for"
)
})

test_that("option takes precedence over environment variable", {
withr::local_options(querychat.tool_details = "collapsed")
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = "expanded")

expect_false(querychat_tool_starts_open("query"))
})