querychat 0.2.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {querychat-0.2.2 → querychat-0.3.0}/.gitignore +5 -0
  2. {querychat-0.2.2 → querychat-0.3.0}/PKG-INFO +6 -6
  3. querychat-0.3.0/pkg-py/src/querychat/__init__.py +15 -0
  4. querychat-0.2.2/pkg-py/src/querychat/datasource.py → querychat-0.3.0/pkg-py/src/querychat/_datasource.py +155 -47
  5. querychat-0.3.0/pkg-py/src/querychat/_deprecated.py +108 -0
  6. querychat-0.3.0/pkg-py/src/querychat/_icons.py +20 -0
  7. querychat-0.3.0/pkg-py/src/querychat/_querychat.py +733 -0
  8. querychat-0.3.0/pkg-py/src/querychat/_querychat_module.py +197 -0
  9. querychat-0.3.0/pkg-py/src/querychat/_utils.py +157 -0
  10. querychat-0.3.0/pkg-py/src/querychat/data/__init__.py +68 -0
  11. querychat-0.3.0/pkg-py/src/querychat/data/tips.csv.gz +0 -0
  12. querychat-0.3.0/pkg-py/src/querychat/data/titanic.csv.gz +0 -0
  13. querychat-0.3.0/pkg-py/src/querychat/express/__init__.py +3 -0
  14. querychat-0.3.0/pkg-py/src/querychat/prompts/prompt.md +148 -0
  15. querychat-0.3.0/pkg-py/src/querychat/prompts/tool-query.md +34 -0
  16. querychat-0.3.0/pkg-py/src/querychat/prompts/tool-reset-dashboard.md +12 -0
  17. querychat-0.3.0/pkg-py/src/querychat/prompts/tool-update-dashboard.md +28 -0
  18. querychat-0.3.0/pkg-py/src/querychat/static/js/querychat.js +20 -0
  19. querychat-0.3.0/pkg-py/src/querychat/tools.py +248 -0
  20. querychat-0.3.0/pkg-py/src/querychat/types/__init__.py +9 -0
  21. {querychat-0.2.2 → querychat-0.3.0}/pyproject.toml +14 -10
  22. querychat-0.2.2/pkg-py/src/querychat/__init__.py +0 -13
  23. querychat-0.2.2/pkg-py/src/querychat/_utils.py +0 -48
  24. querychat-0.2.2/pkg-py/src/querychat/prompt/prompt.md +0 -103
  25. querychat-0.2.2/pkg-py/src/querychat/querychat.py +0 -660
  26. {querychat-0.2.2 → querychat-0.3.0}/LICENSE.md +0 -0
  27. {querychat-0.2.2 → querychat-0.3.0}/pkg-py/LICENSE +0 -0
  28. {querychat-0.2.2 → querychat-0.3.0}/pkg-py/README.md +0 -0
  29. {querychat-0.2.2 → querychat-0.3.0}/pkg-py/src/querychat/static/css/styles.css +0 -0
@@ -260,3 +260,8 @@ _dev
260
260
  .Rprofile
261
261
  renv/
262
262
  renv.lock
263
+
264
+ # Claude
265
+ .claude/settings.local.json
266
+
267
+ /.luarc.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: querychat
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Chat with your data using natural language
5
5
  Project-URL: Homepage, https://github.com/posit-dev/querychat
6
6
  Project-URL: Repository, https://github.com/posit-dev/querychat
@@ -31,20 +31,20 @@ License: # MIT License
31
31
  SOFTWARE.
32
32
  License-File: LICENSE.md
33
33
  Classifier: Programming Language :: Python
34
- Classifier: Programming Language :: Python :: 3.8
35
- Classifier: Programming Language :: Python :: 3.9
36
34
  Classifier: Programming Language :: Python :: 3.10
37
35
  Classifier: Programming Language :: Python :: 3.11
38
36
  Classifier: Programming Language :: Python :: 3.12
39
37
  Classifier: Programming Language :: Python :: 3.13
40
- Requires-Python: >=3.9
41
- Requires-Dist: chatlas
38
+ Classifier: Programming Language :: Python :: 3.14
39
+ Requires-Python: >=3.10
40
+ Requires-Dist: chatlas>=0.13.2
42
41
  Requires-Dist: chevron
43
42
  Requires-Dist: duckdb
44
43
  Requires-Dist: htmltools
45
44
  Requires-Dist: narwhals
46
45
  Requires-Dist: pandas
