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
11 changes: 8 additions & 3 deletions pkg-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED]

* Added rich tool UI support using shinychat development version and chatlas >= 0.11.1. (#67)
### New features

* Added `querychat_reset_dashboard()` tool for easily resetting the dashboard filters when asked by the user. (#81)
* The `.sql` query and `.title` returned from `querychat.server()` are now reactive values, meaning you can now `.set()` their value, and `.df()` will update accordingly. (#98)

* Added `querychat.greeting()` to help you create a greeting message for your querychat bot. (#87)

* Added `querychat_reset_dashboard()` tool for easily resetting the dashboard filters when asked by the user. (#81)

### Improvements

* Added rich tool UI support using shinychat development version and chatlas >= 0.11.1. (#67)

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

## [0.2.2] - 2025-09-04
Expand Down Expand Up @@ -40,4 +46,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.1.0] - 2025-05-24

This first release of the `querychat` package.

96 changes: 59 additions & 37 deletions pkg-py/src/querychat/querychat.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import warnings
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Optional, Protocol, Union
from typing import TYPE_CHECKING, Any, Callable, Optional, Protocol, Union, overload

import chatlas
import chevron
Expand Down Expand Up @@ -41,6 +41,12 @@ class QueryChatConfig:
client: chatlas.Chat


ReactiveString = reactive.Value[str]
"""A reactive string value."""
ReactiveStringOrNone = reactive.Value[Union[str, None]]
"""A reactive string (or None) value."""


class QueryChat:
"""
An object representing a query chat session. This is created within a Shiny
Expand All @@ -52,18 +58,18 @@ class QueryChat:
def __init__(
self,
chat: chatlas.Chat,
sql: Callable[[], str],
title: Callable[[], Union[str, None]],
sql: ReactiveString,
title: ReactiveStringOrNone,
df: Callable[[], pd.DataFrame],
):
"""
Initialize a QueryChat object.

Args:
chat: The chat object for the session
sql: Reactive that returns the current SQL query
title: Reactive that returns the current title
df: Reactive that returns the filtered data frame
sql: Reactively read (or set) the current SQL query
title: Reactively read (or set) the current title
df: Reactively read the current filtered data frame

"""
self._chat = chat
Expand All @@ -81,29 +87,57 @@ def chat(self) -> chatlas.Chat:
"""
return self._chat

def sql(self) -> str:
@overload
def sql(self, query: None = None) -> str: ...

@overload
def sql(self, query: str) -> bool: ...

def sql(self, query: Optional[str] = None) -> str | bool:
"""
Reactively read the current SQL query that is in effect.
Reactively read (or set) the current SQL query that is in effect.

Args:
query: If provided, sets the current SQL query to this value.

Returns:
The current SQL query as a string, or `""` if no query has been set.
If no `query` is provided, returns the current SQL query as a string
(possibly `""` if no query has been set). If a `query` is provided,
returns `True` if the query was changed to a new value, or `False`
if it was the same as the current value.

"""
return self._sql()
if query is None:
return self._sql()
else:
return self._sql.set(query)

@overload
def title(self, value: None = None) -> str | None: ...

def title(self) -> Union[str, None]:
@overload
def title(self, value: str) -> bool: ...

def title(self, value: Optional[str] = None) -> str | None | bool:
"""
Reactively read the current title that is in effect. The title is a
short description of the current query that the LLM provides to us
whenever it generates a new SQL query. It can be used as a status string
for the data dashboard.
Reactively read (or set) the current title that is in effect.

The title is a short description of the current query that the LLM
provides to us whenever it generates a new SQL query. It can be used as
a status string for the data dashboard.

Returns:
The current title as a string, or `None` if no title has been set
due to no SQL query being set.
If no `value` is provided, returns the current title as a string, or
`None` if no title has been set due to no SQL query being set. If a
`value` is provided, sets the current title to this value and
returns `True` if the title was changed to a new value, or `False`
if it was the same as the current value.

"""
return self._title()
if value is None:
return self._title()
else:
return self._title.set(value)

def df(self) -> pd.DataFrame:
"""
Expand Down Expand Up @@ -496,22 +530,15 @@ def mod_server( # noqa: D417
- chat: The chat object.

"""

@reactive.effect
def _():
# This will be triggered when the module is initialized
# Here we would set up the chat interface, initialize the chat model, etc.
pass

# Extract config parameters
data_source = querychat_config.data_source
system_prompt = querychat_config.system_prompt
greeting = querychat_config.greeting
client = querychat_config.client

# Reactive values to store state
current_title = reactive.value[Union[str, None]](None)
current_query = reactive.value("")
current_title = ReactiveStringOrNone(None)
current_query = ReactiveString("")

@reactive.calc
def filtered_df():
Expand All @@ -520,20 +547,15 @@ def filtered_df():
else:
return data_source.execute_query(current_query.get())

# This would handle appending messages to the chat UI
async def append_output(text):
async with chat_ui.message_stream_context() as msgstream:
await msgstream.append(text)

# Create the tool functions
update_dashboard_tool = tool_update_dashboard(
data_source,
current_query.set,
current_title.set,
current_query,
current_title,
)
reset_dashboard_tool = tool_reset_dashboard(
current_query.set,
current_title.set,
current_query,
current_title,
)
query_tool = tool_query(data_source)

Expand Down Expand Up @@ -584,4 +606,4 @@ async def greet_on_startup():
await chat_ui.append_message_stream(stream)

# Return the interface for other components to use
return QueryChat(chat, current_query.get, current_title.get, filtered_df)
return QueryChat(chat, current_query, current_title, filtered_df)
41 changes: 22 additions & 19 deletions pkg-py/src/querychat/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

if TYPE_CHECKING:
from .datasource import DataSource
from .querychat import ReactiveString, ReactiveStringOrNone


def _read_prompt_template(filename: str, **kwargs) -> str:
Expand All @@ -23,8 +24,8 @@ def _read_prompt_template(filename: str, **kwargs) -> str:

def _update_dashboard_impl(
data_source: DataSource,
current_query: Callable,
current_title: Callable,
current_query: ReactiveString,
current_title: ReactiveStringOrNone,
) -> Callable[[str, str], ContentToolResult]:
"""Create the implementation function for updating the dashboard."""

Expand All @@ -47,9 +48,9 @@ def update_dashboard(query: str, title: str) -> ContentToolResult:

# Update state on success
if query is not None:
current_query(query)
current_query.set(query)
if title is not None:
current_title(title)
current_title.set(title)

except Exception as e:
error = str(e)
Expand Down Expand Up @@ -77,19 +78,19 @@ def update_dashboard(query: str, title: str) -> ContentToolResult:

def tool_update_dashboard(
data_source: DataSource,
current_query: Callable,
current_title: Callable,
current_query: ReactiveString,
current_title: ReactiveStringOrNone,
) -> Tool:
"""
Create a tool that modifies the data presented in the dashboard based on the SQL query.

Parameters
----------
data_source : DataSource
data_source
The data source to query against
current_query : Callable
current_query
Reactive value for storing the current SQL query
current_title : Callable
current_title
Reactive value for storing the current title

Returns
Expand All @@ -114,15 +115,15 @@ def tool_update_dashboard(


def _reset_dashboard_impl(
current_query: Callable,
current_title: Callable,
current_query: ReactiveString,
current_title: ReactiveStringOrNone,
) -> Callable[[], ContentToolResult]:
"""Create the implementation function for resetting the dashboard."""

def reset_dashboard() -> ContentToolResult:
# Reset current query and title
current_query("")
current_title(None)
current_query.set("")
current_title.set(None)

# Add Reset Filter button
button_html = """<button
Expand Down Expand Up @@ -152,17 +153,17 @@ def reset_dashboard() -> ContentToolResult:


def tool_reset_dashboard(
current_query: Callable,
current_title: Callable,
current_query: ReactiveString,
current_title: ReactiveStringOrNone,
) -> Tool:
"""
Create a tool that resets the dashboard to show all data.

Parameters
----------
current_query : Callable
current_query
Reactive value for storing the current SQL query
current_title : Callable
current_title
Reactive value for storing the current title

Returns
Expand Down Expand Up @@ -228,7 +229,7 @@ def tool_query(data_source: DataSource) -> Tool:

Parameters
----------
data_source : DataSource
data_source
The data source to query against

Returns
Expand All @@ -239,7 +240,9 @@ def tool_query(data_source: DataSource) -> Tool:
"""
impl = _query_impl(data_source)

description = _read_prompt_template("tool-query.md", db_type=data_source.get_db_type())
description = _read_prompt_template(
"tool-query.md", db_type=data_source.get_db_type()
)
impl.__doc__ = description

return Tool.from_func(
Expand Down