geo_chat() connects
GEO metadata tables to a large language model (LLM) so that you can
explore them with natural language: find single-cell studies with
more than 20 samples, which platforms dominate these
datasets?, or filter to post-2020 human studies. Under the
hood it delegates schema summarization, SQL querying, and dashboard
filtering to the querychat package,
so the model never needs the raw table serialized into the prompt — it
queries your local copy with SQL instead.
Three functions cover the whole workflow:
geo_chat() creates and returns a QueryChat
objectgeo_shiny() creates it and immediately launches a Shiny
appgeo_console() creates it and immediately starts a
console chatInstall querychat if it is not already on your system. It is currently archived on CRAN, so install it from the posit-dev R-universe:
To chat with a plain data frame, querychat needs a local SQL
engine: install either duckdb or
RSQLite.
Configure an LLM client (see the next section).
Note: LLM answers are generated from your metadata by an external model, so treat them as suggestions and verify important selections yourself.
The first argument of geo_chat(), client,
is passed to querychat and accepts:
NULL (default): querychat uses the
querychat.client option, the QUERYCHAT_CLIENT
environment variable, or falls back to
ellmer::chat_openai().
An ellmer
Chat object, for full control over model, base URL, and
system prompt:
A provider string understood by ellmer::chat(), such
as "anthropic/claude-sonnet-4-5".
A function returning a Chat object — handy for
OpenAI-compatible endpoints or custom credentials. querychat calls the
function with system_prompt = NULL:
API keys are read from environment variables by ellmer (e.g.,
OPENAI_API_KEY, ANTHROPIC_API_KEY); set them
in your .Renviron.
A typical session starts with geo_search(). The returned
data frame can be handed to geo_chat() directly:
records <- geo_search(
"diabetes[ALL] AND Homo sapiens[ORGN] AND GSE[ETYP]"
)
chat <- geo_chat(
NULL,
records,
data_description = paste(
"GEO DataSets records about diabetes in human, one row per record.",
"The Contains column lists related GSM/GSE/GPL accessions."
)
)data_description gives the model background about what
the columns mean, which noticeably improves query quality. Use
extra_instructions for additional behavioral rules (both
are passed through to querychat).
For richer series-level metadata, build a local database with
geo_meta() first (see vignette("geometadb"))
and chat with that table:
meta <- geo_meta(
records[["Series Accession"]][1:20],
odir = tempdir()
)
qc <- geo_chat(
NULL,
meta,
data_description = paste(
"One row per GEO Series fetched from SOFT metadata.",
"Series_title, Series_summary and Series_overall_design describe each",
"study; Series_sample_id lists its GSM accessions separated by '; '."
)
)The object returned by geo_chat() is a standard
QueryChat object, so you can launch the interface whenever
you are ready:
The convenience wrappers do both steps at once:
The following example needs no LLM access — it only builds and destroys the chat object, which is useful to verify your installation:
records <- data.frame(
`Series Accession` = c("GSE1", "GSE2"),
Title = c("human diabetes study", "mouse liver study"),
Type = c("Expression profiling by array", "RNA-seq"),
Samples = c(12L, 8L),
check.names = FALSE
)
chat <- geo_chat(NULL, records, cleanup = TRUE)
#> Warning: The "visualize" tool requires the {.pkg ggsql} package.
#> ℹ Install it with `install.packages("ggsql")`; continuing without the "visualize" tool.
#> This warning is displayed once per session.
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/Rtmpq6eTaI/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
chat$cleanup()sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] tidyr_1.3.2 ggplot2_4.0.3 stringr_1.6.0 dplyr_1.2.1 geokit_0.0.2
#> [6] rmarkdown_2.32
#>
#> loaded via a namespace (and not attached):
#> [1] querychat_0.4.1 sass_0.4.10 generics_0.1.4 stringi_1.8.9
#> [5] digest_0.6.39 magrittr_2.0.5 evaluate_1.0.5 grid_4.6.1
#> [9] RColorBrewer_1.1-3 fastmap_1.2.0 jsonlite_2.0.0 DBI_1.3.0
#> [13] promises_1.5.0 httr_1.4.9 purrr_1.2.2 scales_1.4.0
#> [17] XML_3.99-0.25 codetools_0.2-20 jquerylib_0.1.4 shiny_1.14.0
#> [21] duckdb_2.0.0 cli_3.6.6 rlang_1.3.0 withr_3.0.3
#> [25] cachem_1.1.0 yaml_2.3.12 otel_0.2.0 tools_4.6.1
#> [29] httpuv_1.6.17 curl_8.1.0 mime_0.13 buildtools_1.0.0
#> [33] vctrs_0.7.3 R6_2.6.1 lifecycle_1.0.5 pkgconfig_2.0.3
#> [37] later_1.4.8 pillar_1.11.1 bslib_0.12.0 rentrez_1.2.4
#> [41] gtable_0.3.6 Rcpp_1.1.2 glue_1.8.1 xfun_0.62
#> [45] tibble_3.3.1 tidyselect_1.2.1 sys_3.4.3 knitr_1.52
#> [49] xtable_1.8-8 farver_2.1.2 htmltools_0.5.9 maketools_1.3.2
#> [53] compiler_4.6.1 S7_0.2.2