47
- Requires-Dist: shiny
46
+ Requires-Dist: shiny>=1.5.1
47
+ Requires-Dist: shinychat>=0.2.8
48
48
  Requires-Dist: shinywidgets
49
49
  Requires-Dist: sqlalchemy>=2.0.0
50
50
  Description-Content-Type: text/markdown
@@ -0,0 +1,15 @@
1
+ from ._deprecated import greeting, init, sidebar, system_prompt
2
+ from ._deprecated import mod_server as server
3
+ from ._deprecated import mod_ui as ui
4
+ from ._querychat import QueryChat
5
+
6
+ __all__ = (
7
+ "QueryChat",
8
+ # TODO(lifecycle): Remove these deprecated functions when we reach v1.0
9
+ "greeting",
10
+ "init",
11
+ "server",
12
+ "sidebar",
13
+ "system_prompt",
14
+ "ui",
15
+ )
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, ClassVar, Protocol
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING
4
5
 
5
6
  import duckdb
6
7
  import narwhals.stable.v1 as nw
@@ -13,83 +14,142 @@ if TYPE_CHECKING:
13
14
  from sqlalchemy.engine import Connection, Engine
14
15
 
15
16
 
16
- class DataSource(Protocol):
17
- db_engine: ClassVar[str]
17
+ class DataSource(ABC):
18
+ """
19
+ An abstract class defining the interface for data sources used by QueryChat.
20
+
21
+ Attributes
22
+ ----------
23
+ table_name
24
+ Name of the table to be used in SQL queries.
25
+
26
+ """
27
+
28
+ table_name: str
18
29
 
19
- def get_schema(self, *, categorical_threshold) -> str:
30
+ @abstractmethod
31
+ def get_db_type(self) -> str:
32
+ """Name for the database behind the SQL execution."""
33
+ ...
34
+
35
+ @abstractmethod
36
+ def get_schema(self, *, categorical_threshold: int) -> str:
20
37
  """
21
38
  Return schema information about the table as a string.
22
39
 
23
- Args:
24
- categorical_threshold: Maximum number of unique values for a text
25
- column to be considered categorical
40
+ Parameters
41
+ ----------
42
+ categorical_threshold
43
+ Maximum number of unique values for a text column to be considered
44
+ categorical
26
45
 
27
- Returns:
46
+ Returns
47
+ -------
48
+ :
28
49
  A string containing the schema information in a format suitable for
29
50
  prompting an LLM about the data structure
30
51
 
31
52
  """
32
53
  ...
33
54
 
55
+ @abstractmethod
34
56
  def execute_query(self, query: str) -> pd.DataFrame:
35
57
  """
36
58
  Execute SQL query and return results as DataFrame.
37
59
 
38
- Args:
39
- query: SQL query to execute
60
+ Parameters
61
+ ----------
62
+ query
63
+ SQL query to execute
40
64
 
41
- Returns:
65
+ Returns
66
+ -------
67
+ :
42
68
  Query results as a pandas DataFrame
43
69
 
44
70
  """
45
71
  ...
46
72
 
73
+ @abstractmethod
47
74
  def get_data(self) -> pd.DataFrame:
48
75
  """
49
76
  Return the unfiltered data as a DataFrame.
50
77
 
51
- Returns:
78
+ Returns
79
+ -------
80
+ :
52
81
  The complete dataset as a pandas DataFrame
53
82
 
54
83
  """
55
84
  ...
56
85
 
86
+ @abstractmethod
87
+ def cleanup(self) -> None:
88
+ """
89
+ Clean up resources associated with the data source.
90
+
91
+ This method should clean up any connections or resources used by the
92
+ data source.
93
+
94
+ Returns
95
+ -------
96
+ None
97
+
98
+ """
57
99
 
58
- class DataFrameSource:
100
+
101
+ class DataFrameSource(DataSource):
59
102
  """A DataSource implementation that wraps a pandas DataFrame using DuckDB."""
60
103
 
61
- db_engine: ClassVar[str] = "DuckDB"
62
104
  _df: nw.DataFrame | nw.LazyFrame
63
105
 
64
106
  def __init__(self, df: IntoFrame, table_name: str):
65
107
  """
66
108
  Initialize with a pandas DataFrame.
67
109
 
68
- Args:
69
- df: The DataFrame to wrap
70
- table_name: Name of the table in SQL queries
110
+ Parameters
111
+ ----------
112
+ df
113
+ The DataFrame to wrap
114
+ table_name
115
+ Name of the table in SQL queries
71
116
 
72
117
  """
