Skip to content
Merged
60 changes: 60 additions & 0 deletions pkg-py/src/querychat/_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

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

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[str]:
"""
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: str) -> 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
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: {.val expanded}, {.val collapsed}, or {.val default}"
))
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,
"default" = 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"))
})