diff --git a/.github/workflows/R-CMD-check.yml b/.github/workflows/R-CMD-check.yml new file mode 100644 index 00000000..dbc97381 --- /dev/null +++ b/.github/workflows/R-CMD-check.yml @@ -0,0 +1,27 @@ +# Workflow derived from https://github.com/rstudio/shiny-workflows +# +# NOTE: This Shiny team GHA workflow is overkill for most R packages. +# For most R packages it is better to use https://github.com/r-lib/actions +on: + push: + branches: [main, rc-**] + pull_request: + schedule: + - cron: "0 8 * * 1" # every monday + +name: Package checks + +jobs: + website: + uses: rstudio/shiny-workflows/.github/workflows/website.yaml@v1 + with: + working-directory: ./r-package + routine: + uses: rstudio/shiny-workflows/.github/workflows/routine.yaml@v1 + with: + format-r-code: true + working-directory: ./r-package + R-CMD-check: + uses: rstudio/shiny-workflows/.github/workflows/R-CMD-check.yaml@v1 + with: + working-directory: ./r-package diff --git a/.github/workflows/py-release.yml b/.github/workflows/py-release.yml new file mode 100644 index 00000000..1bb93ed1 --- /dev/null +++ b/.github/workflows/py-release.yml @@ -0,0 +1,58 @@ +name: Python - Release + +on: + release: + types: [published] + +env: + PYTHON_VERSION: 3.12 + +jobs: + pypi-release: + name: Build and release Python package + runs-on: ubuntu-latest + + if: startsWith(github.ref, 'refs/tags/py-v') + + environment: + name: pypi + url: https://pypi.org/project/querychat/ + + permissions: # for trusted publishing + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: ๐Ÿš€ Install uv + uses: astral-sh/setup-uv@v3 + + - name: ๐Ÿ Set up Python ${{ env.PYTHON_VERSION }} + working-directory: ./python-package + run: uv python install ${{ env.PYTHON_VERSION }} + + - name: ๐Ÿ“ฆ Install the project + working-directory: ./python-package + run: uv sync --python ${{ env.PYTHON_VERSION }} --all-extras + + # - name: ๐Ÿงช Check tests + # working-directory: ./python-package + # run: make check-tests + + - name: ๐Ÿ“ Check types + working-directory: ./python-package + run: make check-types + + - name: ๐Ÿ“ Check formatting + working-directory: ./python-package + run: make check-format + + - name: ๐Ÿงณ Build package + working-directory: ./python-package + run: make build + + # TODO: https://pypi.org/manage/project/querychat/settings/publishing/ + - name: ๐Ÿšข Publish release on PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: ./python-package/dist diff --git a/.github/workflows/py-test.yml b/.github/workflows/py-test.yml new file mode 100644 index 00000000..2e973435 --- /dev/null +++ b/.github/workflows/py-test.yml @@ -0,0 +1,53 @@ +name: Test - Python + +on: + workflow_dispatch: + push: + branches: ["main", "rc-*"] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + release: + types: [published] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + config: + - { python-version: "3.9", test_google: false, test_azure: false } + - { python-version: "3.10", test_google: false, test_azure: false } + - { python-version: "3.11", test_google: false, test_azure: false } + - { python-version: "3.12", test_google: true, test_azure: true } + - { python-version: "3.13", test_google: false, test_azure: false } + fail-fast: false + + steps: + - uses: actions/checkout@v4 + + - name: ๐Ÿš€ Install uv + uses: astral-sh/setup-uv@v3 + + - name: ๐Ÿ Set up Python ${{ matrix.config.python-version }} + working-directory: ./python-package + run: uv python install ${{matrix.config.python-version }} + + - name: ๐Ÿ“ฆ Install the project + working-directory: ./python-package + run: uv sync --python ${{ matrix.config.python-version }} --all-extras + + # - name: ๐Ÿงช Check tests + # working-directory: ./python-package + # run: make check-tests + + - name: ๐Ÿ“ Check types + # if: ${{ matrix.config.python-version != '3.9' }} + working-directory: ./python-package + run: make check-types + + - name: ๐Ÿ“ Check formatting + working-directory: ./python-package + run: make check-format diff --git a/.vscode/settings.json b/.vscode/settings.json index 76c2ea3a..5c5e9d63 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,12 @@ { - "python.autoComplete.extraPaths": ["${workspaceFolder}/python-package"] + "python.autoComplete.extraPaths": ["${workspaceFolder}/python-package"], + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff", + }, + "flake8.args": ["--max-line-length=120"] } diff --git a/python-package/.gitignore b/python-package/.gitignore new file mode 100644 index 00000000..07df930a --- /dev/null +++ b/python-package/.gitignore @@ -0,0 +1 @@ +uv.lock diff --git a/python-package/Makefile b/python-package/Makefile index 26d6d3db..a9e24342 100644 --- a/python-package/Makefile +++ b/python-package/Makefile @@ -1,22 +1,58 @@ -.PHONY: build publish create activate publish publish-test install-test +# Inspired by https://github.com/posit-dev/chatlas/blob/main/Makefile -build: - hatch build +.PHONY: setup +setup: ## [py] Setup python environment + uv sync --all-extras +.PHONY: build +build: ## [py] Build python package + @echo "๐Ÿงณ Building python package" + @[ -d dist ] && rm -r dist || true + uv build + +.PHONY: publish publish: hatch publish -create: - hatch env create +.PHONY: check +check: check-format check-types ## [py] Run python checks -activate: - hatch shell +.PHONY: check-types +check-types: ## [py] Run python type checks + @echo "" + @echo "๐Ÿ“ Checking types with pyright" + uv run --with pyright pyright -publish: - hatch publish +.PHONY: check-format +check-format: + @echo "" + @echo "๐Ÿ“ Checking format with ruff" + uv run --with ruff ruff check src/querychat --config pyproject.toml + +.PHONY: format +format: ## [py] Format python code + uv run --with ruff ruff check --fix src/querychat --config pyproject.toml + uv run --with ruff ruff format src/querychat --config pyproject.toml + +.PHONY: check-tox +check-tox: ## [py] Run python 3.9 - 3.12 checks with tox + @echo "" + @echo "๐Ÿ”„ Running tests and type checking with tox for Python 3.9--3.12" + uv run tox run-parallel -publish-test: - hatch publish -r test +.PHONY: help +help: ## Show help messages for make targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; { \ + printf "\033[32m%-18s\033[0m", $$1; \ + if ($$2 ~ /^\[docs\]/) { \ + printf "\033[34m[docs]\033[0m%s\n", substr($$2, 7); \ + } else if ($$2 ~ /^\[py\]/) { \ + printf " \033[33m[py]\033[0m%s\n", substr($$2, 5); \ + } else if ($$2 ~ /^\[r\]/) { \ + printf " \033[31m[r]\033[0m%s\n", substr($$2, 4); \ + } else { \ + printf " %s\n", $$2; \ + } \ + }' -install-test: - python3 -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ querychat +.DEFAULT_GOAL := help diff --git a/python-package/pyproject.toml b/python-package/pyproject.toml index a3ae1762..17eba3f3 100644 --- a/python-package/pyproject.toml +++ b/python-package/pyproject.toml @@ -7,11 +7,9 @@ name = "querychat" version = "0.1.0" description = "Chat with your data using natural language" readme = "README.md" -requires-python = ">=3.8" -license = {file = "LICENSE"} -authors = [ - {name = "Posit", email = "info@posit.co"}, -] +requires-python = ">=3.9" +license = { file = "LICENSE" } +authors = [{ name = "Posit", email = "info@posit.co" }] dependencies = [ "duckdb", "pandas", @@ -41,8 +39,144 @@ Source = "https://github.com/posit-dev/querychat/tree/main/python-package" packages = ["querychat"] [tool.hatch.build.targets.sdist] -include = [ - "querychat", - "LICENSE", - "README.md", +include = ["src/querychat", "LICENSE", "README.md"] + +[tool.uv] +dev-dependencies = ["ruff>=0.6.5", "pyright>=1.1.401", "tox-uv>=1.11.4"] + +[tool.ruff] +src = ["src/querychat"] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", + "app.py", # ignore examples for now +] + +line-length = 88 +indent-width = 4 + +target-version = "py39" + +# [tool.ruff.lint] +# select = ['E', 'F', 'W', 'A', 'PLC', 'PLE', 'PLW', 'I'] +[tool.ruff.lint] +extend-ignore = [ + "A002", # Shadowing a built-in + "ARG001", # Unused argument + "D200", # One-line docstring should fit on one line with quotes + "D203", # 1 blank line required before class docstring + "D212", # Multi-line docstring summary should start at the first line + "E501", # Line too long + "ISC001", # single-line-implicit-string-concatenation + "ISC002", # multi-line-implicit-string-concatenation + "PD901", # Avoid using the generic variable name `df` for DataFrames + "PLR0913", # Too many arguments in function definition + "PLR0915", # Too many statements in function + "RET504", # Unnecessary assignment to `{name}` before `return` statement + "RET505", # Unnecessary branch after `return` statement + "UP007", # Use `X | Y` for type annotations (or Optional[X]) + # TODO: Remove in the future, when we have docstrings. + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D104", # Missing docstring in public package + "D107", # Missing docstring in __init__ + "D205", # 1 blank line required between summary line and description +] +extend-select = [ + # "C90", # C90; mccabe: https://docs.astral.sh/ruff/rules/complex-structure/ + "ASYNC", # ASYNC; flake8-async: https://docs.astral.sh/ruff/rules/#flake8-async-async + "S", # S; flake8-bandit: https://docs.astral.sh/ruff/rules/#flake8-bandit-s + "FBT", # FBT; flake8-boolean-trap: https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt + "B", # B; flake8-bugbear: https://docs.astral.sh/ruff/rules/#flake8-bugbear-b + "A", # A; flake8-builtins: https://docs.astral.sh/ruff/rules/#flake8-builtins-a + "COM", # COM; Commas: https://docs.astral.sh/ruff/rules/#flake8-commas-com + "C4", # C4; flake8-comprehensions: https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 + "DTZ", # DTZ; flake8-datetimez: https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + "T10", # T10; flake8-dbugger: https://docs.astral.sh/ruff/rules/#flake8-debugger-t10 + "FA", # FA; flake8-future-annotations: https://docs.astral.sh/ruff/rules/#flake8-future-annotations-fa + "ISC", # ISC; flake8-implicit-str-concat: https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc + "ICN", # ICN; flake8-import-conventions: https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn + "PIE", # PIE; flake8-pie: https://docs.astral.sh/ruff/rules/#flake8-pie-pie + "PYI", # PYI; flake8-pyi : https://docs.astral.sh/ruff/rules/#flake8-pyi-pyi + "PT", # PT; flake8-pytest-style: https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt + "Q", # Q; flake8-quotes: https://docs.astral.sh/ruff/rules/#flake8-quotes-q + "RET", # RET; flake8-return: https://docs.astral.sh/ruff/rules/#flake8-return-ret + "SIM", # SIM; flake8-simplify: https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + "TID253", # banned-module-level-imports: https://docs.astral.sh/ruff/rules/banned-module-level-imports/#banned-module-level-imports-tid253 + "TC", # TC; flake8-type-checking: https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch + "TD", # TD; flake8-todos: https://docs.astral.sh/ruff/rules/#flake8-todosimports-td + "ARG", # ARG; flake8-argparse: https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg + "PTH", # PTH; flake8-use-pathlib: https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth + "I", # I; isort: https://docs.astral.sh/ruff/rules/#isort-i + "NPY", # NPY; NumPy-specific rules: https://docs.astral.sh/ruff/rules/#numpy-specific-rules-npy + "PD", # PD; pandas-vet: https://docs.astral.sh/ruff/rules/#pandas-vet-pd + "N", # N; pep8-naming: https://docs.astral.sh/ruff/rules/#pep8-naming-n + "PERF", # PERF; flake8-performance: https://docs.astral.sh/ruff/rules/#flake8-performance-perf + "E", # E; pycodestyle: https://docs.astral.sh/ruff/rules/#pycodestyle-e-w + "W", # W; pycodestyle: https://docs.astral.sh/ruff/rules/#pycodestyle-e-w + "D", # D; pydocstyle: https://docs.astral.sh/ruff/rules/#pydocstyle-d + "F", # F; Pyflakes: https://docs.astral.sh/ruff/rules/#pyflakes-f + "PGH", # PGH; pygrep-hooks: https://docs.astral.sh/ruff/rules/#pygrep-hooks-pgh + "PL", # PL; pylint: https://docs.astral.sh/ruff/rules/#pylint-pl + "UP", # UP; pyupgrade: https://docs.astral.sh/ruff/rules/#pyupgrade-up + "FURB", # FURB; refurb: https://docs.astral.sh/ruff/rules/#refurb-furb + "RUF", # RUF; Ruff specific rules: https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf ] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = true +docstring-code-line-length = "dynamic" + +[tool.pyright] +include = ["src/querychat"] + + +# For more tox testing usage (in addition to typing), see: +# https://github.com/posit-dev/chatlas/blob/b91c020a555917c1be5ae2462496de25d82c529d/pyproject.toml#L122-L139 +[tool.tox] +legacy_tox_ini = """ +[tox] +env_list = py3{9,10,11,12} +isolated_build = True + +[testenv] +package = wheel +wheel_build_env = .pkg +commands = pyright +""" diff --git a/python-package/src/querychat/__init__.py b/python-package/src/querychat/__init__.py index 5ad87cf6..3aa1ef83 100644 --- a/python-package/src/querychat/__init__.py +++ b/python-package/src/querychat/__init__.py @@ -1,3 +1,3 @@ -from querychat.querychat import init, ui, sidebar, server +from querychat.querychat import init, server, sidebar, ui -__all__ = ["init", "ui", "sidebar", "server"] +__all__ = ["init", "server", "sidebar", "ui"] diff --git a/python-package/src/querychat/querychat.py b/python-package/src/querychat/querychat.py index c6a4f4e5..f8763e0d 100644 --- a/python-package/src/querychat/querychat.py +++ b/python-package/src/querychat/querychat.py @@ -1,19 +1,19 @@ from __future__ import annotations -import sys -import os import re -import pandas as pd -import duckdb -import json +import sys from functools import partial -from typing import List, Dict, Any, Callable, Optional, Union, Protocol +from pathlib import Path +from typing import TYPE_CHECKING, Any, Optional, Protocol, Union import chatlas -from htmltools import TagList, tags, HTML -from shiny import module, reactive, ui, Inputs, Outputs, Session +import duckdb import narwhals as nw -from narwhals.typing import IntoFrame +from shiny import Inputs, Outputs, Session, module, reactive, ui + +if TYPE_CHECKING: + import pandas as pd + from narwhals.typing import IntoFrame def system_prompt( @@ -44,13 +44,13 @@ def system_prompt( ------- str The system prompt for the chat model. + """ schema = df_to_schema(df, table_name, categorical_threshold) # Read the prompt file - prompt_path = os.path.join(os.path.dirname(__file__), "prompt", "prompt.md") - with open(prompt_path, "r") as f: - prompt_text = f.read() + prompt_path = Path(__file__).parent / "prompt" / "prompt.md" + prompt_text = prompt_path.read_text() # Simple template replacement (a more robust template engine could be used) if data_description: @@ -67,7 +67,8 @@ def system_prompt( prompt_text = prompt_text.replace("{{schema}}", schema) prompt_text = prompt_text.replace("{{data_description}}", data_description_section) prompt_text = prompt_text.replace( - "{{extra_instructions}}", extra_instructions or "" + "{{extra_instructions}}", + extra_instructions or "", ) return prompt_text @@ -90,8 +91,8 @@ def df_to_schema(df: IntoFrame, table_name: str, categorical_threshold: int) -> ------- str A string containing the schema information. - """ + """ ndf = nw.from_native(df) schema = [f"Table: {table_name}", "Columns:"] @@ -150,12 +151,16 @@ def df_to_html(df: IntoFrame, maxrows: int = 5) -> str: ------- str HTML string representation of the table + """ ndf = nw.from_native(df) df_short = nw.from_native(df).head(maxrows) # Generate HTML table - table_html = df_short.to_pandas().to_html(index=False, classes="table table-striped") + table_html = df_short.to_pandas().to_html( + index=False, + classes="table table-striped", + ) # Add note about truncated rows if needed if len(df_short) != len(ndf): @@ -225,11 +230,12 @@ def init( ------- QueryChatConfig A QueryChatConfig object that can be passed to server() + """ # Validate table name (must begin with letter, contain only letters, numbers, underscores) if not re.match(r"^[a-zA-Z][a-zA-Z0-9_]*$", table_name): raise ValueError( - "Table name must begin with a letter and contain only letters, numbers, and underscores" + "Table name must begin with a letter and contain only letters, numbers, and underscores", ) # Process greeting @@ -243,7 +249,10 @@ def init( # Create the system prompt if system_prompt_override is None: _system_prompt = system_prompt( - df, table_name, data_description, extra_instructions + df, + table_name, + data_description, + extra_instructions, ) else: _system_prompt = system_prompt_override @@ -254,7 +263,8 @@ def init( # Default chat function if none provided create_chat_callback = create_chat_callback or partial( - chatlas.ChatOpenAI, model="gpt-4o" + chatlas.ChatOpenAI, + model="gpt-4o", ) return QueryChatConfig( @@ -271,13 +281,19 @@ def mod_ui() -> ui.TagList: """ Create the UI for the querychat component. + Parameters + ---------- + id : str + The module ID + Returns ------- ui.TagList A UI component. + """ # Include CSS - css_path = os.path.join(os.path.dirname(__file__), "static", "css", "styles.css") + css_path = Path(__file__).parent / "static" / "css" / "styles.css" return ui.TagList( ui.include_css(css_path), @@ -306,6 +322,7 @@ def sidebar(id: str, width: int = 400, height: str = "100%", **kwargs) -> ui.Sid ------- ui.Sidebar A sidebar UI component. + """ return ui.sidebar( mod_ui(id), @@ -316,9 +333,12 @@ def sidebar(id: str, width: int = 400, height: str = "100%", **kwargs) -> ui.Sid @module.server -def server( - input: Inputs, output: Outputs, session: Session, querychat_config: QueryChatConfig -) -> Dict[str, Any]: +def server( # noqa: D417 + input: Inputs, + output: Outputs, + session: Session, + querychat_config: QueryChatConfig, +) -> dict[str, Any]: """ Initialize the querychat server. @@ -329,15 +349,16 @@ def server( Returns ------- - Dict[str, Any] + dict[str, Any] A dictionary with reactive components: - sql: A reactive that returns the current SQL query. - title: A reactive that returns the current title. - df: A reactive that returns the filtered data frame. - chat: The chat object. + """ - @reactive.Effect + @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. @@ -351,10 +372,10 @@ def _(): create_chat_callback = querychat_config.create_chat_callback # Reactive values to store state - current_title = reactive.Value(None) - current_query = reactive.Value("") + current_title = reactive.value[Union[str, None]](None) + current_query = reactive.value("") - @reactive.Calc + @reactive.calc def filtered_df(): if current_query.get() == "": return df @@ -369,7 +390,7 @@ async def append_output(text): # The function that updates the dashboard with a new SQL query async def update_dashboard(query: str, title: str): """ - Modifies the data presented in the data dashboard, based on the given SQL query, + Modify the data presented in the data dashboard, based on the given SQL query, and also updates the title. Parameters @@ -378,8 +399,8 @@ async def update_dashboard(query: str, title: str): A DuckDB SQL query; must be a SELECT statement. title : str A title to display at the top of the data dashboard, summarizing the intent of the SQL query. - """ + """ await append_output(f"\n```sql\n{query}\n```\n\n") try: @@ -404,8 +425,8 @@ async def query(query: str): ---------- query A DuckDB SQL query; must be a SELECT statement. - """ + """ await append_output(f"\n```sql\n{query}\n```\n\n") try: diff --git a/r-package/DESCRIPTION b/r-package/DESCRIPTION index de15fd5a..6319c056 100644 --- a/r-package/DESCRIPTION +++ b/r-package/DESCRIPTION @@ -5,15 +5,14 @@ Authors@R: c( person("Joe", "Cheng", , "joe@posit.co", role = c("aut", "cre")), person("Posit Software, PBC", role = c("cph", "fnd")) ) -Description: Adds an LLM-powered chatbot to your 'shiny' app, that can turn your - users' natural language questions into SQL queries that run against your data, - and return the result as a reactive dataframe. Use it to drive reactive - calculations, visualizations, downloads, etc. +Description: Adds an LLM-powered chatbot to your 'shiny' app, that can + turn your users' natural language questions into SQL queries that run + against your data, and return the result as a reactive dataframe. Use + it to drive reactive calculations, visualizations, downloads, etc. License: MIT + file LICENSE -Encoding: UTF-8 -Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.2 -Imports: +Depends: + R (>= 4.1.0) +Imports: bslib, DBI, duckdb, @@ -27,3 +26,6 @@ Imports: shinychat, whisker, xtable +Encoding: UTF-8 +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.3.2 diff --git a/r-package/R/prompt.R b/r-package/R/prompt.R index fc0933e3..75ac68b6 100644 --- a/r-package/R/prompt.R +++ b/r-package/R/prompt.R @@ -1,5 +1,5 @@ #' Create a system prompt for the chat model -#' +#' #' This function generates a system prompt for the chat model based on a data frame's #' schema and optional additional context and instructions. #' @@ -8,11 +8,17 @@ #' @param data_description Optional description of the data, in plain text or Markdown format. #' @param extra_instructions Optional additional instructions for the chat model, in plain text or Markdown format. #' @param categorical_threshold The maximum number of unique values for a text column to be considered categorical. -#' +#' #' @return A string containing the system prompt for the chat model. #' #' @export -querychat_system_prompt <- function(df, name, data_description = NULL, extra_instructions = NULL, categorical_threshold = 10) { +querychat_system_prompt <- function( + df, + name, + data_description = NULL, + extra_instructions = NULL, + categorical_threshold = 10 +) { schema <- df_to_schema(df, name, categorical_threshold) if (!is.null(data_description)) { @@ -27,14 +33,21 @@ querychat_system_prompt <- function(df, name, data_description = NULL, extra_ins prompt_content <- readLines(prompt_path, warn = FALSE) prompt_text <- paste(prompt_content, collapse = "\n") - whisker::whisker.render(prompt_text, list( - schema = schema, - data_description = data_description, - extra_instructions = extra_instructions - )) + whisker::whisker.render( + prompt_text, + list( + schema = schema, + data_description = data_description, + extra_instructions = extra_instructions + ) + ) } -df_to_schema <- function(df, name = deparse(substitute(df)), categorical_threshold) { +df_to_schema <- function( + df, + name = deparse(substitute(df)), + categorical_threshold +) { schema <- c(paste("Table:", name), "Columns:") column_info <- lapply(names(df), function(column) { diff --git a/r-package/R/querychat.R b/r-package/R/querychat.R index 5196b1e8..891081dd 100644 --- a/r-package/R/querychat.R +++ b/r-package/R/querychat.R @@ -30,7 +30,7 @@ #' @returns An object that can be passed to `querychat_server()` as the #' `querychat_config` argument. By convention, this object should be named #' `querychat_config`. -#' +#' #' @export querychat_init <- function( df, @@ -39,7 +39,12 @@ querychat_init <- function( data_description = NULL, extra_instructions = NULL, create_chat_func = purrr::partial(ellmer::chat_openai, model = "gpt-4o"), - system_prompt = querychat_system_prompt(df, tbl_name, data_description = data_description, extra_instructions = extra_instructions) + system_prompt = querychat_system_prompt( + df, + tbl_name, + data_description = data_description, + extra_instructions = extra_instructions + ) ) { is_tbl_name_ok <- is.character(tbl_name) && length(tbl_name) == 1 && @@ -124,28 +129,28 @@ querychat_ui <- function(id) { ns <- shiny::NS(id) htmltools::tagList( # TODO: Make this into a proper HTML dependency - shiny::includeCSS(system.file("www","styles.css", package = "querychat")), + shiny::includeCSS(system.file("www", "styles.css", package = "querychat")), shinychat::chat_ui(ns("chat"), height = "100%", fill = TRUE) ) } #' Initalize the querychat server -#' +#' #' @param id The ID of the module instance. Must match the ID passed to #' the corresponding call to `querychat_ui()`. #' @param querychat_config An object created by `querychat_init()`. -#' +#' #' @returns A querychat instance, which is a named list with the following #' elements: -#' +#' #' - `sql`: A reactive that returns the current SQL query. #' - `title`: A reactive that returns the current title. #' - `df`: A reactive that returns the data frame, filtered and sorted by the #' current SQL query. #' - `chat`: The [ellmer::Chat] object that powers the chat interface. -#' +#' #' By convention, this object should be named `querychat_config`. -#' +#' #' @export querychat_server <- function(id, querychat_config) { shiny::moduleServer(id, function(input, output, session) {