73
118
  self._conn = duckdb.connect(database=":memory:")
74
119
  self._df = nw.from_native(df)
75
- self._table_name = table_name
120
+ self.table_name = table_name
76
121
  # TODO(@gadenbuie): If the data frame is already SQL-backed, maybe we shouldn't be making a new copy here.
77
122
  self._conn.register(table_name, self._df.lazy().collect().to_pandas())
78
123
 
124
+ def get_db_type(self) -> str:
125
+ """
126
+ Get the database type.
127
+
128
+ Returns
129
+ -------
130
+ :
131
+ The string "DuckDB"
132
+
133
+ """
134
+ return "DuckDB"
135
+
79
136
  def get_schema(self, *, categorical_threshold: int) -> str:
80
137
  """
81
138
  Generate schema information from DataFrame.
82
139
 
83
- Args:
84
- table_name: Name to use for the table in schema description
85
- categorical_threshold: Maximum number of unique values for a text column
86
- to be considered categorical
140
+ Parameters
141
+ ----------
142
+ categorical_threshold
143
+ Maximum number of unique values for a text column to be considered
144
+ categorical
87
145
 
88
- Returns:
146
+ Returns
147
+ -------
148
+ :
89
149
  String describing the schema
90
150
 
91
151
  """
92
- schema = [f"Table: {self._table_name}", "Columns:"]
152
+ schema = [f"Table: {self.table_name}", "Columns:"]
93
153
 
94
154
  # Ensure we're working with a DataFrame, not a LazyFrame
