diff --git a/DESCRIPTION b/DESCRIPTION index b8128f916..868ce116e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -134,6 +134,7 @@ Collate: 'report.survreg.R' 'report.test_performance.R' 'report.zeroinfl.R' + 'report_ai.R' 'report_effectsize.R' 'report_htest_chi2.R' 'report_htest_cor.R' diff --git a/NAMESPACE b/NAMESPACE index b835971fd..3447bd881 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -13,6 +13,7 @@ S3method(format_model,character) S3method(format_model,default) S3method(print,cite_easystats) S3method(print,report) +S3method(print,report_ai) S3method(print,report_effectsize) S3method(print,report_info) S3method(print,report_intercept) @@ -60,6 +61,11 @@ S3method(report,stanreg) S3method(report,survreg) S3method(report,test_performance) S3method(report,zeroinfl) +S3method(report_ai,default) +S3method(report_ai,glm) +S3method(report_ai,glmmTMB) +S3method(report_ai,lm) +S3method(report_ai,merMod) S3method(report_effectsize,MixMod) S3method(report_effectsize,anova) S3method(report_effectsize,aov) @@ -299,6 +305,7 @@ export(is.report) export(print_html) export(print_md) export(report) +export(report_ai) export(report_date) export(report_effectsize) export(report_info) diff --git a/NEWS.md b/NEWS.md index 6a32e9e76..4baaa9f62 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,17 @@ # report 0.6.4 +New features + +* `report_ai()`: add support for `glm`, `merMod` (lme4), and `glmmTMB` model classes. + +* `report_ai()`: `## Model` section now includes a CI / degrees-of-freedom estimation line (e.g., `Inference: 95% CI [Satterthwaite df]`) when the information is available from `parameters::model_parameters()`. + +* `report_ai.default()`: instead of stopping with an error, now emits a warning and falls back to the standard `report()` output so that documents continue to render for unsupported model classes. + +* `report()`: new `audience` argument (`"humans"` (default) or `"ai"`). When `"ai"`, `report()` delegates to `report_ai()`. The default can be set globally via `options(report_audience = "ai")`. + +* New vignette: *AI-Optimized Reports* — explains `report_ai()`, the `audience` argument, and how to convert an entire Quarto document with a single option. + Bug fixes * `report_participants()`: fix CRAN failure on r-devel due to `row names contain missing values` error by replacing `datawizard::data_tabulate()` with a direct `table()` call for country and race frequency tables (#593). diff --git a/R/report.R b/R/report.R index 70b8fe313..d641e99fd 100644 --- a/R/report.R +++ b/R/report.R @@ -20,6 +20,16 @@ #' #' @param x The R object that you want to report (see list of of supported #' objects above). +#' @param audience The intended audience for the report. `"humans"` (default) +#' produces the standard narrative text report. `"ai"` produces a compact, +#' structured Markdown output designed for consumption by a Large Language +#' Model (LLM) or AI agent. It strikes a careful balance between +#' comprehensiveness, specificity, and compactness, giving the model the +#' clearest and most relevant analytical information at the lowest possible +#' token cost. The output is a single character vector of class `report_ai` +#' that can be pasted directly into a chat window or fed to an LLM API. +#' The default can be changed globally with `options(report_audience = "ai")`. +#' See `vignette("report_ai", package = "report")` for details and examples. #' @param ... Arguments passed to or from other methods. #' #' @details @@ -97,7 +107,11 @@ #' summary(as.data.frame(r)) #' #' @export -report <- function(x, ...) { +report <- function(x, ..., audience = getOption("report_audience", "humans")) { + audience <- match.arg(audience, c("humans", "ai")) + if (audience == "ai") { + return(report_ai(x, ...)) + } UseMethod("report") } diff --git a/R/report_ai.R b/R/report_ai.R new file mode 100644 index 000000000..e7a123892 --- /dev/null +++ b/R/report_ai.R @@ -0,0 +1,265 @@ +# Internal generic — use report(x, audience = "ai") instead. +report_ai <- function(x, ...) { + UseMethod("report_ai") +} + +report_ai.default <- function(x, ...) { + insight::format_warning( + paste0( + "AI-optimized reports are not yet available for objects of class '", + class(x)[1], + "'. Falling back to report()." + ) + ) + report(x, ..., audience = "humans") +} + +report_ai.lm <- function(x, ...) { + .report_ai_models(x, ...) +} + +report_ai.glm <- report_ai.lm + +report_ai.merMod <- function(x, ...) { + .report_ai_models(x, ...) +} + +report_ai.glmmTMB <- function(x, ...) { + .report_ai_models(x, ...) +} + + +# --- Internal Workhorse Function --- +.report_ai_models <- function(x, ...) { + mi <- insight::model_info(x) + dat <- insight::get_data(x) + n_obs <- insight::n_obs(x) + form <- insight::find_formula(x) + + func_name <- tryCatch( + { + dep <- insight::safe_deparse(insight::get_call(x)[[1]]) + sub(".*::", "", dep) + }, + error = function(e) class(x)[1] + ) + mod_family <- if (is.null(mi$family)) "Unknown" else mi$family + + model_vars_list <- insight::find_variables(x) + # Use only response + conditional (fixed) variables for descriptives; + # random grouping variables (e.g. Subject) are excluded. + fixed_var_comps <- intersect( + c("response", "conditional"), + names(model_vars_list) + ) + fixed_vars <- unique(unlist( + model_vars_list[fixed_var_comps], + use.names = FALSE + )) + fixed_vars <- fixed_vars[fixed_vars %in% colnames(dat)] + + if (length(fixed_vars) > 0) { + desc_report <- suppressWarnings(summary(report::report( + dat[, fixed_vars, drop = FALSE], + audience = "humans" + ))) + desc_lines <- unlist(strsplit( + as.character(desc_report), + "\n", + fixed = TRUE + )) + + if (length(desc_lines) > 1) { + # Use trimws() to kill the spaces that cause nested bullets + clean_lines <- trimws(desc_lines[-1]) + desc_str <- paste(clean_lines, collapse = "\n") + } else { + desc_str <- paste(trimws(desc_lines), collapse = "\n") + } + } else { + desc_str <- "- No variables found." + } + + params <- parameters::model_parameters(x, ...) + + # Separate fixed and random effects to avoid duplicated table headers + # (model_parameters returns both in one table for mixed models) + has_random <- "Effects" %in% + names(params) && + any(!is.na(params$Effects) & params$Effects != "fixed") + + if (has_random) { + fixed_params <- params[ + !is.na(params$Effects) & params$Effects == "fixed", + , + drop = FALSE + ] + random_params <- params[ + !is.na(params$Effects) & params$Effects != "fixed", + , + drop = FALSE + ] + } else { + fixed_params <- params + random_params <- NULL + } + + param_table <- insight::format_table(fixed_params) + param_markdown <- insight::export_table(param_table, format = "markdown") + param_str <- paste(param_markdown, collapse = "\n") + + # Format random effect variances as metadata bullet points + random_str <- NULL + if (!is.null(random_params) && nrow(random_params) > 0) { + coef_col <- intersect( + c("Coefficient", "Estimate", "SD"), + names(random_params) + )[1] + random_str <- paste( + vapply( + seq_len(nrow(random_params)), + function(i) { + param_row <- random_params[i, , drop = FALSE] + param_name <- if ("Parameter" %in% names(param_row)) { + as.character(param_row$Parameter) + } else { + "?" + } + group_tag <- if ( + "Group" %in% + names(param_row) && + !is.na(param_row$Group) && + nzchar(as.character(param_row$Group)) + ) { + paste0(" [", param_row$Group, "]") + } else { + "" + } + val <- if (!is.na(coef_col) && coef_col %in% names(param_row)) { + sprintf("%.3f", as.numeric(param_row[[coef_col]])) + } else { + "?" + } + paste0("- ", param_name, group_tag, ": ", val) + }, + character(1) + ), + collapse = "\n" + ) + } + + perf <- performance::model_performance(x, ...) + perf_table <- insight::format_table(perf) + perf_markdown <- insight::export_table(perf_table, format = "markdown") + perf_str <- paste(perf_markdown, collapse = "\n") + + if ("p" %in% names(fixed_params) && "Parameter" %in% names(fixed_params)) { + sig_effects <- fixed_params$Parameter[ + !is.na(fixed_params$p) & + fixed_params$p < 0.05 & + fixed_params$Parameter != "(Intercept)" + ] + highlights_str <- if (length(sig_effects) == 0) { + "- Significant effects: None" + } else { + sprintf( + "- Significant effects (p < 0.05): %s", + toString(sig_effects) + ) + } + } else { + highlights_str <- "- Significant effects: Could not be determined." + } + + formula_str <- if (is.list(form)) { + Reduce(paste, deparse(form$conditional)) + } else { + Reduce(paste, deparse(form)) + } + + # CI / degrees-of-freedom estimation method + ci_level <- attr(params, "ci") + ci_method <- attr(params, "ci_method") + if (!is.null(ci_level) && !is.null(ci_method)) { + ci_pct <- sprintf("%.0f%%", ci_level * 100) + ci_label <- .ci_method_label(ci_method) + inference_str <- paste0("- Inference: ", ci_pct, " CI [", ci_label, "]") + } else if (is.null(ci_level)) { + inference_str <- NULL + } else { + inference_str <- paste0( + "- Inference: ", + sprintf("%.0f%%", ci_level * 100), + " CI" + ) + } + + param_section <- if (is.null(random_str)) { + paste0("## Parameters\n", param_str) + } else { + paste0("## Parameters\n", param_str, "\n\n### Random Effects\n", random_str) + } + + model_section <- paste0( + "## Model\n", + "- Call: ", + func_name, + "\n", + "- Formula: ", + formula_str, + "\n", + "- Family: ", + mod_family, + "\n", + "- N: ", + n_obs, + if (is.null(inference_str)) "" else paste0("\n", inference_str) + ) + + res <- paste0( + model_section, + "\n\n", + "## Variables\n", + desc_str, + "\n\n", + param_section, + "\n\n", + "## Performance\n", + perf_str, + "\n\n", + "## Highlights\n", + highlights_str + ) + + class(res) <- c("report_ai", "character") + res +} + +# Helper: human-readable CI / df-method label +.ci_method_label <- function(method) { + method_labels <- c( + wald = "Wald", + residual = "Residual df (t/F)", + satterthwaite = "Satterthwaite df", + kenward = "Kenward-Roger df", + normal = "Normal (z)", + profile = "Profile likelihood", + boot = "Bootstrap", + uniroot = "Uniroot", + hdi = "HDI", + eti = "ETI", + si = "SI" + ) + lab <- method_labels[tolower(as.character(method))] + if (is.na(lab)) { + tools::toTitleCase(tolower(as.character(method))) + } else { + unname(lab) + } +} + +#' @export +print.report_ai <- function(x, ...) { + cat(x, "\n") + invisible(x) +} diff --git a/man/report.Rd b/man/report.Rd index c795eee64..181175af8 100644 --- a/man/report.Rd +++ b/man/report.Rd @@ -4,13 +4,19 @@ \alias{report} \title{Automatic reporting of R objects} \usage{ -report(x, ...) +report(x, ..., audience = getOption("report_audience", "humans")) } \arguments{ \item{x}{The R object that you want to report (see list of of supported objects above).} \item{...}{Arguments passed to or from other methods.} + +\item{audience}{The intended audience for the report. \code{"humans"} (default) +produces the standard formatted text report. \code{"ai"} produces a compact, +structured output optimised for consumption by a Large Language Model (LLM) +or AI agent via \code{\link[=report_ai]{report_ai()}}. The default can be changed globally with +\code{options(report_audience = "ai")}.} } \value{ A list-object of class \code{report}, which contains further diff --git a/man/report_ai.Rd b/man/report_ai.Rd new file mode 100644 index 000000000..f2488289b --- /dev/null +++ b/man/report_ai.Rd @@ -0,0 +1,45 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/report_ai.R +\name{report_ai} +\alias{report_ai} +\alias{report_ai.merMod} +\alias{report_ai.glmmTMB} +\title{Generate AI-optimized reports} +\usage{ +report_ai(x, ...) + +\method{report_ai}{merMod}(x, ...) + +\method{report_ai}{glmmTMB}(x, ...) +} +\arguments{ +\item{x}{A statistical model.} + +\item{...}{Arguments passed to other functions, like \code{parameters::model_parameters()}, +\code{performance::model_performance()} or \code{insight::format_table()}.} +} +\value{ +A character vector of class \code{report_ai} containing the formatted text. +} +\description{ +This function is designed to produce AI-optimized output for statistical models. +It strikes a careful balance between comprehensiveness, specificity, and compactness. +The primary goal is to provide a Large Language Model (LLM) or AI agent with the +clearest and most relevant analytical information at the lowest possible token cost. +} +\examples{ +m <- lm(mpg ~ wt + hp, data = mtcars) +report_ai(m) +\dontshow{if (requireNamespace("lme4", quietly = TRUE)) withAutoprint(\{ # examplesIf} +\donttest{ +m <- lme4::lmer(Reaction ~ Days + (1 | Subject), data = lme4::sleepstudy) +report_ai(m) +} +\dontshow{\}) # examplesIf} +\dontshow{if (requireNamespace("glmmTMB", quietly = TRUE)) withAutoprint(\{ # examplesIf} +\donttest{ +m <- glmmTMB::glmmTMB(count ~ mined + (1 | site), family = poisson(), data = glmmTMB::Salamanders) +report_ai(m) +} +\dontshow{\}) # examplesIf} +} diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index 76ad46fa9..66b56df42 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -1,103 +1,106 @@ url: https://easystats.github.io/report/ template: - bootstrap: 5 - package: easystatstemplate + bootstrap: 5 + package: easystatstemplate navbar: - type: default - left: - - text: Tutorials - icon: fa fa-book-reader - href: articles/index.html - aria-label: Articles - menu: - - text: Report and Cite Packages - href: articles/cite_packages.html - - text: Supporting New Models - href: articles/new_models.html - - text: 'Automated Reporting: Getting Started' - href: articles/report.html - - text: Publication-ready Tables - href: articles/report_table.html - - text: Functions - icon: fa fa-file-code - href: reference/index.html - aria-label: Reference - - text: News - icon: fa fa-newspaper - href: news/index.html - aria-label: News - - text: Help - icon: fa fa-question-circle - href: SUPPORT.html - aria-label: Support + type: default + left: + - text: Tutorials + icon: fa fa-book-reader + href: articles/index.html + aria-label: Articles + menu: + - text: Report and Cite Packages + href: articles/cite_packages.html + - text: Supporting New Models + href: articles/new_models.html + - text: "Automated Reporting: Getting Started" + href: articles/report.html + - text: Publication-ready Tables + href: articles/report_table.html + - text: AI-Optimized Reports + href: articles/report_ai.html + - text: Functions + icon: fa fa-file-code + href: reference/index.html + aria-label: Reference + - text: News + icon: fa fa-newspaper + href: news/index.html + aria-label: News + - text: Help + icon: fa fa-question-circle + href: SUPPORT.html + aria-label: Support articles: -- title: Tutorials - navbar: ~ - contents: - - cite_packages - - new_models - - report - - report_table + - title: Tutorials + navbar: ~ + contents: + - cite_packages + - new_models + - report + - report_table + - report_ai reference: - - title: Report Statistical Information - desc: | - Main functions for reporting statistical information - contents: - - report_effectsize - - report_info - - report_intercept - - report_model - - report_parameters - - report_participants - - report_performance - - report_priors - - report_random - - report_s - - report_sample - - report_statistics + - title: Report Statistical Information + desc: | + Main functions for reporting statistical information + contents: + - report_effectsize + - report_info + - report_intercept + - report_model + - report_parameters + - report_participants + - report_performance + - report_priors + - report_random + - report_s + - report_sample + - report_statistics - - title: Formatting - desc: | - Functions for formatting content - contents: - - cite_easystats - - format_citation - - format_formula - - format_model - - as.report_text - - report - - report.default - - report_table - - report_text + - title: Formatting + desc: | + Functions for formatting content + contents: + - cite_easystats + - format_citation + - format_formula + - format_model + - as.report_text + - report + - report.default + - report_table + - report_text - - title: Report Statistical Objects - desc: | - Helper functions for reporting of statistical objects - contents: - - report.aov - - report.bayesfactor_models - - report.brmsfit - - report.compare_performance - - report.htest - - report.lavaan - - report.lm - - report.stanreg - - report.test_performance - - report.estimate_contrasts - - report.compare.loo - - report.BFBayesFactor + - title: Report Statistical Objects + desc: | + Helper functions for reporting of statistical objects + contents: + - report.aov + - report.bayesfactor_models + - report.brmsfit + - report.compare_performance + - report.htest + - report.lavaan + - report.lm + - report.stanreg + - report.test_performance + - report.estimate_contrasts + - report.compare.loo + - report.BFBayesFactor - - title: Report Non-Statistical Objects - desc: | - Helper functions for reporting of non-statistical objects - contents: - - report.numeric - - report.character - - report.factor - - report.data.frame - - report_date - - report.sessionInfo + - title: Report Non-Statistical Objects + desc: | + Helper functions for reporting of non-statistical objects + contents: + - report.numeric + - report.character + - report.factor + - report.data.frame + - report_date + - report.sessionInfo diff --git a/tests/testthat/test-report_ai.R b/tests/testthat/test-report_ai.R new file mode 100644 index 000000000..66f6c850b --- /dev/null +++ b/tests/testthat/test-report_ai.R @@ -0,0 +1,163 @@ +test_that("report_ai.lm - basic structure", { + m <- lm(mpg ~ wt + hp, data = mtcars) + result <- report_ai(m) + + expect_s3_class(result, "report_ai") + expect_s3_class(result, "character") + expect_match(result, "## Model", fixed = TRUE) + expect_match(result, "## Variables", fixed = TRUE) + expect_match(result, "## Parameters", fixed = TRUE) + expect_match(result, "## Performance", fixed = TRUE) + expect_match(result, "## Highlights", fixed = TRUE) + # lm has no random effects section + expect_false(grepl("### Random Effects", result, fixed = TRUE)) + # CI / df method line + expect_match(result, "Inference:", fixed = TRUE) +}) + +test_that("report_ai.lm - model metadata", { + m <- lm(mpg ~ wt + hp, data = mtcars) + result <- report_ai(m) + + expect_match(result, "Call: lm", fixed = TRUE) + expect_match(result, "N: 32", fixed = TRUE) + expect_match(result, "gaussian", fixed = TRUE) + # significant predictors + expect_match(result, "wt", fixed = TRUE) +}) + +test_that("report_ai.lm - print method", { + m <- lm(mpg ~ wt + hp, data = mtcars) + result <- report_ai(m) + expect_output(print(result)) +}) + +test_that("report_ai.default - warns and falls back to report()", { + # htest has report() support but no dedicated report_ai() method + ht <- t.test(mtcars$mpg ~ mtcars$am) + result <- expect_warning(report_ai(ht), "not yet available") + expect_s3_class(result, "report") +}) + +test_that("report_ai.default - falls back to human report() when report_audience option is ai", { + # Regression test: fallback from report_ai.default() must not recurse back + # into AI routing when the global audience option is set to "ai". + ht <- t.test(mtcars$mpg ~ mtcars$am) + old <- getOption("report_audience") + on.exit(options(report_audience = old)) + + options(report_audience = "ai") + result <- expect_warning(report_ai(ht), "not yet available") + expect_s3_class(result, "report") + expect_false(inherits(result, "report_ai")) +}) + +test_that("report() audience argument dispatches to report_ai", { + m <- lm(mpg ~ wt + hp, data = mtcars) + result_ai <- report(m, audience = "ai") + result_human <- report(m, audience = "humans") + + expect_s3_class(result_ai, "report_ai") + expect_s3_class(result_human, "report") +}) + +test_that("report() respects report_audience option", { + m <- lm(mpg ~ wt + hp, data = mtcars) + old <- getOption("report_audience") + on.exit(options(report_audience = old)) + + options(report_audience = "ai") + expect_s3_class(report(m), "report_ai") + + options(report_audience = "humans") + expect_s3_class(report(m), "report") +}) + +test_that("report_ai.glm - binomial family", { + m <- glm(vs ~ mpg + hp, data = mtcars, family = binomial()) + result <- report_ai(m) + + expect_s3_class(result, "report_ai") + expect_match(result, "## Model", fixed = TRUE) + expect_match(result, "## Parameters", fixed = TRUE) + expect_match(result, "## Performance", fixed = TRUE) + expect_match(result, "binomial", fixed = TRUE) + expect_match(result, "Call: glm", fixed = TRUE) +}) + +test_that("report_ai.glm - poisson family", { + m <- glm(gear ~ mpg + hp, data = mtcars, family = poisson()) + result <- report_ai(m) + + expect_s3_class(result, "report_ai") + expect_match(result, "poisson", fixed = TRUE) +}) + +test_that("report_ai.merMod - lmer", { + skip_if_not_installed("lme4") + skip_on_cran() + + m <- lme4::lmer(Reaction ~ Days + (1 | Subject), data = lme4::sleepstudy) + result <- report_ai(m) + + expect_s3_class(result, "report_ai") + expect_match(result, "## Model", fixed = TRUE) + expect_match(result, "## Parameters", fixed = TRUE) + expect_match(result, "## Performance", fixed = TRUE) + expect_match(result, "Call: lmer", fixed = TRUE) + expect_match(result, "Days", fixed = TRUE) + # Random effects should appear as bullet points, not in the fixed-effects table + expect_match(result, "### Random Effects", fixed = TRUE) + expect_match(result, "[Subject]", fixed = TRUE) + # Subject (grouping variable) should NOT appear in the Variables section + expect_false(grepl( + "Subject", + strsplit( + strsplit(result, "## Variables\n", fixed = TRUE)[[1]][2], + "## Parameters", + fixed = TRUE + )[[1]][ + 1 + ], + fixed = TRUE + )) +}) + +test_that("report_ai.merMod - glmer", { + skip_if_not_installed("lme4") + skip_on_cran() + + set.seed(123) + m <- lme4::glmer( + cbind(incidence, size - incidence) ~ period + (1 | herd), + data = lme4::cbpp, + family = binomial() + ) + result <- report_ai(m) + + expect_s3_class(result, "report_ai") + expect_match(result, "binomial", fixed = TRUE) + expect_match(result, "Call: glmer", fixed = TRUE) +}) + +test_that("report_ai.glmmTMB - poisson with random effect", { + skip_if_not_installed("glmmTMB") + skip_on_cran() + + set.seed(123) + m <- suppressWarnings(glmmTMB::glmmTMB( + count ~ mined + (1 | site), + family = poisson(), + data = glmmTMB::Salamanders + )) + result <- report_ai(m) + + expect_s3_class(result, "report_ai") + expect_match(result, "## Model", fixed = TRUE) + expect_match(result, "## Parameters", fixed = TRUE) + expect_match(result, "## Performance", fixed = TRUE) + expect_match(result, "Call: glmmTMB", fixed = TRUE) + expect_match(result, "poisson", fixed = TRUE) + # Random effects section should be present + expect_match(result, "### Random Effects", fixed = TRUE) +}) diff --git a/vignettes/report_ai.Rmd b/vignettes/report_ai.Rmd new file mode 100644 index 000000000..7d0bc108c --- /dev/null +++ b/vignettes/report_ai.Rmd @@ -0,0 +1,103 @@ +--- +title: "AI-Optimized Reports" +output: + rmarkdown::html_vignette: + toc: true + fig_width: 10.08 + fig_height: 6 +tags: [r, report, AI, LLM] +vignette: > + %\VignetteIndexEntry{AI-Optimized Reports} + \usepackage[utf8]{inputenc} + %\VignetteEngine{knitr::rmarkdown} +editor_options: + chunk_output_type: console +--- + +```{r setup, echo=FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + warning = FALSE, + message = FALSE +) +``` + +## How to Use `report()` and get AI-Optimized Output + +### The *"last statistical mile"* + +The **report** package was originally designed in a pre-AI era with one overarching goal: to produce human-readable prose. It was built to bridge the scientist's *"last statistical mile"*: that final, often tedious transition from the raw output of statistical software to the polished, written sentences of a manuscript. To achieve this, we leveraged the power **easystats**'s ecosystem to automatically and flexibly extract relevant information to engineer text with very specific characteristics: the output had to be **deterministic**, perfectly **consistent**, and adhere to fixed **reporting norms** (such as APA style). By automating this translation, our aim was to facilitate **fully reproducible research** (enabling reproducible *manuscripts*) and drastically reduce human error. Ultimately, this allowed us to shift the focus of **statistics education** away from memorizing where to find the right numbers and how to format them, empowering researchers to focus entirely on how to interpret and use those numbers to answer their research questions. + +### *easystats* in the Age of AI + +However, the analytical landscape has fundamentally changed. Today, researchers increasingly rely on AI agents and Large Language Models (LLMs) to help interpret results, summarize findings, and draft manuscripts, often by simply **pasting raw outputs directly into a chat window**. This shift prompted us to ask: should the scope of the *report* package be expanded to **support** this new workflow, rather than pretending it doesn't exist? And it wasn't even a matter of staying relevant, it was a matter of empowering our users to get the best possible results from their new AI assistants. + +While our standard narrative outputs are excellent for humans, they are remarkably suboptimal for AI: verbose prose forces an LLM to waste valuable context tokens re-parsing implicit structures, wasting tokens and context memory. The answer to this challenge is `report_ai()`, a function that can be triggered via an argument from the main `report()` function. By stripping away the narrative bloat in favour of highly structured, token-efficient formats, `report_ai()` bridges the gap between R and the context window, giving your AI assistant exactly the information it needs in the most effective way possible. + +## Basic Usage + +```{r} +library(report) + +m <- lm(mpg ~ wt + hp, data = mtcars) + +# Human-readable report (default) +report(m) +``` + +```{r} +# AI-optimized report +report(m, audience = "ai") +``` + +The AI output is a single character vector of class `report_ai` that you can +pass directly to any LLM API, embed in a system prompt, or include in a +context window. + +## Setting the Option Globally + +For a single analysis session or a whole document you can flip **all** `report()` +calls at once with one option: + +```{r eval=FALSE} +options(report_audience = "ai") + +# Every subsequent report() call now returns an AI-optimized output +report(m) +report(lm(mpg ~ am, data = mtcars)) +``` + +Reset to the default at any time: + +```{r eval=FALSE} +options(report_audience = "humans") +``` + +## Converting a Quarto Document + +If you have an existing Quarto (`.qmd`) or R Markdown (`.Rmd`) document that +already uses `report()` throughout, converting it to produce AI-optimized +output requires only **one line** added to the `setup` chunk: + +````{verbatim} +--- +title: "My Analysis" +format: html +--- + +```{r setup, include=FALSE} +library(report) + +# One line — all report() calls in this document become AI-optimised +options(report_audience = "ai") +``` + +```{r model} +m <- lm(mpg ~ wt + hp, data = mtcars) +report(m) # automatically AI-optimised because of the global option +``` +```` + +To make the option permanent across an entire Quarto project, add it to the +project-level `_quarto.yml` execute block or to your `.Rprofile`.