95
155
  ndf = (
@@ -140,10 +200,14 @@ class DataFrameSource:
140
200
  """
141
201
  Execute query using DuckDB.
142
202
 
143
- Args:
144
- query: SQL query to execute
203
+ Parameters
204
+ ----------
205
+ query
206
+ SQL query to execute
145
207
 
146
- Returns:
208
+ Returns
209
+ -------
210
+ :
147
211
  Query results as pandas DataFrame
148
212
 
149
213
  """
@@ -153,40 +217,66 @@ class DataFrameSource:
153
217
  """
154
218
  Return the unfiltered data as a DataFrame.
155
219
 
156
- Returns:
220
+ Returns
221
+ -------
222
+ :
157
223
  The complete dataset as a pandas DataFrame
158
224
 
159
225
  """
160
226
  # TODO(@gadenbuie): This should just return `self._df` and not a pandas DataFrame
161
227
  return self._df.lazy().collect().to_pandas()
162
228
 
229
+ def cleanup(self) -> None:
230
+ """
231
+ Close the DuckDB connection.
163
232
 
164
- class SQLAlchemySource:
165
- """
166
- A DataSource implementation that supports multiple SQL databases via SQLAlchemy.
233
+ Returns
234
+ -------
235
+ None
167
236
 
168
- Supports various databases including PostgreSQL, MySQL, SQLite, Snowflake, and Databricks.
237
+ """
238
+ if self._conn:
239
+ self._conn.close()
240
+
241
+
242
+ class SQLAlchemySource(DataSource):
169
243
  """
244
+ A DataSource implementation that supports multiple SQL databases via
245
+ SQLAlchemy.
170
246
 
171
- db_engine: ClassVar[str] = "SQLAlchemy"
247
+ Supports various databases including PostgreSQL, MySQL, SQLite, Snowflake,
248
+ and Databricks.
249
+ """
172
250
 
173
251
  def __init__(self, engine: Engine, table_name: str):
174
252
  """
175
253
  Initialize with a SQLAlchemy engine.
176
254
 
177
- Args:
178
- engine: SQLAlchemy engine
179
- table_name: Name of the table to query
255
+ Parameters
256
+ ----------
257
+ engine
258
+ SQLAlchemy engine
259
+ table_name
260
+ Name of the table to query
180
261
 
181
262
  """
182
263
  self._engine = engine
183
- self._table_name = table_name
264
+ self.table_name = table_name
184
265
 
185
266
  # Validate table exists
186
267
  inspector = inspect(self._engine)
187
268
  if not inspector.has_table(table_name):
188
269
  raise ValueError(f"Table '{table_name}' not found in database")
189
270
 
271
+ def get_db_type(self) -> str:
272
+ """
273
+ Get the database type.
274
+
275
+ Returns the specific database type (e.g., POSTGRESQL, MYSQL, SQLITE) by
276
+ inspecting the SQLAlchemy engine. Removes " SQL" suffix if present.
277
+ """
278
+ return self._engine.dialect.name.upper().replace(" SQL", "")
279
+
190
280
  def get_schema(self, *, categorical_threshold: int) -> str: # noqa: PLR0912
191
281
  """
192
282
  Generate schema information from database table.
@@ -196,9 +286,9 @@ class SQLAlchemySource:
196
286
 
197
287
  """
198
288
  inspector = inspect(self._engine)
199
- columns = inspector.get_columns(self._table_name)
289
+ columns = inspector.get_columns(self.table_name)
200
290
 
201
- schema = [f"Table: {self._table_name}", "Columns:"]
291
+ schema = [f"Table: {self.table_name}", "Columns:"]
202
292
 
203
293
  # Build a single query to get all column statistics
204
294
  select_parts = []
@@ -245,13 +335,13 @@ class SQLAlchemySource:
245
335
  if select_parts:
246
336
  try:
247
337
  stats_query = text(
248
- f"SELECT {', '.join(select_parts)} FROM {self._table_name}",
338
+ f"SELECT {', '.join(select_parts)} FROM {self.table_name}",
249
339
  )
250
340
  with self._get_connection() as conn:
251
341
  result = conn.execute(stats_query).fetchone()
252
342
  if result:
253
343
  # Convert result to dict for easier access
254
- column_stats = dict(zip(result._fields, result))
344
+ column_stats = dict(zip(result._fields, result, strict=False))
255
345
  except Exception: # noqa: S110
256
346
  pass # Fall back to no statistics if query fails
257
347
 
@@ -273,7 +363,7 @@ class SQLAlchemySource:
273
363
  # Build UNION query for all categorical columns
274
364
  union_parts = [
275
365
  f"SELECT '{col_name}' as column_name, {col_name} as value "
276
- f"FROM {self._table_name} WHERE {col_name} IS NOT NULL "
366
+ f"FROM {self.table_name} WHERE {col_name} IS NOT NULL "
277
367
  f"GROUP BY {col_name}"
278
368
  for col_name in text_cols_to_query
279
369
  ]
@@ -326,10 +416,14 @@ class SQLAlchemySource:
326
416
  """
327
417
  Execute SQL query and return results as DataFrame.
328
418
 
329
- Args:
330
- query: SQL query to execute
419
+ Parameters
420
+ ----------
421
+ query
422
+ SQL query to execute
331
423
 
332
- Returns:
424
+ Returns
425
+ -------
426
+ :
333
427
  Query results as pandas DataFrame
334
428
 
335
429
  """
@@ -340,11 +434,13 @@ class SQLAlchemySource:
340
434
  """
341
435
  Return the unfiltered data as a DataFrame.
342
436
 
343
- Returns:
437
+ Returns
438
+ -------
439
+ :
344
440
  The complete dataset as a pandas DataFrame
345
441
 
346
442
  """
347
- return self.execute_query(f"SELECT * FROM {self._table_name}")
443
+ return self.execute_query(f"SELECT * FROM {self.table_name}")
348
444
 
349
445
  def _get_sql_type_name(self, type_: sqltypes.TypeEngine) -> str: # noqa: PLR0911
350
446
  """Convert SQLAlchemy type to SQL type name."""
@@ -370,3 +466,15 @@ class SQLAlchemySource:
370
466
  def _get_connection(self) -> Connection:
371
467
  """Get a connection to use for queries."""
372
468
  return self._engine.connect()
469
+
470
+ def cleanup(self) -> None:
471
+ """
472
+ Dispose of the SQLAlchemy engine.
473
+
474
+ Returns
475
+ -------
476
+ None
477
+
478
+ """
479
+ if self._engine:
480
+ self._engine.dispose()
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Optional, Union
4
+
5
+ from shiny import Inputs, Outputs, Session, module, ui
6
+
7
+ if TYPE_CHECKING:
8
+ from pathlib import Path
9
+
10
+ import chatlas
11
+ import sqlalchemy
12
+ from narwhals.stable.v1.typing import IntoFrame
13
+
14
+ from ._datasource import DataSource
15
+
16
+
17
+ def init(
18
+ data_source: IntoFrame | sqlalchemy.Engine,
19
+ table_name: str,
20
+ *,
21
+ greeting: Optional[str | Path] = None,
22
+ data_description: Optional[str | Path] = None,
23
+ extra_instructions: Optional[str | Path] = None,
24
+ prompt_template: Optional[str | Path] = None,
25
+ system_prompt_override: Optional[str] = None,
26
+ client: Optional[Union[chatlas.Chat, str]] = None,
27
+ ):
28
+ """
29
+ Initialize querychat with any compliant data source.
30
+
31
+ **Deprecated.** Use `QueryChat()` instead.
32
+ """
33
+ raise RuntimeError("init() is deprecated. Use QueryChat() instead.")
34
+
35
+
36
+ @module.ui
37
+ def mod_ui(**kwargs) -> ui.TagList:
38
+ """
39
+ Create the UI for the querychat component.
40
+
41
+ **Deprecated.** Use `QueryChat.ui()` instead.
42
+ """
43
+ raise RuntimeError("mod_ui() is deprecated. Use QueryChat.ui() instead.")
44
+
45
+
46
+ @module.server
47
+ def mod_server(
48
+ input: Inputs,
49
+ output: Outputs,
50
+ session: Session,
51
+ querychat_config: Any,
52
+ ):
53
+ """
54
+ Initialize the querychat server.
55
+
56
+ **Deprecated.** Use `QueryChat.server()` instead.
57
+ """
58
+ raise RuntimeError("mod_server() is deprecated. Use QueryChat.server() instead.")
59
+
60
+
61
+ def sidebar(
62
+ id: str,
63
+ width: int = 400,
64
+ height: str = "100%",
65
+ **kwargs,
66
+ ) -> ui.Sidebar:
67
+ """
68
+ Create a sidebar containing the querychat UI.
69
+
70
+ **Deprecated.** Use `QueryChat.sidebar()` instead.
71
+ """
72
+ raise RuntimeError("sidebar() is deprecated. Use QueryChat.sidebar() instead.")
73
+
74
+
75
+ def system_prompt(
76
+ data_source: DataSource,
77
+ *,
78
+ data_description: Optional[str | Path] = None,
79
+ extra_instructions: Optional[str | Path] = None,
80
+ categorical_threshold: int = 20,
81
+ prompt_template: Optional[str | Path] = None,
82
+ ) -> str:
83
+ """
84
+ Create a system prompt for the chat model based on a data source's schema
85
+ and optional additional context and instructions.
86
+
87
+ **Deprecated.** Use `QueryChat.set_system_prompt()` instead.
88
+ """
89
+ raise RuntimeError(
90
+ "system_prompt() is deprecated. Use QueryChat.set_system_prompt() instead."
91
+ )
92
+
93
+
94
+ def greeting(
95
+ querychat_config,
96
+ *,
97
+ generate: bool = True,
98
+ stream: bool = False,
99
+ **kwargs,
100
+ ) -> str | None:
101
+ """
102
+ Generate or retrieve a greeting message.
103
+
104
+ **Deprecated.** Use `QueryChat.generate_greeting()` instead.
105
+ """
106
+ raise RuntimeError(
107
+ "greeting() is deprecated. Use QueryChat.generate_greeting() instead."
108
+ )
@@ -0,0 +1,20 @@
1
+ from typing import Literal
2
+
3
+ from shiny import ui
4
+
5
+ ICON_NAMES = Literal["arrow-counterclockwise", "funnel-fill", "terminal-fill", "table"]
6
+
7
+
8
+ def bs_icon(name: ICON_NAMES) -> ui.HTML:
9
+ """Get Bootstrap icon SVG by name."""
10
+ if name not in BS_ICONS:
11
+ raise ValueError(f"Unknown Bootstrap icon: {name}")
12
+ return ui.HTML(BS_ICONS[name])
13
+
14
+
15
+ BS_ICONS = {
16
+ "arrow-counterclockwise": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-arrow-counterclockwise" style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img"><path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z"></path><path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z"></path></svg>',
17
+ "funnel-fill": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel-fill" viewBox="0 0 16 16"><path d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5z"/></svg>',
18
+ "terminal-fill": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-terminal-fill " style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img" ><path d="M0 3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3zm9.5 5.5h-3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm-6.354-.354a.5.5 0 1 0 .708.708l2-2a.5.5 0 0 0 0-.708l-2-2a.5.5 0 1 0-.708.708L4.793 6.5 3.146 8.146z"></path></svg>',
19
+ "table": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-table " style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img" ><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm15 2h-4v3h4V4zm0 4h-4v3h4V8zm0 4h-4v3h3a1 1 0 0 0 1-1v-2zm-5 3v-3H6v3h4zm-5 0v-3H1v2a1 1 0 0 0 1 1h3zm-4-4h4V8H1v3zm0-4h4V4H1v3zm5-3v3h4V4H6zm4 4H6v3h4V8z"></path></svg>',
20
+ }