<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Blog on Posit Open Source</title>
    <link>https://posit-open-source.netlify.app/blog/</link>
    <description>Recent content in Blog on Posit Open Source</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Fri, 03 Apr 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://posit-open-source.netlify.app/blog/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>PDF Accessibility and Standards</title>
      <link>https://posit-open-source.netlify.app/blog/quarto/2026-03-05-pdf-accessibility-and-standards/</link>
      <pubDate>Thu, 05 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/quarto/2026-03-05-pdf-accessibility-and-standards/</guid>
      <dc:creator>Gordon Woodhull</dc:creator><description><![CDATA[<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Pre-release Feature</span>
</div>
<div class="callout-body">
<p>This feature is new in the upcoming Quarto 1.9 release. To use the feature now, you&rsquo;ll need to <a href="https://quarto.org/docs/download/prerelease.html" target="_blank" rel="noopener">download and install</a>
 the Quarto pre-release.</p>
</div>
</div>
<p>2025 was a big year for PDF accessibility. LaTeX and Typst both released support for PDF tagging and accessibility standards, just in time for new regulations in the <a href="https://en.wikipedia.org/wiki/European_Accessibility_Act" target="_blank" rel="noopener">EU</a>
 (June 2025) and <a href="https://accessible.org/ada-title-ii-web-accessibility/" target="_blank" rel="noopener">US</a>
 (April 2026).</p>
<p>Quarto 1.9 brings this support to you as a Quarto user.</p>
<h2 id="what-pdf-standards-do">What PDF Standards Do
</h2>
<p>Currently LaTeX supports the newer UA-2 standard, and Typst supports the older UA-1 standard. Typst is likely to have UA-2 support later in 2026.</p>
<p>Both standards instruct the PDF renderer to provide screen readers:</p>
<ul>
<li>The semantic structure of the text (title, heading, paragraph, figure, etc)</li>
<li>The natural reading order</li>
<li>Spatial coordinates for highlighting and assistive navigation</li>
<li>Required metadata such as title and language</li>
</ul>
<h2 id="how-to-enable-a-pdf-standard-in-quarto">How to enable a PDF Standard in Quarto
</h2>
<p>In Quarto 1.9, specify a PDF standard for your document or project with <code>pdf-standard</code></p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr>
<td style="text-align: left;"><div width="50.0%" data-layout-align="left">
<p><strong>PDF (LaTeX)</strong></p>
<div class="sourceCode" id="cb1"><pre class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">format</span><span class="kw">:</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">pdf</span><span class="kw">:</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">pdf-standard</span><span class="kw">:</span><span class="at"> ua-2</span></span></code></pre></div>
</div></td>
<td style="text-align: left;"><div width="50.0%" data-layout-align="left">
<p><strong>Typst</strong></p>
<div class="sourceCode" id="cb2"><pre class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="fu">format</span><span class="kw">:</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">typst</span><span class="kw">:</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">pdf-standard</span><span class="kw">:</span><span class="at"> ua-1</span></span></code></pre></div>
</div></td>
</tr>
</tbody>
</table>
<p><code>pdf-standard</code> takes a single standard name or list of standard names. PDF version is used if provided in the list, but otherwise inferred from the standard.</p>
<p>If you specify a PDF standard, Quarto first instructs LaTeX or Typst to use the standard when producing the PDF, and then validates the output PDF against the standard using veraPDF, an open-source PDF validation tool. If veraPDF is not installed, you&rsquo;ll get a warning but still receive a PDF &ndash; it just won&rsquo;t be validated.</p>
<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Installing veraPDF</span>
</div>
<div class="callout-body">
<p>To install veraPDF, you&rsquo;ll first need Java, then run:</p>
<p><strong>Terminal</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">quarto install verapdf
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>When a document passes validation, you&rsquo;ll see output like:</p>
<pre><code>[verapdf]: Validating my-document.pdf against PDF/UA-2... PASSED
</code></pre>
<h2 id="creating-accessible-pdfs">Creating accessible PDFs
</h2>
<p>Quarto&rsquo;s Markdown-based workflow handles many accessibility requirements automatically:</p>
<ul>
<li>Document metadata (title, author, date, language) flows into the PDF&rsquo;s built-in metadata fields.</li>
<li>The semantic structure of Markdown satisfies PDF tagging requirements. For Typst this is always enabled; for LaTeX it is enabled when you specify a standard that requires it.</li>
<li>Alt text for images is carried through to the PDF for screen readers.</li>
</ul>
<p>But you do need to make sure your document has:</p>
<ul>
<li>A <strong>title</strong> in the YAML front matter.</li>
<li><strong>Alt text for every image</strong>, specified with <code>fig-alt</code>. See <a href="https://quarto.org/docs/authoring/figures.html#alt-text" target="_blank" rel="noopener">Figures</a>
 for details.</li>
</ul>
<p>See the <a href="https://quarto.org/docs/output-formats/pdf-basics.html#accessibility-requirements" target="_blank" rel="noopener">LaTeX</a>
 and <a href="https://quarto.org/docs/output-formats/typst.html#accessibility-requirements" target="_blank" rel="noopener">Typst</a>
 documentation for more details.</p>
<h2 id="if-your-document-fails-validation">If your document fails validation
</h2>
<p>LaTeX does not perform validation during PDF generation, so if veraPDF validation fails, that&rsquo;s a warning, and you still get a partially-accessible PDF as long as you use <code>pdf-standard: ua-2</code>.</p>
<p>Typst fails and does not produce a PDF if its built-in validation fails during PDF generation. However, in Typst all accessibility features are on by default, so you can generate a partially-accessible PDF by rendering without <code>pdf-standard</code>.</p>
<h2 id="current-limitations">Current limitations
</h2>
<p>We ran our test suite &ndash; 188 LaTeX examples and 317 Typst examples &ndash; to find where Quarto PDFs do not yet pass UA-1 or UA-2, and where users will need to change their documents.</p>
<h3 id="latex">LaTeX
</h3>
<p>Margin content is the biggest structural blocker. If you use <code>.column-margin</code> divs, <code>cap-location: margin</code>, <code>reference-location: margin</code>, or <code>citation-location: margin</code>, the resulting PDF will not pass UA-2. The underlying <code>sidenotes</code> and <code>marginnote</code> LaTeX packages <a href="https://github.com/quarto-dev/quarto-cli/issues/14103" target="_blank" rel="noopener">do not cooperate with PDF tagging</a>
.</p>
<p>(Margin content does work with Typst and passes UA-1 &ndash; see <a href="https://quarto.org/docs/output-formats/typst.html#article-layout" target="_blank" rel="noopener">Typst Article Layout</a>
.)</p>
<p>There are smaller upstream issues in Pandoc, LaTeX, and LaTeX packages, <a href="https://github.com/quarto-dev/quarto-cli/pull/14097#issuecomment-3947653207" target="_blank" rel="noopener">documented here</a>
.</p>
<h3 id="typst">Typst
</h3>
<p>In our tests, Typst catches every UA-1 violation, and fails to generate the PDF. veraPDF did not detect any violation that Typst did not.</p>
<p>Typst also seems to do a very good job of generating UA-1 compliant output by default &ndash; almost all errors were due to missing titles or missing alt text.</p>
<p>However, we did discover that <a href="https://quarto.org/docs/books/book-output.html#typst-output" target="_blank" rel="noopener">Typst books</a>
 are not yet compliant. There is a <a href="https://github.com/flavio20002/typst-orange-template/issues/38" target="_blank" rel="noopener">structural problem with the Typst orange-book package</a>
 and we&rsquo;ll work with the maintainers to correct it.</p>
<h2 id="conclusion">Conclusion
</h2>
<p>Although Typst currently targets an the earlier UA-1 standard, today it seems to offer better PDF accessibility than LaTeX.</p>
<p>We expect PDF accessibility support to improve through the LaTeX ecosystem throughout 2026 as awareness of UA-2 and the new regulations spreads.</p>
<p>If you run into accessibility issues with PDF output, please search the <a href="https://github.com/orgs/quarto-dev/discussions" target="_blank" rel="noopener">Quarto discussions</a>
 and open a new one with the <code>accessibility</code> label for any issues you discover.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/quarto/2026-03-05-pdf-accessibility-and-standards/thumbnail.png" length="41719" type="image/png" />
    </item>
    <item>
      <title>Rapp 0.3.0</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/rapp-0-3-0/</link>
      <pubDate>Wed, 18 Feb 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/rapp-0-3-0/</guid>
      <dc:creator>Tomasz Kalinowski</dc:creator><description><![CDATA[<p>We&rsquo;re excited to share our first tidyverse blog post for Rapp, alongside the <code>0.3.0</code> release. Rapp helps you turn R scripts into polished command-line tools, with argument parsing and help generation built in.</p>
<h2 id="why-a-command-line-interface-for-r">Why a command-line interface for R?
</h2>
<p>A command-line interface (CLI) lets you run programs from a terminal, without opening an IDE or starting an interactive R session. This is useful when you want to:</p>
<ul>
<li>automate tasks via cron jobs, scheduled tasks, or CI/CD pipelines</li>
<li>chain R scripts together with other tools in data pipelines</li>
<li>let others run your R code without needing to know R</li>
<li>package reusable tools that feel native to the terminal</li>
<li>expose specific actions through a clean interface that LLM agents can invoke</li>
</ul>
<p>There are several established packages for building CLIs in R, including argparse, optparse, and docopt, where you explicitly parse and handle command-line arguments in code. Rapp takes a different approach: it derives the CLI surface from the structure of your R script and injects values at runtime, so you never need to handle CLI arguments manually.</p>
<h2 id="how-rapp-works">How Rapp works
</h2>
<p>At its core, Rapp is an alternative front-end to R: a drop-in replacement for <code>Rscript</code> that automatically turns common R expression patterns into command-line options, switches, positional arguments, and subcommands. You write normal R code and Rapp handles the CLI surface.</p>
<p>Rapp also uses special <code>#|</code> comments (similar to Quarto&rsquo;s YAML-in-comments syntax) to add metadata such as help descriptions and short aliases.</p>
<h2 id="a-tiny-example">A tiny example
</h2>
<p>Here&rsquo;s a complete Rapp script (from the package examples), a coin flipper:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#!/usr/bin/env Rapp</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| name: flip-coin</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| description: |</span>
</span></span><span class="line"><span class="cl"><span class="c1">#|   Flip a coin.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#| description: Number of coin flips</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| short: &#39;n&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">flips</span> <span class="o">&lt;-</span> <span class="m">1L</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sep</span> <span class="o">&lt;-</span> <span class="s">&#34; &#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">wrap</span> <span class="o">&lt;-</span> <span class="kc">TRUE</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">seed</span> <span class="o">&lt;-</span> <span class="kc">NA_integer_</span>
</span></span><span class="line"><span class="cl"><span class="kr">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">is.na</span><span class="p">(</span><span class="n">seed</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nf">set.seed</span><span class="p">(</span><span class="n">seed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">cat</span><span class="p">(</span><span class="nf">sample</span><span class="p">(</span><span class="nf">c</span><span class="p">(</span><span class="s">&#34;heads&#34;</span><span class="p">,</span> <span class="s">&#34;tails&#34;</span><span class="p">),</span> <span class="n">flips</span><span class="p">,</span> <span class="kc">TRUE</span><span class="p">),</span> <span class="n">sep</span> <span class="o">=</span> <span class="n">sep</span><span class="p">,</span> <span class="n">fill</span> <span class="o">=</span> <span class="n">wrap</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Let&rsquo;s break down how Rapp interprets this script:</p>
<table>
  <thead>
      <tr>
          <th>R code</th>
          <th>Generated CLI option</th>
          <th>What it does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>flips &lt;- 1L</code></td>
          <td><code>--flips</code> or <code>-n</code></td>
          <td>Integer option with default of 1</td>
      </tr>
      <tr>
          <td><code>sep &lt;- &quot; &quot;</code></td>
          <td><code>--sep</code></td>
          <td>String option with default of <code>&quot; &quot;</code></td>
      </tr>
      <tr>
          <td><code>wrap &lt;- TRUE</code></td>
          <td><code>--wrap</code> / <code>--no-wrap</code></td>
          <td>Boolean toggle (TRUE/FALSE becomes on/off)</td>
      </tr>
      <tr>
          <td><code>seed &lt;- NA_integer_</code></td>
          <td><code>--seed</code></td>
          <td>Optional integer (NA means &ldquo;not set&rdquo;)</td>
      </tr>
  </tbody>
</table>
<p>The <code>#| short: 'n'</code> comment adds <code>-n</code> as a short alias for <code>--flips</code>. The <code>#!/usr/bin/env Rapp</code> line (called a &ldquo;shebang&rdquo;) lets you run the script directly on macOS and Linux without typing <code>Rapp</code> first.</p>
<h3 id="running-the-script">Running the script
</h3>
<p>With Rapp installed and <code>flip-coin</code> available on your <code>PATH</code> (see <a href="#get-started">Get started</a>
 below), you can run the app from the terminal:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">flip-coin -n <span class="m">3</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; heads tails heads</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">flip-coin --seed <span class="m">42</span> -n <span class="m">5</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; tails heads tails tails heads</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="auto-generated-help">Auto-generated help
</h3>
<p>Rapp generates <code>--help</code> from your script (and <code>--help-yaml</code> if you want a machine-readable spec):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">flip-coin --help
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Usage: flip-coin [OPTIONS]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Flip a coin.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Options:
</span></span><span class="line"><span class="cl">  -n, --flips &lt;FLIPS&gt;  Number of coin flips [default: 1] [type: integer]
</span></span><span class="line"><span class="cl">  --sep &lt;SEP&gt;          [default: &#34; &#34;] [type: string]
</span></span><span class="line"><span class="cl">  --wrap / --no-wrap   [default: true] Disable with `--no-wrap`.
</span></span><span class="line"><span class="cl">  --seed &lt;SEED&gt;        [default: NA] [type: integer]
</span></span></code></pre></td></tr></table>
</div>
</div><div class="callout-warning">
<h2 id="breaking-change-in-030-positional-arguments-are-now-required-by-default">Breaking change in 0.3.0: positional arguments are now required by default
</h2>
<p>If you&rsquo;re upgrading from an earlier version of Rapp, note that positional arguments are now <strong>required</strong> unless explicitly marked optional.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># Before 0.3.0: this positional was optional</span>
</span></span><span class="line"><span class="cl"><span class="n">name</span> <span class="o">&lt;-</span> <span class="kc">NULL</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># In 0.3.0+: add this comment to keep it optional</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| required: false</span>
</span></span><span class="line"><span class="cl"><span class="n">name</span> <span class="o">&lt;-</span> <span class="kc">NULL</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>If your scripts use positional arguments with <code>NULL</code> defaults that should remain optional, add <code>#| required: false</code> above them.</p>
</div>
<h2 id="highlights-in-030">Highlights in 0.3.0
</h2>
<p>Rapp will be new to most readers, so rather than listing every change, here are the main ideas (and what&rsquo;s improved in 0.3.0).</p>
<h3 id="options-switches-and-repeatable-flags-from-plain-r">Options, switches, and repeatable flags from plain R
</h3>
<p>Rapp recognizes a small set of &ldquo;declarative&rdquo; patterns at the top level of your script:</p>
<ul>
<li>Scalar literals like <code>flips &lt;- 1L</code> become options like <code>--flips 10</code>.</li>
<li>Logical defaults like <code>wrap &lt;- TRUE</code> become toggles like <code>--wrap</code> / <code>--no-wrap</code>.</li>
<li><code>#| short: n</code> adds a short alias like <code>-n</code> (new in 0.3.0).</li>
<li><a href="https://rdrr.io/r/base/c.html" target="_blank" rel="noopener"><code>c()</code></a>
 and <a href="https://rdrr.io/r/base/list.html" target="_blank" rel="noopener"><code>list()</code></a>
 defaults declare repeatable options (new in 0.3.0): callers can supply the same flag multiple times and values are appended.</li>
</ul>
<h3 id="subcommands-with-switch">Subcommands with <code>switch()</code>
</h3>
<p>Rapp can now turn a <a href="https://rdrr.io/r/base/switch.html" target="_blank" rel="noopener"><code>switch()</code></a>
 block into subcommands (and you can nest <a href="https://rdrr.io/r/base/switch.html" target="_blank" rel="noopener"><code>switch()</code></a>
 blocks for nested commands). Here&rsquo;s a small sketch of a <code>todo</code>-style app:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#!/usr/bin/env Rapp</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| name: todo</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| description: Manage a simple todo list.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#| description: Path to the todo list file.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| short: s</span>
</span></span><span class="line"><span class="cl"><span class="n">store</span> <span class="o">&lt;-</span> <span class="s">&#34;.todo.yml&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">switch</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">command</span> <span class="o">&lt;-</span> <span class="s">&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">#| description: Display the todos</span>
</span></span><span class="line"><span class="cl">  <span class="n">list</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">limit</span> <span class="o">&lt;-</span> <span class="m">30L</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">#| description: Add a new todo</span>
</span></span><span class="line"><span class="cl">  <span class="n">add</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">task</span> <span class="o">&lt;-</span> <span class="kc">NULL</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Help is scoped to the command you&rsquo;re asking about, so <code>todo --help</code> lists the commands, and <code>todo list --help</code> shows just the options/arguments for <code>list</code> (plus any parent/global options).</p>
<h3 id="installable-launchers-for-package-clis">Installable launchers for package CLIs
</h3>
<p>A big part of sharing CLI tools is making them easy to run after installation. In <code>0.3.0</code>, <code>install_pkg_cli_apps()</code> installs lightweight launchers for scripts in a package&rsquo;s <code>exec/</code> directory that use either <code>#!/usr/bin/env Rapp</code> or <code>#!/usr/bin/env Rscript</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">Rapp</span><span class="o">::</span><span class="nf">install_pkg_cli_apps</span><span class="p">(</span><span class="s">&#34;mypackage&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>(There&rsquo;s also <code>uninstall_pkg_cli_apps()</code> to remove a package&rsquo;s launchers.)</p>
<h2 id="get-started">Get started
</h2>
<p>Here&rsquo;s the quickest path to your first Rapp script:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># 1. Install the package</span>
</span></span><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;Rapp&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># 2. Install the command-line launcher</span>
</span></span><span class="line"><span class="cl"><span class="n">Rapp</span><span class="o">::</span><span class="nf">install_pkg_cli_apps</span><span class="p">(</span><span class="s">&#34;Rapp&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Then create a script (e.g., <code>hello.R</code>):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#!/usr/bin/env Rapp</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| name: hello</span>
</span></span><span class="line"><span class="cl"><span class="c1">#| description: Say hello</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">name</span> <span class="o">&lt;-</span> <span class="s">&#34;world&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nf">cat</span><span class="p">(</span><span class="s">&#34;Hello,&#34;</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="s">&#34;\n&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>And run it:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">Rapp hello.R --name <span class="s2">&#34;R users&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Hello, R users</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="learn-more">Learn more
</h3>
<p>To dig deeper into Rapp:</p>
<ul>
<li>browse examples in the package: <code>system.file(&quot;examples&quot;, package = &quot;Rapp&quot;)</code></li>
<li>read the full documentation: <a href="https://github.com/r-lib/Rapp" target="_blank" rel="noopener">https://github.com/r-lib/Rapp</a>
</li>
<li>note that Rapp requires R ≥ 4.1.0</li>
</ul>
<p>If you try Rapp, we&rsquo;d love feedback! We especially want to hear about your experiences with edge cases in argument parsing, help output, and how commands should feel. Issues and ideas are welcome at <a href="https://github.com/r-lib/Rapp/issues" target="_blank" rel="noopener">https://github.com/r-lib/Rapp/issues</a>
.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/rapp-0-3-0/thumbnail-wd.jpg" length="89445" type="image/jpeg" />
    </item>
    <item>
      <title>mirai 2.6.0</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/mirai-2-6-0/</link>
      <pubDate>Thu, 12 Feb 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/mirai-2-6-0/</guid>
      <dc:creator>Charlie Gao</dc:creator><description><![CDATA[<p><a href="https://mirai.r-lib.org" target="_blank" rel="noopener">mirai</a>
 2.6.0 is now on CRAN. mirai is R&rsquo;s framework for parallel and asynchronous computing. If you&rsquo;re fitting models, running simulations, or building Shiny apps, mirai lets you spread that work across multiple processes &ndash; locally or on remote infrastructure.</p>
<p>With this release, it bridges the gap between your laptop and enterprise infrastructure &ndash; the same code you prototype locally now deploys to Posit Workbench or any cloud HTTP API, with a single function call.</p>
<p>You can install it from CRAN with:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;mirai&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The flagship feature for this release is the HTTP launcher for deploying daemons to cloud and enterprise platforms. This release also brings a C-level dispatcher for minimal task dispatch overhead, <a href="https://mirai.r-lib.org/reference/race_mirai.html" target="_blank" rel="noopener"><code>race_mirai()</code></a>
 for process-as-completed patterns, synchronous mode for debugging, and daemon synchronization for remote deployments. You can see a full list of changes in the <a href="https://mirai.r-lib.org/news/#mirai-260" target="_blank" rel="noopener">release notes</a>
.</p>
<h2 id="how-mirai-works">How mirai works
</h2>
<p>If you&rsquo;ve ever waited for a loop to finish fitting models, processing files, or calling APIs, mirai can help. Any task that&rsquo;s repeated independently across items is a candidate for parallel execution.</p>
<p>The <a href="https://posit-open-source.netlify.app/blog/tidyverse/2025/mirai-2-5-0">previous release post</a>
 covered mirai&rsquo;s design philosophy in detail. Here&rsquo;s a brief overview for readers encountering mirai for the first time.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://mirai.r-lib.org'>mirai</a></span><span class='o'>)</span></span>
<span><span class='c'># Set up 4 background processes</span></span>
<span><span class='nf'><a href='https://mirai.r-lib.org/reference/daemons.html'>daemons</a></span><span class='o'>(</span><span class='m'>4</span><span class='o'>)</span></span>
<span></span>
<span><span class='c'># Send work -- non-blocking, returns immediately</span></span>
<span><span class='nv'>m</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://mirai.r-lib.org/reference/mirai.html'>mirai</a></span><span class='o'>(</span><span class='o'>&#123;</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/Sys.sleep.html'>Sys.sleep</a></span><span class='o'>(</span><span class='m'>1</span><span class='o'>)</span></span>
<span>  <span class='m'>100</span> <span class='o'>+</span> <span class='m'>42</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span>
<span><span class='nv'>m</span></span>
<span><span class='c'>#&gt; &lt; mirai [] &gt;</span></span>
<span></span><span></span>
<span><span class='c'># Collect the result when ready</span></span>
<span><span class='nv'>m</span><span class='o'>[</span><span class='o'>]</span></span>
<span><span class='c'>#&gt; [1] 142</span></span>
<span></span><span></span>
<span><span class='c'># Shut down</span></span>
<span><span class='nf'><a href='https://mirai.r-lib.org/reference/daemons.html'>daemons</a></span><span class='o'>(</span><span class='m'>0</span><span class='o'>)</span></span></code></pre>
</div>
<p>That&rsquo;s mirai in a nutshell: <a href="https://mirai.r-lib.org/reference/daemons.html" target="_blank" rel="noopener"><code>daemons()</code></a>
 to set up workers, <a href="https://mirai.r-lib.org/reference/mirai.html" target="_blank" rel="noopener"><code>mirai()</code></a>
 to send work, <code>[]</code> to collect results. Everything else builds on this.</p>
<p>In mirai&rsquo;s hub architecture, the host session listens at a URL and <em>daemons</em> &ndash; background R processes that do the actual work &ndash; connect to it. You send tasks with <a href="https://mirai.r-lib.org/reference/mirai.html" target="_blank" rel="noopener"><code>mirai()</code></a>
, and the dispatcher routes them to available daemons in first-in, first-out (FIFO) order.</p>
<p>This design enables dynamic scaling: daemons can connect and disconnect at any time without disrupting the host. Add capacity when you need it, release it when you don&rsquo;t.</p>
<img src="https://posit-open-source.netlify.app/blog/tidyverse/mirai-2-6-0/architecture.svg" alt="Hub architecture diagram showing compute profiles with daemons connecting to host" width="100%" />
<p>A single compute profile can mix daemons launched by different methods, and you can run multiple profiles simultaneously to direct different tasks to different resources. The basic syntax for each deployment method:</p>
<table>
  <thead>
      <tr>
          <th>Deploy to</th>
          <th>Setup</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Local</td>
          <td><code>daemons(4)</code></td>
      </tr>
      <tr>
          <td>Remote (SSH)</td>
          <td><code>daemons(url = host_url(), remote = ssh_config(...))</code></td>
      </tr>
      <tr>
          <td>HPC cluster (Slurm, SGE, PBS, LSF)</td>
          <td><code>daemons(url = host_url(), remote = cluster_config())</code></td>
      </tr>
      <tr>
          <td>HTTP API / Posit Workbench</td>
          <td><code>daemons(url = host_url(), remote = http_config())</code></td>
      </tr>
  </tbody>
</table>
<p>Change one line and your local prototype runs on a Slurm cluster. Change it again and it runs on Posit Workbench. Your analysis code stays identical.</p>
<h2 id="the-async-foundation-for-the-modern-r-stack">The async foundation for the modern R stack
</h2>
<p>mirai has become the convergence point for asynchronous and parallel computing across the R ecosystem.</p>
<p>It is the <a href="https://rstudio.github.io/promises/articles/promises_04_mirai.html" target="_blank" rel="noopener">recommended async backend</a>
 for <a href="https://shiny.posit.co/" target="_blank" rel="noopener">Shiny</a>
 &ndash; if you&rsquo;re building production Shiny apps, you should be using mirai. It is the <em>only</em> async backend for the next-generation <a href="https://plumber2.posit.co/" target="_blank" rel="noopener">plumber2</a>
 &ndash; if you&rsquo;re building APIs with plumber2, you&rsquo;re already using mirai.</p>
<p>It is the parallel backend for <a href="https://purrr.tidyverse.org/" target="_blank" rel="noopener">purrr</a>
 &ndash; if you use <code>map()</code>, mirai is how you make it parallel. Wrap your function in <a href="https://purrr.tidyverse.org/reference/in_parallel.html" target="_blank" rel="noopener"><code>in_parallel()</code></a>
, set up daemons, and your map calls run across all of them:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">purrr</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="m">4</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">models</span> <span class="o">&lt;-</span> <span class="nf">split</span><span class="p">(</span><span class="n">mtcars</span><span class="p">,</span> <span class="n">mtcars</span><span class="o">$</span><span class="n">cyl</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">map</span><span class="p">(</span><span class="nf">in_parallel</span><span class="p">(</span><span class="nf">\</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="nf">lm</span><span class="p">(</span><span class="n">mpg</span> <span class="o">~</span> <span class="n">wt</span> <span class="o">+</span> <span class="n">hp</span><span class="p">,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">x</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="m">0</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>It powers <a href="https://docs.ropensci.org/targets/" target="_blank" rel="noopener">targets</a>
 &ndash; the pipeline orchestration tool for reproducible analysis. And most recently, <a href="https://ragnar.tidyverse.org/" target="_blank" rel="noopener">ragnar</a>
 &ndash; the Tidyverse package for retrieval-augmented generation (RAG) &ndash; adopted mirai for its parallel processing.</p>
<p>As an <a href="https://stat.ethz.ch/R-manual/R-devel/library/parallel/html/makeCluster.html" target="_blank" rel="noopener">official alternative communications backend</a>
 for R&rsquo;s <code>parallel</code> package, mirai underpins workflows from interactive web applications to pipeline orchestration to AI-powered document processing.</p>
<p>Learn mirai, and you&rsquo;ve learned the async primitive that powers the modern R stack. The same two concepts &ndash; <a href="https://mirai.r-lib.org/reference/daemons.html" target="_blank" rel="noopener"><code>daemons()</code></a>
 to set up workers, <a href="https://mirai.r-lib.org/reference/mirai.html" target="_blank" rel="noopener"><code>mirai()</code></a>
 to send work &ndash; are all you need to keep a Shiny app responsive or run async tasks in production.</p>
<h2 id="http-launcher">HTTP launcher
</h2>
<p>This release extends the &ldquo;deploy everywhere&rdquo; principle with <a href="https://mirai.r-lib.org/reference/http_config.html" target="_blank" rel="noopener"><code>http_config()</code></a>
, a new remote launch configuration that deploys daemons via HTTP API calls &ndash; any platform with an HTTP API for launching jobs.</p>
<h3 id="posit-workbench">Posit Workbench
</h3>
<p>Many organizations use <a href="https://posit.co/products/enterprise/workbench/" target="_blank" rel="noopener">Posit Workbench</a>
 to run research and data science at scale. mirai now integrates directly with it.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> Call <a href="https://mirai.r-lib.org/reference/http_config.html" target="_blank" rel="noopener"><code>http_config()</code></a>
 with no arguments and it auto-configures using the Workbench environment:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="n">n</span> <span class="o">=</span> <span class="m">4</span><span class="p">,</span> <span class="n">url</span> <span class="o">=</span> <span class="nf">host_url</span><span class="p">(),</span> <span class="n">remote</span> <span class="o">=</span> <span class="nf">http_config</span><span class="p">())</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>That&rsquo;s it. Four daemons launch as Workbench jobs, connect back to your session, and you can start sending work to them.</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/tidyverse/mirai-2-6-0/workbench.png" alt="Posit Workbench session showing launched mirai daemons" />
<figcaption aria-hidden="true">Posit Workbench session showing launched mirai daemons</figcaption>
</figure>
<p>Here&rsquo;s what that looks like in practice: you&rsquo;re developing a model in your Workbench session. Fitting it locally is slow. Add that line, and those fits fan out across four Workbench-managed compute jobs. When you&rsquo;re done, <code>daemons(0)</code> releases them. No YAML, no job scripts, no leaving your R session &ndash; resource allocation, access control, and job lifecycle are all handled by the platform.</p>
<p>If you&rsquo;ve been bitten by expired tokens in long-running sessions, <a href="https://mirai.r-lib.org/reference/http_config.html" target="_blank" rel="noopener"><code>http_config()</code></a>
 is designed to prevent that. Under the hood, it stores <em>functions</em> rather than static values for credentials and endpoint URLs. These functions are called at the moment daemons actually launch, so session cookies and API tokens are always fresh &ndash; even if you created the configuration hours earlier.</p>
<p>See the mirai vignette for <a href="https://mirai.r-lib.org/articles/v01-reference.html#troubleshooting" target="_blank" rel="noopener">troubleshooting</a>
 remote launches.</p>
<h3 id="custom-apis">Custom APIs
</h3>
<p>The HTTP launcher works with any HTTP API, not just Workbench. Supply your own endpoint, authentication, and request body:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">n</span> <span class="o">=</span> <span class="m">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">url</span> <span class="o">=</span> <span class="nf">host_url</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">  <span class="n">remote</span> <span class="o">=</span> <span class="nf">http_config</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="s">&#34;https://api.example.com/launch&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">method</span> <span class="o">=</span> <span class="s">&#34;POST&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">token</span> <span class="o">=</span> <span class="kr">function</span><span class="p">()</span> <span class="nf">Sys.getenv</span><span class="p">(</span><span class="s">&#34;MY_API_KEY&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">=</span> <span class="s">&#39;{&#34;command&#34;: &#34;%s&#34;}&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The <code>&quot;%s&quot;</code> placeholder in <code>data</code> is where mirai inserts the daemon launch command at launch time. Each argument can be a plain value or a function &ndash; use functions for anything that changes between launches (tokens, cookies, dynamic URLs).</p>
<p>This opens up a wide range of deployment targets: Kubernetes job APIs, other cloud container services, or any internal job scheduler with an HTTP interface. If you can launch a process with an HTTP call, mirai can use it.</p>
<h2 id="c-level-dispatcher">C-level dispatcher
</h2>
<p>The overhead of distributing your tasks is now negligible. In a <a href="https://mirai.r-lib.org/reference/mirai_map.html" target="_blank" rel="noopener"><code>mirai_map()</code></a>
 over thousands of items, what you measure is the time of your actual computation, not the framework &ndash; per-task dispatch overhead is now in the tens of microseconds, where existing R parallelism solutions typically operate in the millisecond range.</p>
<p>Under the hood, the dispatcher &ndash; the process that sits between your session and the daemons, routing tasks to available workers &ndash; has been re-implemented entirely in C code within <a href="https://nanonext.r-lib.org" target="_blank" rel="noopener">nanonext</a>
. This eliminates the R interpreter overhead that remained, while the dispatcher continues to be event-driven and consume zero CPU when idle.</p>
<p>This also removes the bottleneck when coordinating large numbers of daemons, which matters directly for the kind of scaled-out deployments that the HTTP launcher enables &ndash; dozens of Workbench jobs or cloud instances all connecting to a single dispatcher. The two features are designed to work together: deploy broadly, dispatch efficiently. mirai is built to scale from 2 cores on your laptop to 200 across a cluster, without the framework slowing you down.</p>
<h2 id="race_mirai"><code>race_mirai()</code>
</h2>
<p><a href="https://mirai.r-lib.org/reference/race_mirai.html" target="_blank" rel="noopener"><code>race_mirai()</code></a>
 lets you process results as they arrive, rather than waiting for the slowest task. Suppose you&rsquo;re fitting 10 models with different hyperparameters in parallel &ndash; some converge quickly, others take much longer. Without <a href="https://mirai.r-lib.org/reference/race_mirai.html" target="_blank" rel="noopener"><code>race_mirai()</code></a>
, you wait for the slowest fit to complete before seeing any results. With it, you can inspect or save each model the instant it finishes &ndash; updating a progress display, freeing memory, or deciding whether to continue the remaining fits at all.</p>
<p><a href="https://mirai.r-lib.org/reference/race_mirai.html" target="_blank" rel="noopener"><code>race_mirai()</code></a>
 returns the integer <em>index</em> of the first resolved mirai. This makes the &ldquo;process as completed&rdquo; pattern clean and efficient:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="m">4</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Launch 10 model fits in parallel</span>
</span></span><span class="line"><span class="cl"><span class="n">fits</span> <span class="o">&lt;-</span> <span class="nf">lapply</span><span class="p">(</span><span class="n">param_grid</span><span class="p">,</span> <span class="kr">function</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="nf">mirai</span><span class="p">(</span><span class="nf">fit_model</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">p</span><span class="p">),</span> <span class="n">data</span> <span class="o">=</span> <span class="n">data</span><span class="p">,</span> <span class="n">p</span> <span class="o">=</span> <span class="n">p</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Process each result as soon as it&#39;s ready</span>
</span></span><span class="line"><span class="cl"><span class="n">remaining</span> <span class="o">&lt;-</span> <span class="n">fits</span>
</span></span><span class="line"><span class="cl"><span class="kr">while</span> <span class="p">(</span><span class="nf">length</span><span class="p">(</span><span class="n">remaining</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">idx</span> <span class="o">&lt;-</span> <span class="nf">race_mirai</span><span class="p">(</span><span class="n">remaining</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="nf">cat</span><span class="p">(</span><span class="s">&#34;Finished model with params:&#34;</span><span class="p">,</span> <span class="n">remaining[[idx]]</span><span class="o">$</span><span class="n">data</span><span class="o">$</span><span class="n">p</span><span class="p">,</span> <span class="s">&#34;\n&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">remaining</span> <span class="o">&lt;-</span> <span class="n">remaining[</span><span class="o">-</span><span class="n">idx]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="m">0</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Send off a batch of tasks, then process results in the order they finish &ndash; no polling, no wasted time waiting on the slowest one. If any mirai is already resolved when you call <a href="https://mirai.r-lib.org/reference/race_mirai.html" target="_blank" rel="noopener"><code>race_mirai()</code></a>
, it returns immediately. This pattern applies whenever tasks have variable completion times &ndash; parallel model fits, API calls, simulations, or any batch where you want to stream results as they land.</p>
<h2 id="synchronous-mode">Synchronous mode
</h2>
<p>When tasks don&rsquo;t behave as expected, you need a way to inspect them interactively.</p>
<p>Without synchronous mode, errors in a mirai return as <code>miraiError</code> objects &ndash; you can see that something went wrong, but you can&rsquo;t step through the code to find out why. The task ran in a separate process, and by the time you see the error, that process has moved on.</p>
<p><code>daemons(sync = TRUE)</code>, introduced in 2.5.1, solves this. It runs everything in the current process &ndash; no background processes, no networking &ndash; just sequential execution. You can use <a href="https://rdrr.io/r/base/browser.html" target="_blank" rel="noopener"><code>browser()</code></a>
 and other interactive debugging tools directly:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="n">sync</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">mirai</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">browser</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">mypkg</span><span class="o">::</span><span class="nf">some_complex_function</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span> <span class="o">=</span> <span class="n">my_data</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>You can scope synchronous mode to a specific compute profile, isolating the problematic task for inspection while the rest of your pipeline keeps running in parallel.</p>
<h2 id="daemon-synchronization-with-everywhere">Daemon synchronization with <code>everywhere()</code>
</h2>
<p><a href="https://mirai.r-lib.org/reference/everywhere.html" target="_blank" rel="noopener"><code>everywhere()</code></a>
 runs setup operations on all daemons &ndash; loading packages, sourcing scripts, or preparing datasets &ndash; so they&rsquo;re ready before you send work.</p>
<p>When launching remote daemons &ndash; via SSH, HPC schedulers, or the new HTTP launcher &ndash; there&rsquo;s an inherent delay between requesting a daemon and that daemon being ready to accept work. The new <code>.min</code> argument ensures that setup has completed on at least that many daemons before returning:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="n">n</span> <span class="o">=</span> <span class="m">8</span><span class="p">,</span> <span class="n">url</span> <span class="o">=</span> <span class="nf">host_url</span><span class="p">(),</span> <span class="n">remote</span> <span class="o">=</span> <span class="nf">http_config</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Wait until all 8 daemons are connected before continuing</span>
</span></span><span class="line"><span class="cl"><span class="nf">everywhere</span><span class="p">(</span><span class="nf">library</span><span class="p">(</span><span class="n">mypackage</span><span class="p">),</span> <span class="n">.min</span> <span class="o">=</span> <span class="m">8</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Now send work once all daemons are ready</span>
</span></span><span class="line"><span class="cl"><span class="n">mp</span> <span class="o">&lt;-</span> <span class="nf">mirai_map</span><span class="p">(</span><span class="n">tasks</span><span class="p">,</span> <span class="n">process</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This creates a synchronization point, ensuring your pipeline doesn&rsquo;t start sending work before all daemons are ready. It&rsquo;s especially useful for remote deployments where connection times are unpredictable.</p>
<h2 id="minor-improvements-and-fixes">Minor improvements and fixes
</h2>
<ul>
<li><code>miraiError</code> objects now have <a href="https://rdrr.io/r/base/conditions.html" target="_blank" rel="noopener"><code>conditionCall()</code></a>
 and <a href="https://rdrr.io/r/base/conditions.html" target="_blank" rel="noopener"><code>conditionMessage()</code></a>
 methods, making them easier to use with R&rsquo;s standard condition handling.</li>
<li>The default exit behavior for daemons has been updated with a 200ms grace period before forceful termination, which allows OpenTelemetry disconnection events to be traced.</li>
<li>OpenTelemetry span names and attributes have been revised to better follow semantic conventions.</li>
<li><a href="https://mirai.r-lib.org/reference/daemons.html" target="_blank" rel="noopener"><code>daemons()</code></a>
 now properly validates that <code>url</code> is a character value where supplied.</li>
<li>Fixed a bug where repeated mirai cancellation could sometimes cause a daemon to exit prematurely.</li>
</ul>
<h2 id="try-it-now">Try it now
</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;mirai&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">mirai</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="m">4</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">system.time</span><span class="p">(</span><span class="nf">mirai_map</span><span class="p">(</span><span class="m">1</span><span class="o">:</span><span class="m">4</span><span class="p">,</span> <span class="nf">\</span><span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="nf">Sys.sleep</span><span class="p">(</span><span class="m">1</span><span class="p">))</span><span class="n">[]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;    user  system elapsed</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   0.000   0.001   1.003</span>
</span></span><span class="line"><span class="cl"><span class="nf">daemons</span><span class="p">(</span><span class="m">0</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Four one-second tasks, one second of wall time. If those were four model fits that each took a minute, you&rsquo;d go from four minutes down to one &ndash; and if you needed more power, switching to Workbench or a Slurm cluster is a one-line change. Visit <a href="https://mirai.r-lib.org" target="_blank" rel="noopener">mirai.r-lib.org</a>
 for the full documentation.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thank you to all the folks who helped make this release happen:</p>
<p><a href="https://github.com/agilly" target="_blank" rel="noopener">@agilly</a>
, <a href="https://github.com/aimundo" target="_blank" rel="noopener">@aimundo</a>
, <a href="https://github.com/barnabasharris" target="_blank" rel="noopener">@barnabasharris</a>
, <a href="https://github.com/beevabeeva" target="_blank" rel="noopener">@beevabeeva</a>
, <a href="https://github.com/boshek" target="_blank" rel="noopener">@boshek</a>
, <a href="https://github.com/eliocamp" target="_blank" rel="noopener">@eliocamp</a>
, <a href="https://github.com/jan-swissre" target="_blank" rel="noopener">@jan-swissre</a>
, <a href="https://github.com/jeroenjanssens" target="_blank" rel="noopener">@jeroenjanssens</a>
, <a href="https://github.com/kentqin-cve" target="_blank" rel="noopener">@kentqin-cve</a>
, <a href="https://github.com/mcol" target="_blank" rel="noopener">@mcol</a>
, <a href="https://github.com/michaelmayer2" target="_blank" rel="noopener">@michaelmayer2</a>
, <a href="https://github.com/pmac0451" target="_blank" rel="noopener">@pmac0451</a>
, <a href="https://github.com/r2evans" target="_blank" rel="noopener">@r2evans</a>
, <a href="https://github.com/shikokuchuo" target="_blank" rel="noopener">@shikokuchuo</a>
, <a href="https://github.com/t-kalinowski" target="_blank" rel="noopener">@t-kalinowski</a>
, <a href="https://github.com/VincentGuyader" target="_blank" rel="noopener">@VincentGuyader</a>
, <a href="https://github.com/wlandau" target="_blank" rel="noopener">@wlandau</a>
, and <a href="https://github.com/xwanner" target="_blank" rel="noopener">@xwanner</a>
.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Requires Posit Workbench version 2026.01 or later, which enables launcher authentication using the session cookie.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/mirai-2-6-0/thumbnail-wd.jpg" length="208853" type="image/jpeg" />
    </item>
    <item>
      <title>`dplyr::if_else()` and `dplyr::case_when()` are up to 30x faster</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/dplyr-performance/</link>
      <pubDate>Tue, 10 Feb 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/dplyr-performance/</guid>
      <dc:creator>Davis Vaughan</dc:creator><description><![CDATA[<p>In this technical post, we&rsquo;ll dive into some performance improvements we&rsquo;ve made to dplyr 1.2.0 to make <a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
 and <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 up to 30x faster and use up to 10x less memory.</p>
<p>If you haven&rsquo;t seen our <a href="https://tidyverse.org/blog/2026/02/dplyr-1-2-0/" target="_blank" rel="noopener">previous post</a>
 about the exciting new features in dplyr 1.2.0, you&rsquo;ll want to go check that out first!</p>
<p>Here&rsquo;s a before-and-after benchmark with <a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># Using https://github.com/DavisVaughan/cross</span></span>
<span><span class='nf'>cross</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/pkg/cross/man/bench_versions.html'>bench_versions</a></span><span class='o'>(</span>pkgs <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"tidyverse/dplyr@v1.1.4"</span>, <span class='s'>"tidyverse/dplyr"</span><span class='o'>)</span>, <span class='o'>&#123;</span></span>
<span>  <span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://dplyr.tidyverse.org'>dplyr</a></span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/Random.html'>set.seed</a></span><span class='o'>(</span><span class='m'>123</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nv'>condition</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>TRUE</span>, <span class='kc'>FALSE</span>, <span class='kc'>NA</span><span class='o'>)</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span>  <span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span>  <span class='nv'>y</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span>  <span class='nv'>z</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nf'>bench</span><span class='nf'>::</span><span class='nf'><a href='https://bench.r-lib.org/reference/mark.html'>mark</a></span><span class='o'>(</span>if_else <span class='o'>=</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/if_else.html'>if_else</a></span><span class='o'>(</span><span class='nv'>condition</span>, <span class='nv'>x</span>, <span class='nv'>y</span>, missing <span class='o'>=</span> <span class='nv'>z</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span></code></pre>
</div>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; # A tibble: 2 × 6</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   pkg                    expression      min   median `itr/sec` mem_alloc</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &lt;chr&gt;                  &lt;bch:expr&gt; &lt;bch:tm&gt; &lt;bch:tm&gt;     &lt;dbl&gt; &lt;bch:byt&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 tidyverse/dplyr@v1.1.4 if_else    248.25ms 249.25ms      4.02   381.6MB</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 2 tidyverse/dplyr        if_else      7.27ms   7.51ms    132.      38.2MB</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>And with <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>cross</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/pkg/cross/man/bench_versions.html'>bench_versions</a></span><span class='o'>(</span>pkgs <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"tidyverse/dplyr@v1.1.4"</span>, <span class='s'>"tidyverse/dplyr"</span><span class='o'>)</span>, <span class='o'>&#123;</span></span>
<span>  <span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://dplyr.tidyverse.org'>dplyr</a></span><span class='o'>)</span></span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/Random.html'>set.seed</a></span><span class='o'>(</span><span class='m'>123</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nv'>column</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>100</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nv'>x_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>20</span></span>
<span>  <span class='nv'>y_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>50</span></span>
<span>  <span class='nv'>z_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>80</span></span>
<span></span>
<span>  <span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span>  <span class='nv'>y</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span>  <span class='nv'>z</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span>  <span class='nf'>bench</span><span class='nf'>::</span><span class='nf'><a href='https://bench.r-lib.org/reference/mark.html'>mark</a></span><span class='o'>(</span></span>
<span>    case_when <span class='o'>=</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>      <span class='nv'>x_condition</span> <span class='o'>~</span> <span class='nv'>x</span>,</span>
<span>      <span class='nv'>y_condition</span> <span class='o'>~</span> <span class='nv'>y</span>,</span>
<span>      <span class='nv'>z_condition</span> <span class='o'>~</span> <span class='nv'>z</span></span>
<span>    <span class='o'>)</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span></code></pre>
</div>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; # A tibble: 2 × 6</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   pkg                    expression      min   median `itr/sec` mem_alloc</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &lt;chr&gt;                  &lt;bch:expr&gt; &lt;bch:tm&gt; &lt;bch:tm&gt;     &lt;dbl&gt; &lt;bch:byt&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 tidyverse/dplyr@v1.1.4 case_when   228.3ms  231.2ms      4.33   419.9MB</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 2 tidyverse/dplyr        case_when    15.5ms   15.8ms     62.8     38.3MB</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>So a 33x speed improvement for <a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
, a 15x speed improvement for <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
, and a 10x improvement in memory usage for both! In the rest of this post, we&rsquo;ll explain how we&rsquo;ve achieved these numbers.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://dplyr.tidyverse.org'>dplyr</a></span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="lets-talk-memory">Let&rsquo;s talk memory
</h2>
<p>We&rsquo;ll start with <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
, because <a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
 is actually just a small variant of that.</p>
<p>The most important place to start is with the memory usage. Memory usage and raw speed are often related, as allocating memory takes time. Let&rsquo;s look at the memory usage of <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 in dplyr 1.1.4:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/base/Random.html'>set.seed</a></span><span class='o'>(</span><span class='m'>123</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>column</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>100</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>x_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>20</span></span>
<span><span class='nv'>y_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>50</span></span>
<span><span class='nv'>z_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>80</span></span>
<span></span>
<span><span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>y</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>z</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span></code></pre>
</div>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">profmem</span><span class="o">::</span><span class="nf">profmem</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">threshold</span> <span class="o">=</span> <span class="m">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nf">case_when</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">x_condition</span> <span class="o">~</span> <span class="n">x</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">y_condition</span> <span class="o">~</span> <span class="n">y</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">z_condition</span> <span class="o">~</span> <span class="n">z</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Rprofmem memory profiling of:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; case_when(x_condition ~ x, y_condition ~ y, z_condition ~ z)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Memory allocations (&gt;= 1000 bytes):</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;        what     bytes                                           calls</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     alloc  40000048     case_when() -&gt; vec_case_when() -&gt; vec_rep()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 2     alloc  40000048                  case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 3     alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 4     alloc   7600664       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 5     alloc  40000048                  case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 6     alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 7     alloc  12003312       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 8     alloc  40000048                  case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 9     alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 10    alloc  11996112       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 11    alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 12    alloc   8400112       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 13    alloc   7600664   case_when() -&gt; vec_case_when() -&gt; vec_slice()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 14    alloc  12003312   case_when() -&gt; vec_case_when() -&gt; vec_slice()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 15    alloc  11996112   case_when() -&gt; vec_case_when() -&gt; vec_slice()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 16    alloc   8400112 case_when() -&gt; vec_case_when() -&gt; vec_recycle()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 17    alloc  40000048 case_when() -&gt; vec_case_when() -&gt; list_unchop()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; total       440000864</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>That&rsquo;s a lot of allocations! And it&rsquo;s pretty hard to understand where they are coming from without a bit more explanation. For that, we&rsquo;re actually going to &ldquo;manually&rdquo; implement an underpowered version of <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 for this example.</p>
<p>Here&rsquo;s a diagram of what we need to accomplish:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/tidyverse/dplyr-performance/images/x-y-z-default.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>In bullets:</p>
<ul>
<li><code>x_condition</code> selects the blue elements of <code>x</code></li>
<li><code>y_condition</code> selects the red elements of <code>y</code></li>
<li><code>z_condition</code> selects the green elements of <code>z</code></li>
<li>A <code>default</code> is built around the unused locations</li>
<li>We combine all of the pieces into <code>out</code></li>
</ul>
<p>The trickiest part about <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 is handling places where <code>x_condition</code> and <code>y_condition</code> overlap. In the image, even though both <code>x</code> and <code>y</code> are selected at location 5, only the value of <code>x</code> is retained since it is hit &ldquo;first&rdquo;. This forces us to have to modify <code>y_condition</code> to avoid already &ldquo;used&rdquo; locations.</p>
<p>An R implementation that computes these modified locations might look like:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>n</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/length.html'>length</a></span><span class='o'>(</span><span class='nv'>x_condition</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>unused</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/rep.html'>rep</a></span><span class='o'>(</span><span class='kc'>TRUE</span>, times <span class='o'>=</span> <span class='nv'>n</span><span class='o'>)</span> <span class='c'># 1</span></span>
<span></span>
<span><span class='nv'>x_loc</span> <span class='o'>&lt;-</span> <span class='nv'>unused</span> <span class='o'>&amp;</span> <span class='nv'>x_condition</span> <span class='c'># 2</span></span>
<span><span class='nv'>x_loc</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/which.html'>which</a></span><span class='o'>(</span><span class='nv'>x_loc</span><span class='o'>)</span> <span class='c'># 3,4</span></span>
<span><span class='nv'>unused</span><span class='o'>[</span><span class='nv'>x_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='kc'>FALSE</span></span>
<span></span>
<span><span class='nv'>y_loc</span> <span class='o'>&lt;-</span> <span class='nv'>unused</span> <span class='o'>&amp;</span> <span class='nv'>y_condition</span> <span class='c'># 5</span></span>
<span><span class='nv'>y_loc</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/which.html'>which</a></span><span class='o'>(</span><span class='nv'>y_loc</span><span class='o'>)</span> <span class='c'># 6,7</span></span>
<span><span class='nv'>unused</span><span class='o'>[</span><span class='nv'>y_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='kc'>FALSE</span></span>
<span></span>
<span><span class='nv'>z_loc</span> <span class='o'>&lt;-</span> <span class='nv'>unused</span> <span class='o'>&amp;</span> <span class='nv'>z_condition</span> <span class='c'># 8</span></span>
<span><span class='nv'>z_loc</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/which.html'>which</a></span><span class='o'>(</span><span class='nv'>z_loc</span><span class='o'>)</span> <span class='c'># 9,10</span></span>
<span><span class='nv'>unused</span><span class='o'>[</span><span class='nv'>z_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='kc'>FALSE</span></span></code></pre>
</div>
<p>Anything that is still <code>unused</code> falls through to the <code>default</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>default</span> <span class='o'>&lt;-</span> <span class='kc'>NA_integer_</span></span>
<span><span class='nv'>default_loc</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/which.html'>which</a></span><span class='o'>(</span><span class='nv'>unused</span><span class='o'>)</span> <span class='c'># 11,12</span></span></code></pre>
</div>
<p>With <code>x_loc</code>, <code>y_loc</code>, <code>z_loc</code>, and <code>default_loc</code> in hand, we can build the output from the pieces:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/vector.html'>vector</a></span><span class='o'>(</span><span class='s'>"integer"</span>, length <span class='o'>=</span> <span class='nv'>n</span><span class='o'>)</span> <span class='c'># 17</span></span>
<span></span>
<span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>x_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nv'>x</span><span class='o'>[</span><span class='nv'>x_loc</span><span class='o'>]</span> <span class='c'># 13</span></span>
<span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>y_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nv'>y</span><span class='o'>[</span><span class='nv'>y_loc</span><span class='o'>]</span> <span class='c'># 14</span></span>
<span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>z_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nv'>z</span><span class='o'>[</span><span class='nv'>z_loc</span><span class='o'>]</span> <span class='c'># 15</span></span>
<span></span>
<span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>default_loc</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/rep.html'>rep</a></span><span class='o'>(</span><span class='nv'>default</span>, times <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/length.html'>length</a></span><span class='o'>(</span><span class='nv'>default_loc</span><span class='o'>)</span><span class='o'>)</span> <span class='c'># 16</span></span></code></pre>
</div>
<p>And sure enough, this is identical to <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/base/identical.html'>identical</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>out</span>,</span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>    <span class='nv'>x_condition</span> <span class='o'>~</span> <span class='nv'>x</span>,</span>
<span>    <span class='nv'>y_condition</span> <span class='o'>~</span> <span class='nv'>y</span>,</span>
<span>    <span class='nv'>z_condition</span> <span class='o'>~</span> <span class='nv'>z</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] TRUE</span></span>
<span></span></code></pre>
</div>
<p>You might be wondering what all of the comments with numbers beside them mean. Those actually map 1:1 with the allocations that <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 was emitting. In fact, we can now split up those allocations into their respective role:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; # Tracking `unused` locations</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     alloc  40000048     case_when() -&gt; vec_case_when() -&gt; vec_rep()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # Computing `x_loc`, `y_loc`, and `z_loc`</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 2     alloc  40000048                  case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 3     alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 4     alloc   7600664       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 5     alloc  40000048                  case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 6     alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 7     alloc  12003312       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 8     alloc  40000048                  case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 9     alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 10    alloc  11996112       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # Computing `default_loc`</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 11    alloc  40000056       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 12    alloc   8400112       case_when() -&gt; vec_case_when() -&gt; which()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # Slicing `x`, `y`, and `z` to align with `x_loc`, `y_loc`, and `z_loc`</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 13    alloc   7600664   case_when() -&gt; vec_case_when() -&gt; vec_slice()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 14    alloc  12003312   case_when() -&gt; vec_case_when() -&gt; vec_slice()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 15    alloc  11996112   case_when() -&gt; vec_case_when() -&gt; vec_slice()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # Recycling `default` of `NA` to align with `default_loc`</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 16    alloc   8400112 case_when() -&gt; vec_case_when() -&gt; vec_recycle()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # Final output container, which we assign `x`, `y`, `z`, and `default` into</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; # at locations `x_loc`, `y_loc`, and `z_loc`</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 17    alloc  40000048 case_when() -&gt; vec_case_when() -&gt; list_unchop()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>We sought to remove every one of these allocations except for the last one, which is the final output container that is returned to the user. In other words, we were after this, which is the actual profmem result of this <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 call in dplyr 1.2.0:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; Rprofmem memory profiling of:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; case_when(x_condition ~ x, y_condition ~ y, z_condition ~ z)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Memory allocations (&gt;= 1000 bytes):</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;        what    bytes                          calls</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     alloc 40000048 case_when() -&gt; vec_case_when()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; total       40000048</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="sliced-assignment">Sliced assignment
</h2>
<p>To work towards this, let&rsquo;s focus on what happens to <code>x</code> throughout this process:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/tidyverse/dplyr-performance/images/just-x.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>We had a hypothesis that we could cut out the intermediate work here. Ideally, we&rsquo;d take the logical LHS <code>x_condition</code> and the RHS <code>x</code> and map that straight into the output, with no extra allocations:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/tidyverse/dplyr-performance/images/just-x-ideal.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>But this just wasn&rsquo;t possible with the way that assignment typically works in R!</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"a"</span>, <span class='s'>"b"</span>, <span class='s'>"c"</span>, <span class='s'>"d"</span>, <span class='s'>"e"</span>, <span class='s'>"f"</span>, <span class='s'>"g"</span><span class='o'>)</span></span>
<span><span class='nv'>x_condition</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>FALSE</span>, <span class='kc'>TRUE</span>, <span class='kc'>FALSE</span>, <span class='kc'>FALSE</span>, <span class='kc'>TRUE</span>, <span class='kc'>FALSE</span>, <span class='kc'>FALSE</span><span class='o'>)</span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/vector.html'>vector</a></span><span class='o'>(</span><span class='s'>"character"</span>, length <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/length.html'>length</a></span><span class='o'>(</span><span class='nv'>x_condition</span><span class='o'>)</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>x_condition</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nv'>x</span></span>
<span><span class='c'>#&gt; Warning in out[x_condition] &lt;- x: number of items to replace is not a multiple of replacement length</span></span>
<span></span></code></pre>
</div>
<p>Instead, you must pre-slice <code>x</code> to a length that matches the locations that <code>x_condition</code> points to in <code>out</code>, i.e.:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>x_condition</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nv'>x</span><span class='o'>[</span><span class='nv'>x_condition</span><span class='o'>]</span></span></code></pre>
</div>
<p>Now, in <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 we don&rsquo;t actually use <code>[&lt;-</code> for assignment or <code>[</code> for slicing. Instead, we use tools from <a href="https://vctrs.r-lib.org/" target="_blank" rel="noopener">vctrs</a>
, a low level package for building consistent tidyverse functions. In this case, we&rsquo;d use <code>vctrs::vec_assign()</code> and <code>vctrs::vec_slice()</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>x_condition</span>, <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_slice</a></span><span class='o'>(</span><span class='nv'>x</span>, <span class='nv'>x_condition</span><span class='o'>)</span><span class='o'>)</span></span></code></pre>
</div>
<p>But <code>vec_assign()</code> had the same problem!</p>
<p>To solve this, we&rsquo;ve added a new boolean argument to <code>vec_assign()</code> called <code>slice_value</code>. You use it like this:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>x_condition</span>, <span class='nv'>x</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span></code></pre>
</div>
<p>With <code>slice_value = TRUE</code>, <code>vec_assign()</code> assumes that both <code>out</code> and <code>x</code> are the same length and that <code>x_condition</code> applies to <em>both</em> of these. Internally, rather than materializing <code>x[x_condition]</code>, we instead just loop over both <code>out</code> and <code>x</code> at the same time (at C level) and copy over values from <code>x</code> whenever <code>x_condition</code> is <code>TRUE</code>.</p>
<p>This is huge! It means that allocations 13-15 from above related to slicing <code>x</code>, <code>y</code>, and <code>z</code> all disappear.</p>
<h2 id="logical-indices">Logical <code>i</code>ndices
</h2>
<p>You might have noticed that we&rsquo;ve been using <code>which()</code> quite a bit in the above algorithm. This turns a logical vector of <code>TRUE</code> and <code>FALSE</code> into an integer vector of locations pointing to where the logical vector was <code>TRUE</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>x_condition</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>TRUE</span>, <span class='kc'>FALSE</span>, <span class='kc'>TRUE</span>, <span class='kc'>FALSE</span>, <span class='kc'>FALSE</span>, <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>x_loc</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/which.html'>which</a></span><span class='o'>(</span><span class='nv'>x_condition</span><span class='o'>)</span></span>
<span><span class='nv'>x_loc</span></span>
<span><span class='c'>#&gt; [1] 1 3 6</span></span>
<span></span></code></pre>
</div>
<p>We perform this conversion up front due to how the following works at C level:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span><span class='o'>[</span><span class='nv'>x_condition</span><span class='o'>]</span> <span class='o'>&lt;-</span> <span class='nv'>x</span><span class='o'>[</span><span class='nv'>x_condition</span><span class='o'>]</span></span></code></pre>
</div>
<p>Both <code>[</code> and <code>[&lt;-</code> will convert a logical <code>x_condition</code> into the integer <code>x_loc</code> form before proceeding with the assignment, meaning that <code>which()</code> gets called twice if we don&rsquo;t do it once up front. And vctrs is the same way! Both <code>vec_assign()</code> and <code>vec_slice()</code> here would convert <code>x_condition</code> to an integer vector.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>x_condition</span>, <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_slice</a></span><span class='o'>(</span><span class='nv'>x</span>, <span class='nv'>x_condition</span><span class='o'>)</span><span class='o'>)</span></span></code></pre>
</div>
<p>Now, with the previous optimization we&rsquo;ve already seen that we can reduce this to:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>x_condition</span>, <span class='nv'>x</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span></code></pre>
</div>
<p>But <code>vec_assign()</code> still converts a logical <code>x_condition</code> to integer locations internally before doing the assignment. So now it doesn&rsquo;t matter whether we do this conversion up front via <code>which()</code> or if we let <code>vec_assign()</code> do it, it still happens once per input. But we&rsquo;d like to avoid it entirely!</p>
<p>The solution here wasn&rsquo;t too magical, it just involved a good bit of grunt work. We&rsquo;ve added a path in <code>vec_assign()</code>&rsquo;s <a href="https://github.com/r-lib/vctrs/blob/94cea16b1ed3939aaa59c58dda75eedc75d6d075/src/slice-assign.c#L390-L417" target="_blank" rel="noopener">C code</a>
 that can handle logical indices like <code>x_condition</code> directly, rather than forcing them to be converted to integer locations first.</p>
<p>But this is a huge win, because it means that allocations 1-10, which were all related to <a href="https://rdrr.io/r/base/which.html" target="_blank" rel="noopener"><code>which()</code></a>
, can now be removed. <code>vec_assign()</code> will just handle that optimally for us without any extra allocations.</p>
<p>The nice part about an optimization like this is that any other existing code that is using <code>vec_assign()</code> with a logical index will also benefit from this without having to change a thing!</p>
<h2 id="default-handling"><code>default</code> handling
</h2>
<p>The remaining allocations are 11-12 and 16, which all have to do with the implied <code>default</code>. Allocations 11-12 were about figuring out where to put <code>default</code>, and allocation 16 was about recycling a typed size 1 <code>default</code> to the right size before assigning it into <code>out</code>.</p>
<p>As it turns out, we don&rsquo;t need any of this!</p>
<p>In vctrs, when we initialize any output container, we use <code>vec_init()</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_init.html'>vec_init</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/integer.html'>integer</a></span><span class='o'>(</span><span class='o'>)</span>, n <span class='o'>=</span> <span class='m'>5</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] NA NA NA NA NA</span></span>
<span></span><span></span>
<span><span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_init.html'>vec_init</a></span><span class='o'>(</span><span class='nf'><a href='https://tibble.tidyverse.org/reference/tibble.html'>tibble</a></span><span class='o'>(</span>x <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/integer.html'>integer</a></span><span class='o'>(</span><span class='o'>)</span>, y <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/character.html'>character</a></span><span class='o'>(</span><span class='o'>)</span><span class='o'>)</span>, n <span class='o'>=</span> <span class='m'>5</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 5 × 2</span></span></span>
<span><span class='c'>#&gt;       x y    </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;int&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span>    <span style='color: #BB0000;'>NA</span> <span style='color: #BB0000;'>NA</span>   </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span>    <span style='color: #BB0000;'>NA</span> <span style='color: #BB0000;'>NA</span>   </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span>    <span style='color: #BB0000;'>NA</span> <span style='color: #BB0000;'>NA</span>   </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span>    <span style='color: #BB0000;'>NA</span> <span style='color: #BB0000;'>NA</span>   </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span>    <span style='color: #BB0000;'>NA</span> <span style='color: #BB0000;'>NA</span></span></span>
<span></span></code></pre>
</div>
<p>This <em>already</em> has the implied <code>default</code> assigned to every location. We then overwrite this with <code>x</code>, <code>y</code>, and <code>z</code> at the appropriate locations, but anything left untouched by those is still set to the <code>default</code>, so we&rsquo;re done!</p>
<p>For cases where the user supplies their own <code>default</code>, things are slightly more complicated. We actually do have to compute a <code>default_loc</code> implied from <code>x_condition</code>, <code>y_condition</code>, and <code>z_condition</code>, but internally we do so using a C vector of <code>bool</code> (even more efficient than R&rsquo;s logical vector type), so the memory footprint is as small as it can be.</p>
<h2 id="the-first-wins-conundrum">The &ldquo;first wins&rdquo; conundrum
</h2>
<p>One thing we&rsquo;ve skipped over is the &ldquo;first wins&rdquo; behavior of <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 mentioned earlier. Now that we&rsquo;ve removed <code>x_loc</code>, <code>y_loc</code>, and <code>z_loc</code>, which is where that was being handled, how do we keep this behavior without slowing things down?</p>
<p>To be explicit, we are talking about this feature of <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 where only the first hit is kept when you have overlapping logical indices:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"x1"</span>, <span class='s'>"x2"</span>, <span class='s'>"x3"</span><span class='o'>)</span></span>
<span><span class='nv'>y</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"y1"</span>, <span class='s'>"y2"</span>, <span class='s'>"y3"</span><span class='o'>)</span></span>
<span><span class='nv'>z</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"z1"</span>, <span class='s'>"z2"</span>, <span class='s'>"z3"</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>x_condition</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>TRUE</span>, <span class='kc'>FALSE</span>, <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>y_condition</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>TRUE</span>, <span class='kc'>TRUE</span>, <span class='kc'>FALSE</span><span class='o'>)</span></span>
<span><span class='nv'>z_condition</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>FALSE</span>, <span class='kc'>TRUE</span>, <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>x_condition</span> <span class='o'>~</span> <span class='nv'>x</span>,</span>
<span>  <span class='nv'>y_condition</span> <span class='o'>~</span> <span class='nv'>y</span>,</span>
<span>  <span class='nv'>z_condition</span> <span class='o'>~</span> <span class='nv'>z</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] "x1" "y2" "x3"</span></span>
<span></span></code></pre>
</div>
<p>A naive approach doesn&rsquo;t work, as you end up with &ldquo;last wins&rdquo; behavior:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_init.html'>vec_init</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/character.html'>character</a></span><span class='o'>(</span><span class='o'>)</span>, n <span class='o'>=</span> <span class='m'>3</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>x_condition</span>, <span class='nv'>x</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>y_condition</span>, <span class='nv'>y</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>z_condition</span>, <span class='nv'>z</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span><span class='c'># This is wrong!</span></span>
<span><span class='nv'>out</span></span>
<span><span class='c'>#&gt; [1] "y1" "z2" "z3"</span></span>
<span></span><span></span>
<span><span class='nf'><a href='https://rdrr.io/r/base/identical.html'>identical</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>out</span>,</span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>    <span class='nv'>x_condition</span> <span class='o'>~</span> <span class='nv'>x</span>,</span>
<span>    <span class='nv'>y_condition</span> <span class='o'>~</span> <span class='nv'>y</span>,</span>
<span>    <span class='nv'>z_condition</span> <span class='o'>~</span> <span class='nv'>z</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] FALSE</span></span>
<span></span></code></pre>
</div>
<p>Instead, <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 just <em>iterates in reverse</em>, assigning <code>z</code>, then <code>y</code>, then <code>x</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_init.html'>vec_init</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/character.html'>character</a></span><span class='o'>(</span><span class='o'>)</span>, n <span class='o'>=</span> <span class='m'>3</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>z_condition</span>, <span class='nv'>z</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>y_condition</span>, <span class='nv'>y</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/vec_slice.html'>vec_assign</a></span><span class='o'>(</span><span class='nv'>out</span>, <span class='nv'>x_condition</span>, <span class='nv'>x</span>, slice_value <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span><span class='nf'><a href='https://rdrr.io/r/base/identical.html'>identical</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>out</span>,</span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>    <span class='nv'>x_condition</span> <span class='o'>~</span> <span class='nv'>x</span>,</span>
<span>    <span class='nv'>y_condition</span> <span class='o'>~</span> <span class='nv'>y</span>,</span>
<span>    <span class='nv'>z_condition</span> <span class='o'>~</span> <span class='nv'>z</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] TRUE</span></span>
<span></span></code></pre>
</div>
<p>This diagram demonstrates how that works:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/tidyverse/dplyr-performance/images/case-when-reverse.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<h2 id="optimizing-speed">Optimizing speed?
</h2>
<p>Now that we&rsquo;ve optimized the memory usage of <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
, you might be wondering if we did anything else to specifically optimize its speed. Not really! We have moved everything from R to C, but focusing our efforts on reducing memory also resulted in some pretty performant code, and there wasn&rsquo;t much left to optimize after that.</p>
<h2 id="if_else"><code>if_else()</code>
</h2>
<p><a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
 can actually be written as a form of <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/if_else.html'>if_else</a></span><span class='o'>(</span><span class='nv'>condition</span>, <span class='nv'>true</span>, <span class='nv'>false</span>, <span class='nv'>missing</span><span class='o'>)</span></span>
<span></span>
<span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>condition</span> <span class='o'>~</span> <span class='nv'>true</span>,</span>
<span>  <span class='o'>!</span><span class='nv'>condition</span> <span class='o'>~</span> <span class='nv'>false</span>,</span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/NA.html'>is.na</a></span><span class='o'>(</span><span class='nv'>condition</span><span class='o'>)</span> <span class='o'>~</span> <span class='nv'>missing</span></span>
<span><span class='o'>)</span></span></code></pre>
</div>
<p>In our actual C implementation of <a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
, for simple types like integer, character, or numeric vectors we have an <a href="https://github.com/r-lib/vctrs/blob/94cea16b1ed3939aaa59c58dda75eedc75d6d075/src/if-else.c#L276-L505" target="_blank" rel="noopener">extremely fast path</a>
 that&rsquo;s even more optimized than this, but for anything with a class we pretty much use this exact <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 approach.</p>
<h2 id="for-package-developers">For package developers
</h2>
<p>If you&rsquo;re a package developer, you&rsquo;ll be happy to know that vctrs itself now exposes low dependency versions of <a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
 and <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
, here&rsquo;s the full family:</p>
<ul>
<li><code>vec_if_else()</code></li>
<li><code>vec_case_when()</code></li>
<li><code>vec_replace_when()</code></li>
<li><code>vec_recode_values()</code></li>
<li><code>vec_replace_values()</code></li>
</ul>
<p><a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>dplyr::if_else()</code></a>
 and friends are now just very thin wrappers over these. Feel free to use the vctrs versions in your package if you need the consistency of the tidyverse without the heavy-ish dependency of dplyr.</p>
<h2 id="at-the-deepest-level-list_combine">At the deepest level, <code>list_combine()</code>
</h2>
<p>At the deepest level of all of this is one final new vctrs function, <code>list_combine()</code>. This is a flexible way to combine multiple vectors together at locations specified by <code>indices</code>.</p>
<p><code>list_combine()</code> powers all of <code>vec_case_when()</code>, <code>vec_replace_when()</code>, <code>vec_recode_values()</code>, <code>vec_replace_values()</code>, <code>vec_if_else()</code>, and even <code>vec_c()</code>, the tidyverse version of <a href="https://rdrr.io/r/base/c.html" target="_blank" rel="noopener"><code>c()</code></a>
.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/base/Random.html'>set.seed</a></span><span class='o'>(</span><span class='m'>123</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>column</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>100</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>x_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>20</span></span>
<span><span class='nv'>y_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>50</span></span>
<span><span class='nv'>z_condition</span> <span class='o'>&lt;-</span> <span class='nv'>column</span> <span class='o'>&lt;</span> <span class='m'>80</span></span>
<span></span>
<span><span class='nv'>x</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>y</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>z</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/sample.html'>sample</a></span><span class='o'>(</span><span class='m'>10</span>, size <span class='o'>=</span> <span class='m'>1e7</span>, replace <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>out</span> <span class='o'>&lt;-</span> <span class='nf'>vctrs</span><span class='nf'>::</span><span class='nf'><a href='https://vctrs.r-lib.org/reference/list_combine.html'>list_combine</a></span><span class='o'>(</span></span>
<span>  x <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span><span class='nv'>x</span>, <span class='nv'>y</span>, <span class='nv'>z</span><span class='o'>)</span>,</span>
<span></span>
<span>  <span class='c'># `indices` are allowed to be logical and aren't forced to integer</span></span>
<span>  indices <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span><span class='nv'>x_condition</span>, <span class='nv'>y_condition</span>, <span class='nv'>z_condition</span><span class='o'>)</span>,</span>
<span>  size <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/length.html'>length</a></span><span class='o'>(</span><span class='nv'>x_condition</span><span class='o'>)</span>,</span>
<span></span>
<span>  <span class='c'># When there are overlaps, take the "first"</span></span>
<span>  multiple <span class='o'>=</span> <span class='s'>"first"</span>,</span>
<span></span>
<span>  <span class='c'># Same as `slice_value` from `vec_assign()`</span></span>
<span>  slice_x <span class='o'>=</span> <span class='kc'>TRUE</span></span>
<span><span class='o'>)</span></span></code></pre>
</div>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/base/identical.html'>identical</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>out</span>,</span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>    <span class='nv'>x_condition</span> <span class='o'>~</span> <span class='nv'>x</span>,</span>
<span>    <span class='nv'>y_condition</span> <span class='o'>~</span> <span class='nv'>y</span>,</span>
<span>    <span class='nv'>z_condition</span> <span class='o'>~</span> <span class='nv'>z</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] TRUE</span></span>
<span></span></code></pre>
</div>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/dplyr-performance/thumbnail-wd.jpg" length="179655" type="image/jpeg" />
    </item>
    <item>
      <title>nanonext 1.8.0</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/nanonext-1-8-0/</link>
      <pubDate>Mon, 09 Feb 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/nanonext-1-8-0/</guid>
      <dc:creator>Charlie Gao</dc:creator><description><![CDATA[<!--
TODO:
* [x] Look over / edit the post's title in the yaml
* [x] Edit (or delete) the description; note this appears in the Twitter card
* [x] Pick category and tags (see existing with [`hugodown::tidy_show_meta()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html))
* [x] Find photo & update yaml metadata
* [x] Create `thumbnail-sq.jpg`; height and width should be equal
* [x] Create `thumbnail-wd.jpg`; width should be >5x height
* [x] [`hugodown::use_tidy_thumbnails()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html)
* [x] Add intro sentence, e.g. the standard tagline for the package
* [x] [`usethis::use_tidy_thanks()`](https://usethis.r-lib.org/reference/use_tidy_thanks.html)
-->
<p>When we <a href="https://posit-open-source.netlify.app/blog/tidyverse/2025/nanonext-1-7-0/">introduced nanonext</a>
 last year, we showed how it connects R directly to Python, Go, Rust, and other languages through NNG&rsquo;s messaging protocols. We hinted at its web capabilities &ndash; but that was just the beginning.</p>
<p>R already has excellent web infrastructure. <a href="https://shiny.posit.co/" target="_blank" rel="noopener">Shiny</a>
 and <a href="https://plumber2.posit.co/" target="_blank" rel="noopener">plumber2</a>
 are the go-to tools for building interactive applications and REST APIs in R. They are both powered by <a href="https://rstudio.github.io/httpuv/" target="_blank" rel="noopener">httpuv</a>
. <a href="https://nanonext.r-lib.org" target="_blank" rel="noopener">nanonext</a>
 adds a complementary option at the httpuv level of the stack &ndash; a low-level streaming HTTP and WebSocket server built on NNG, giving developers fine-grained control over connections, streaming, and static file serving over TLS. nanonext is for when you need lower-level control &ndash; custom protocols, infrastructure endpoints, or embedding a server alongside an existing Shiny or plumber2 application.</p>
<p>You can install it from CRAN with:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;nanonext&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>You can see a full list of changes in the <a href="https://nanonext.r-lib.org/news/#nanonext-180" target="_blank" rel="noopener">release notes</a>
.</p>
<h2 id="streaming-httpwebsocket-server">Streaming HTTP/WebSocket server
</h2>
<p>The flagship feature of this release is <a href="https://nanonext.r-lib.org/reference/http_server.html" target="_blank" rel="noopener"><code>http_server()</code></a>
, a streaming HTTP and WebSocket server with full TLS support. Built on NNG&rsquo;s HTTP server architecture, it brings the same performance that powers nanonext&rsquo;s messaging layer to web serving.</p>
<p>One server, one port &ndash; HTTP endpoints, WebSocket connections, and streaming all coexist. Static files bypass R entirely, served natively by NNG. WebSocket and streaming connections run callbacks on R&rsquo;s main thread via the later package. Mbed TLS is built in for HTTPS/WSS, and there&rsquo;s no need to run separate processes or bind additional ports.</p>
<p>Because it shares the same event loop that Shiny uses, <a href="https://nanonext.r-lib.org/reference/http_server.html" target="_blank" rel="noopener"><code>http_server()</code></a>
 can run alongside a Shiny app in the same R process. You could spin up a nanonext server to handle health checks, serve static assets, or stream real-time events &ndash; while Shiny or plumber2 handles the application logic. They&rsquo;re designed to work together.</p>
<p>As part of our investment in expanding what&rsquo;s possible with R, we&rsquo;re already using nanonext at Posit to explore new real-time capabilities, and we&rsquo;re excited to see what the community builds with it.</p>
<h3 id="basic-http-server">Basic HTTP server
</h3>
<p>Where frameworks like plumber2 give you a full-featured API layer with routing, serialization, and documentation out of the box, <a href="https://nanonext.r-lib.org/reference/http_server.html" target="_blank" rel="noopener"><code>http_server()</code></a>
 gives you direct control over requests and responses &ndash; the kind of direct access you&rsquo;d reach for when building custom infrastructure or embedding a server inside a larger system.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">nanonext</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="nf">http_server</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">url</span> <span class="o">=</span> <span class="s">&#34;http://127.0.0.1:8080&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">handlers</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">handler</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="kr">function</span><span class="p">(</span><span class="n">req</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nf">list</span><span class="p">(</span><span class="n">status</span> <span class="o">=</span> <span class="m">200L</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="s">&#34;Hello from nanonext!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}),</span>
</span></span><span class="line"><span class="cl">    <span class="nf">handler</span><span class="p">(</span><span class="s">&#34;/api/data&#34;</span><span class="p">,</span> <span class="kr">function</span><span class="p">(</span><span class="n">req</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">status</span> <span class="o">=</span> <span class="m">200L</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">headers</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span> <span class="o">=</span> <span class="s">&#34;application/json&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">body</span> <span class="o">=</span> <span class="s">&#39;{&#34;value&#34;: 42}&#39;</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span> <span class="n">method</span> <span class="o">=</span> <span class="s">&#34;GET&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">server</span><span class="o">$</span><span class="nf">start</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Handlers receive a request and return a response list. You can freely mix handler types in a single server:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Handler</th>
          <th style="text-align: left">Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><a href="https://nanonext.r-lib.org/reference/handler.html" target="_blank" rel="noopener"><code>handler()</code></a>
</td>
          <td style="text-align: left">HTTP request/response with R callback</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://nanonext.r-lib.org/reference/handler_ws.html" target="_blank" rel="noopener"><code>handler_ws()</code></a>
</td>
          <td style="text-align: left">WebSocket with <code>on_message</code>, <code>on_open</code>, <code>on_close</code> callbacks</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://nanonext.r-lib.org/reference/handler_stream.html" target="_blank" rel="noopener"><code>handler_stream()</code></a>
</td>
          <td style="text-align: left">Chunked HTTP streaming (SSE, NDJSON, custom)</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://nanonext.r-lib.org/reference/handler_file.html" target="_blank" rel="noopener"><code>handler_file()</code></a>
</td>
          <td style="text-align: left">Serve a single static file</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://nanonext.r-lib.org/reference/handler_directory.html" target="_blank" rel="noopener"><code>handler_directory()</code></a>
</td>
          <td style="text-align: left">Serve a directory tree with automatic MIME types</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://nanonext.r-lib.org/reference/handler_inline.html" target="_blank" rel="noopener"><code>handler_inline()</code></a>
</td>
          <td style="text-align: left">Serve in-memory content</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://nanonext.r-lib.org/reference/handler_redirect.html" target="_blank" rel="noopener"><code>handler_redirect()</code></a>
</td>
          <td style="text-align: left">HTTP redirect</td>
      </tr>
  </tbody>
</table>
<p>Specifying port <code>0</code> in the URL lets the operating system assign an available port. The actual port is reflected in <code>server$url</code> after <code>$start()</code>, so you can set up test servers without worrying about port conflicts.</p>
<h3 id="static-file-serving">Static file serving
</h3>
<p>Static handlers bypass R entirely &ndash; NNG serves content directly and efficiently:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">handler_directory</span><span class="p">(</span><span class="s">&#34;/static&#34;</span><span class="p">,</span> <span class="s">&#34;www/assets&#34;</span><span class="p">)</span>  <span class="c1"># serve a folder</span>
</span></span><span class="line"><span class="cl"><span class="nf">handler_file</span><span class="p">(</span><span class="s">&#34;/favicon.ico&#34;</span><span class="p">,</span> <span class="s">&#34;favicon.ico&#34;</span><span class="p">)</span> <span class="c1"># serve a single file</span>
</span></span><span class="line"><span class="cl"><span class="nf">handler_inline</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;/robots.txt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;User-agent: *\nDisallow:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">content_type</span> <span class="o">=</span> <span class="s">&#34;text/plain&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="c1"># serve in-memory content</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>For example, you can serve a rendered Quarto website with a single handler:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="nf">http_server</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">url</span> <span class="o">=</span> <span class="s">&#34;http://127.0.0.1:0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">handlers</span> <span class="o">=</span> <span class="nf">handler_directory</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="s">&#34;_site&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">server</span><span class="o">$</span><span class="nf">start</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">server</span><span class="o">$</span><span class="n">url</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Browse to the URL to see your Quarto site</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="websocket-server">WebSocket server
</h3>
<p>WebSockets provide full bidirectional communication &ndash; the server can push messages to the client, and the client can send messages back. WebSocket and HTTP handlers share the same server and port, so a browser can load a page over HTTP and open a WebSocket to the same origin &ndash; no cross-origin configuration needed:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="nf">http_server</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">url</span> <span class="o">=</span> <span class="s">&#34;http://127.0.0.1:8080&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">handlers</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">handler</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="kr">function</span><span class="p">(</span><span class="n">req</span><span class="p">)</span> <span class="nf">list</span><span class="p">(</span><span class="n">status</span> <span class="o">=</span> <span class="m">200L</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="s">&#34;&lt;html&gt;...&lt;/html&gt;&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">    <span class="nf">handler_ws</span><span class="p">(</span><span class="s">&#34;/ws&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">on_message</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">ws</span><span class="p">,</span> <span class="n">data</span><span class="p">)</span> <span class="n">ws</span><span class="o">$</span><span class="nf">send</span><span class="p">(</span><span class="n">data</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="n">on_open</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">ws</span><span class="p">)</span> <span class="nf">cat</span><span class="p">(</span><span class="s">&#34;connected:&#34;</span><span class="p">,</span> <span class="n">ws</span><span class="o">$</span><span class="n">id</span><span class="p">,</span> <span class="s">&#34;\n&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="n">on_close</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">ws</span><span class="p">)</span> <span class="nf">cat</span><span class="p">(</span><span class="s">&#34;disconnected:&#34;</span><span class="p">,</span> <span class="n">ws</span><span class="o">$</span><span class="n">id</span><span class="p">,</span> <span class="s">&#34;\n&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This makes it easy to build lightweight real-time services &ndash; monitoring endpoints or live-updating feeds that push results to the browser as they arrive.</p>
<h3 id="http-streaming-and-server-sent-events">HTTP streaming and Server-Sent Events
</h3>
<p>When you only need to push data in one direction &ndash; server to client &ndash; streaming is a lighter-weight alternative to WebSockets. It works over plain HTTP, so any client that speaks HTTP can consume the stream without needing a WebSocket library. <a href="https://nanonext.r-lib.org/reference/handler_stream.html" target="_blank" rel="noopener"><code>handler_stream()</code></a>
 enables chunked transfer encoding for streaming responses:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">conns</span> <span class="o">&lt;-</span> <span class="nf">list</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">handler_stream</span><span class="p">(</span><span class="s">&#34;/events&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">on_request</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">req</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">$</span><span class="nf">set_header</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span><span class="p">,</span> <span class="s">&#34;text/event-stream&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">$</span><span class="nf">set_header</span><span class="p">(</span><span class="s">&#34;Cache-Control&#34;</span><span class="p">,</span> <span class="s">&#34;no-cache&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">conns[</span><span class="nf">[as.character</span><span class="p">(</span><span class="n">conn</span><span class="o">$</span><span class="n">id</span><span class="p">)</span><span class="n">]]</span> <span class="o">&lt;&lt;-</span> <span class="n">conn</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">$</span><span class="nf">send</span><span class="p">(</span><span class="nf">format_sse</span><span class="p">(</span><span class="n">data</span> <span class="o">=</span> <span class="s">&#34;connected&#34;</span><span class="p">,</span> <span class="n">id</span> <span class="o">=</span> <span class="s">&#34;1&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">on_close</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">conns[</span><span class="nf">[as.character</span><span class="p">(</span><span class="n">conn</span><span class="o">$</span><span class="n">id</span><span class="p">)</span><span class="n">]]</span> <span class="o">&lt;&lt;-</span> <span class="kc">NULL</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The <a href="https://nanonext.r-lib.org/reference/format_sse.html" target="_blank" rel="noopener"><code>format_sse()</code></a>
 helper formats messages per the SSE specification. On the browser side, updates arrive automatically as they happen &ndash; no page refreshes or repeated requests needed. Streaming also supports NDJSON and custom formats &ndash; useful for streaming model training progress, sensor readings, monitoring endpoints, or pipeline notifications.</p>
<h3 id="tlsssl-support">TLS/SSL support
</h3>
<p>For HTTPS, pass a TLS configuration. nanonext bundles Mbed TLS, so there&rsquo;s nothing extra to install:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">cert</span> <span class="o">&lt;-</span> <span class="nf">write_cert</span><span class="p">(</span><span class="n">cn</span> <span class="o">=</span> <span class="s">&#34;127.0.0.1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="nf">http_server</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">url</span> <span class="o">=</span> <span class="s">&#34;https://127.0.0.1:0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">handlers</span> <span class="o">=</span> <span class="nf">handler</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="kr">function</span><span class="p">(</span><span class="n">req</span><span class="p">)</span> <span class="nf">list</span><span class="p">(</span><span class="n">status</span> <span class="o">=</span> <span class="m">200L</span><span class="p">,</span> <span class="n">body</span> <span class="o">=</span> <span class="s">&#34;Secure!&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">  <span class="n">tls</span> <span class="o">=</span> <span class="nf">tls_config</span><span class="p">(</span><span class="n">server</span> <span class="o">=</span> <span class="n">cert</span><span class="o">$</span><span class="n">server</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="full-response-headers-for-http-client">Full response headers for HTTP client
</h2>
<p><a href="https://nanonext.r-lib.org/reference/ncurl.html" target="_blank" rel="noopener"><code>ncurl()</code></a>
 now accepts <code>response = TRUE</code> to return all response headers:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>resp</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://nanonext.r-lib.org/reference/ncurl.html'>ncurl</a></span><span class='o'>(</span><span class='s'>"https://postman-echo.com/get"</span>, response <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span></span>
<span><span class='nv'>resp</span><span class='o'>$</span><span class='nv'>headers</span> <span class='o'>|&gt;</span> <span class='nf'><a href='https://rdrr.io/r/base/names.html'>names</a></span><span class='o'>(</span><span class='o'>)</span></span>
<span><span class='c'>#&gt;  [1] "Date"                          "Content-Type"                 </span></span>
<span><span class='c'>#&gt;  [3] "Content-Length"                "Connection"                   </span></span>
<span><span class='c'>#&gt;  [5] "CF-RAY"                        "etag"                         </span></span>
<span><span class='c'>#&gt;  [7] "vary"                          "Set-Cookie"                   </span></span>
<span><span class='c'>#&gt;  [9] "x-envoy-upstream-service-time" "cf-cache-status"              </span></span>
<span><span class='c'>#&gt; [11] "Server"</span></span>
<span></span></code></pre>
</div>
<p>Previously you could only request specific headers by name. Now you can retrieve the complete set &ndash; useful for inspecting rate limits, caching directives, and other metadata from REST APIs.</p>
<h2 id="async-http-with-shiny">Async HTTP with Shiny
</h2>
<p>If your Shiny app calls a REST API, a slow or unresponsive endpoint will block the R process and freeze the app for <em>all</em> users, not just the one who triggered the request. <a href="https://nanonext.r-lib.org/reference/ncurl_aio.html" target="_blank" rel="noopener"><code>ncurl_aio()</code></a>
 avoids this &ndash; it performs the HTTP call on a background thread and returns a promise, so the R process stays free to serve other sessions. It works anywhere that accepts a promise, including Shiny&rsquo;s ExtendedTask:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shiny</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">bslib</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">nanonext</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="nf">page_fluid</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nf">p</span><span class="p">(</span><span class="s">&#34;The time is &#34;</span><span class="p">,</span> <span class="nf">textOutput</span><span class="p">(</span><span class="s">&#34;current_time&#34;</span><span class="p">,</span> <span class="n">inline</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">  <span class="nf">hr</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">  <span class="nf">input_task_button</span><span class="p">(</span><span class="s">&#34;btn&#34;</span><span class="p">,</span> <span class="s">&#34;Fetch data&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="nf">verbatimTextOutput</span><span class="p">(</span><span class="s">&#34;result&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">current_time</span> <span class="o">&lt;-</span> <span class="nf">renderText</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nf">invalidateLater</span><span class="p">(</span><span class="m">1000</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">format</span><span class="p">(</span><span class="nf">Sys.time</span><span class="p">(),</span> <span class="s">&#34;%H:%M:%S %p&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">task</span> <span class="o">&lt;-</span> <span class="n">ExtendedTask</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="kr">function</span><span class="p">()</span> <span class="nf">ncurl_aio</span><span class="p">(</span><span class="s">&#34;https://postman-echo.com/get&#34;</span><span class="p">,</span> <span class="n">response</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="o">|&gt;</span> <span class="nf">bind_task_button</span><span class="p">(</span><span class="s">&#34;btn&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">btn</span><span class="p">,</span> <span class="n">task</span><span class="o">$</span><span class="nf">invoke</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">result</span> <span class="o">&lt;-</span> <span class="nf">renderPrint</span><span class="p">(</span><span class="n">task</span><span class="o">$</span><span class="nf">result</span><span class="p">()</span><span class="o">$</span><span class="n">headers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="new-documentation">New documentation
</h2>
<p>The package documentation has been reorganized into focused, self-contained guides:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Guide</th>
          <th style="text-align: left">Topics</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><a href="https://nanonext.r-lib.org/articles/nanonext.html" target="_blank" rel="noopener">Quick Reference</a>
</td>
          <td style="text-align: left">At-a-glance API overview</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://nanonext.r-lib.org/articles/v01-messaging.html" target="_blank" rel="noopener">Messaging</a>
</td>
          <td style="text-align: left">Cross-language exchange, async I/O, synchronization</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://nanonext.r-lib.org/articles/v02-protocols.html" target="_blank" rel="noopener">Protocols</a>
</td>
          <td style="text-align: left">req/rep, pub/sub, surveyor/respondent</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://nanonext.r-lib.org/articles/v03-configuration.html" target="_blank" rel="noopener">Configuration</a>
</td>
          <td style="text-align: left">TLS, options, serialization</td>
      </tr>
      <tr>
          <td style="text-align: left"><a href="https://nanonext.r-lib.org/articles/v04-web.html" target="_blank" rel="noopener">Web Toolkit</a>
</td>
          <td style="text-align: left">HTTP client/server, WebSocket, streaming</td>
      </tr>
  </tbody>
</table>
<p>Whether you need a quick API cheatsheet or a deep dive into WebSocket chat servers, the new vignettes are designed to get you up and running fast.</p>
<h2 id="bug-fixes-and-improvements">Bug fixes and improvements
</h2>
<p>A new <a href="https://nanonext.r-lib.org/reference/race_aio.html" target="_blank" rel="noopener"><code>race_aio()</code></a>
 function returns the index of the first resolved async operation in a list &ndash; useful when waiting on multiple concurrent operations and you want to act on whichever completes first.</p>
<p>This release also fixes two critical issues &ndash; one affecting TLS operations in fresh sessions with newer system versions of Mbed TLS, another when custom serialization hooks threw errors. Error handling is now more graceful throughout, with closed streams returning error values instead of throwing. Under the hood, serialization, streaming, and async sends are all faster, and the bundled Mbed TLS is updated to 3.6.5 LTS. Building from source no longer requires <code>xz</code>.</p>
<h2 id="looking-ahead">Looking ahead
</h2>
<p>nanonext gives R a new building block for web infrastructure &ndash; one that complements httpuv. We see it as part of a broader investment in making R a first-class platform for real-time, connected applications. If you want to dig deeper, visit the <a href="https://nanonext.r-lib.org" target="_blank" rel="noopener">package website</a>
 or explore the source on <a href="https://github.com/r-lib/nanonext" target="_blank" rel="noopener">GitHub</a>
.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thank you to everyone who contributed to this release:</p>
<p><a href="https://github.com/jeroenjanssens" target="_blank" rel="noopener">@jeroenjanssens</a>
 and <a href="https://github.com/shikokuchuo" target="_blank" rel="noopener">@shikokuchuo</a>
.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/nanonext-1-8-0/thumbnail-wd.jpg" length="278046" type="image/jpeg" />
    </item>
    <item>
      <title>dplyr 1.2.0</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/dplyr-1-2-0/</link>
      <pubDate>Wed, 04 Feb 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/dplyr-1-2-0/</guid>
      <dc:creator>Davis Vaughan</dc:creator><description><![CDATA[<p><a href="https://dplyr.tidyverse.org" target="_blank" rel="noopener">dplyr 1.2.0</a>
 is out now! This large release of dplyr comes with two sets of exciting features:</p>
<ul>
<li>
<p><a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter_out()</code></a>
, the missing complement to <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
, and accompanying <a href="https://dplyr.tidyverse.org/reference/when-any-all.html" target="_blank" rel="noopener"><code>when_any()</code></a>
 and <a href="https://dplyr.tidyverse.org/reference/when-any-all.html" target="_blank" rel="noopener"><code>when_all()</code></a>
 helpers.</p>
</li>
<li>
<p><a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>recode_values()</code></a>
, <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>replace_values()</code></a>
, and <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>replace_when()</code></a>
, three new functions that join <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 to create a cohesive family of powerful tools for recoding and replacing.</p>
</li>
</ul>
<p>Both of these sets of features are backed by successful <em>tidyups</em>, the tidyverse&rsquo;s community facing proposal process (<a href="https://github.com/tidyverse/tidyups/pull/30" target="_blank" rel="noopener">filtering</a>
, <a href="https://github.com/tidyverse/tidyups/pull/29" target="_blank" rel="noopener">recoding</a>
). We really enjoyed having the community weigh in on these features!</p>
<p>You can install dplyr 1.2.0 from CRAN with:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/utils/install.packages.html'>install.packages</a></span><span class='o'>(</span><span class='s'>"dplyr"</span><span class='o'>)</span></span></code></pre>
</div>
<p>You can see a full list of changes in the <a href="https://github.com/tidyverse/dplyr/releases/tag/v1.2.0" target="_blank" rel="noopener">release notes</a>
.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://dplyr.tidyverse.org'>dplyr</a></span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="expanding-the-filter-family">Expanding the <code>filter()</code> family
</h2>
<p><a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
 has been a core dplyr verb since the very beginning, but over the years we&rsquo;ve isolated a few key issues with it:</p>
<ul>
<li>
<p>The name <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
 is ambiguous, are you keeping or dropping rows? i.e., are you filtering <em>for</em> rows or filtering <em>out</em> rows?</p>
</li>
<li>
<p><a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
 is optimized for the case of <em>keeping</em> rows, but you are just as likely to try and use it for <em>dropping</em> rows. Using <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
 to drop rows quickly forces you to confront complex boolean logic and explicitly handle missing values, which is difficult to teach, error prone to write, and hard to understand when you come back to it in the future.</p>
</li>
<li>
<p><a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
 combines comma separated conditions with <code>&amp;</code> because this covers the majority of the cases. But if you&rsquo;d like to combine conditions with <code>|</code>, then you have to introduce parentheses around your conditions and combine them into one large condition separated by <code>|</code>, reducing readability.</p>
</li>
</ul>
<p>In the next few sections, we&rsquo;ll motivate these issues and discuss how some new features in dplyr can simplify things dramatically!</p>
<h3 id="filteringout">Filtering&hellip;out!
</h3>
<p>Take a look at this <code>patients</code> data. Our task with this data is:</p>
<blockquote>
<p><em>Filter out</em> rows where the patient is deceased <em>and</em> the year was before 2012.</p>
</blockquote>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>patients</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://tibble.tidyverse.org/reference/tibble.html'>tibble</a></span><span class='o'>(</span></span>
<span>  name <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"Anne"</span>, <span class='s'>"Mark"</span>, <span class='s'>"Sarah"</span>, <span class='s'>"Davis"</span>, <span class='s'>"Max"</span>, <span class='s'>"Derek"</span>, <span class='s'>"Tina"</span><span class='o'>)</span>,</span>
<span>  deceased <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>FALSE</span>, <span class='kc'>TRUE</span>, <span class='kc'>NA</span>, <span class='kc'>TRUE</span>, <span class='kc'>NA</span>, <span class='kc'>FALSE</span>, <span class='kc'>TRUE</span><span class='o'>)</span>,</span>
<span>  date <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='m'>2005</span>, <span class='m'>2010</span>, <span class='kc'>NA</span>, <span class='m'>2020</span>, <span class='m'>2010</span>, <span class='kc'>NA</span>, <span class='kc'>NA</span><span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>patients</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 7 × 3</span></span></span>
<span><span class='c'>#&gt;   name  deceased  date</span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span>    <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> Anne  FALSE     <span style='text-decoration: underline;'>2</span>005</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> Mark  TRUE      <span style='text-decoration: underline;'>2</span>010</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> Sarah <span style='color: #BB0000;'>NA</span>          <span style='color: #BB0000;'>NA</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span> Davis TRUE      <span style='text-decoration: underline;'>2</span>020</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span> Max   <span style='color: #BB0000;'>NA</span>        <span style='text-decoration: underline;'>2</span>010</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>6</span> Derek FALSE       <span style='color: #BB0000;'>NA</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>7</span> Tina  TRUE        <span style='color: #BB0000;'>NA</span></span></span>
<span></span></code></pre>
</div>
<p>With <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
, you&rsquo;d probably start by translating &ldquo;patient is deceased and the year was before 2012&rdquo; into <code>deceased &amp; date &lt; 2012</code>, and then inverting that with <code>!(&lt;expression&gt;)</code> to drop rows:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>patients</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/filter.html'>filter</a></span><span class='o'>(</span><span class='o'>!</span><span class='o'>(</span><span class='nv'>deceased</span> <span class='o'>&amp;</span> <span class='nv'>date</span> <span class='o'>&lt;</span> <span class='m'>2012</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 3 × 3</span></span></span>
<span><span class='c'>#&gt;   name  deceased  date</span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span>    <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> Anne  FALSE     <span style='text-decoration: underline;'>2</span>005</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> Davis TRUE      <span style='text-decoration: underline;'>2</span>020</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> Derek FALSE       <span style='color: #BB0000;'>NA</span></span></span>
<span></span></code></pre>
</div>
<p>That seems to have worked, let&rsquo;s use an <a href="https://dplyr.tidyverse.org/reference/filter-joins.html" target="_blank" rel="noopener"><code>anti_join()</code></a>
 to check with rows have been dropped from <code>patients</code>:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># These rows were dropped</span></span>
<span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/filter-joins.html'>anti_join</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>patients</span>,</span>
<span>  <span class='nv'>patients</span> <span class='o'>|&gt;</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/filter.html'>filter</a></span><span class='o'>(</span><span class='o'>!</span><span class='o'>(</span><span class='nv'>deceased</span> <span class='o'>&amp;</span> <span class='nv'>date</span> <span class='o'>&lt;</span> <span class='m'>2012</span><span class='o'>)</span><span class='o'>)</span>,</span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/join_by.html'>join_by</a></span><span class='o'>(</span><span class='nv'>name</span>, <span class='nv'>deceased</span>, <span class='nv'>date</span><span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 4 × 3</span></span></span>
<span><span class='c'>#&gt;   name  deceased  date</span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span>    <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> Mark  TRUE      <span style='text-decoration: underline;'>2</span>010</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> Sarah <span style='color: #BB0000;'>NA</span>          <span style='color: #BB0000;'>NA</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> Max   <span style='color: #BB0000;'>NA</span>        <span style='text-decoration: underline;'>2</span>010</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span> Tina  TRUE        <span style='color: #BB0000;'>NA</span></span></span>
<span></span></code></pre>
</div>
<p>This is subtly wrong! We only wanted to drop rows where we <em>know</em> that the patient was deceased before 2012. If a missing value is present, we <em>don&rsquo;t</em> want to drop that row because we aren&rsquo;t sure about the condition. In this case, we were hoping to only drop <code>Mark</code>! It seems like <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
 has unexpectedly <em>dropped more rows than we expected</em>.</p>
<p>Here&rsquo;s what a technically correct <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
 call might look like:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>patients</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/filter.html'>filter</a></span><span class='o'>(</span></span>
<span>    <span class='o'>!</span><span class='o'>(</span><span class='o'>(</span><span class='nv'>deceased</span> <span class='o'>&amp;</span> <span class='o'>!</span><span class='nf'><a href='https://rdrr.io/r/base/NA.html'>is.na</a></span><span class='o'>(</span><span class='nv'>deceased</span><span class='o'>)</span><span class='o'>)</span> <span class='o'>&amp;</span></span>
<span>      <span class='o'>(</span><span class='nv'>date</span> <span class='o'>&lt;</span> <span class='m'>2012</span> <span class='o'>&amp;</span> <span class='o'>!</span><span class='nf'><a href='https://rdrr.io/r/base/NA.html'>is.na</a></span><span class='o'>(</span><span class='nv'>date</span><span class='o'>)</span><span class='o'>)</span><span class='o'>)</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 6 × 3</span></span></span>
<span><span class='c'>#&gt;   name  deceased  date</span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span>    <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> Anne  FALSE     <span style='text-decoration: underline;'>2</span>005</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> Sarah <span style='color: #BB0000;'>NA</span>          <span style='color: #BB0000;'>NA</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> Davis TRUE      <span style='text-decoration: underline;'>2</span>020</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span> Max   <span style='color: #BB0000;'>NA</span>        <span style='text-decoration: underline;'>2</span>010</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span> Derek FALSE       <span style='color: #BB0000;'>NA</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>6</span> Tina  TRUE        <span style='color: #BB0000;'>NA</span></span></span>
<span></span></code></pre>
</div>
<p>That&rsquo;s horrible! You&rsquo;ll likely look back on this in a year wondering what you were even trying to do here.</p>
<p>This phenomenon is rather confusing, but is due to the fact that <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
 is designed around the idea that you&rsquo;re going to tell it which rows to <em>keep</em>. With that design in mind, dropping <code>NA</code>s makes sense, i.e. if you don&rsquo;t <em>know</em> that you want to keep that row (because an <code>NA</code> is ambiguous), then you probably don&rsquo;t want to keep it.</p>
<p>This works well until you try to use <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
 as a way to <em>filter out</em> rows, at which point this behavior works against you. At this point, most people (reasonably!) reach for <code>&amp; !is.na()</code> and you end up with the mess from above.</p>
<p>We took a close look at many examples like this one, and eventually realized that the core issue is:</p>
<ul>
<li><a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
 is designed around supplying which rows to <em>keep</em></li>
<li>We are missing a verb designed around supplying which rows to <em>drop</em></li>
</ul>
<p><a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter_out()</code></a>
 fills that gap:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>patients</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/filter.html'>filter_out</a></span><span class='o'>(</span><span class='nv'>deceased</span>, <span class='nv'>date</span> <span class='o'>&lt;</span> <span class='m'>2012</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 6 × 3</span></span></span>
<span><span class='c'>#&gt;   name  deceased  date</span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;lgl&gt;</span>    <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> Anne  FALSE     <span style='text-decoration: underline;'>2</span>005</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> Sarah <span style='color: #BB0000;'>NA</span>          <span style='color: #BB0000;'>NA</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> Davis TRUE      <span style='text-decoration: underline;'>2</span>020</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span> Max   <span style='color: #BB0000;'>NA</span>        <span style='text-decoration: underline;'>2</span>010</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span> Derek FALSE       <span style='color: #BB0000;'>NA</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>6</span> Tina  TRUE        <span style='color: #BB0000;'>NA</span></span></span>
<span></span></code></pre>
</div>
<p>Just like with <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
, <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter_out()</code></a>
 treats <code>NA</code> values as <code>FALSE</code>. The difference is that <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter_out()</code></a>
 expects that you are going to tell it which rows to <em>drop</em> (rather than which rows to keep), so the default behavior of treating <code>NA</code> like <code>FALSE</code> works <em>with you</em> rather than <em>against you</em>. It&rsquo;s also much easier to understand when you look back on it a year from now!</p>
<p>In general, our advice is that if you find yourself using &ldquo;negative&rdquo; operators like <code>!=</code> or <code>!</code> or reaching for the <code>!is.na()</code> pattern to manually handle missing values, try reaching for <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter_out()</code></a>
 instead.</p>
<p>Personally, I&rsquo;ve always been pretty jealous of Stata here because they had both <a href="https://www.stata.com/manuals/ddrop.pdf" target="_blank" rel="noopener"><code>keep if</code> and <code>drop if</code></a>
, allowing them to write <code>drop if deceased &amp; date &lt; 2012</code>. In my first job, I translated a bunch of Stata code over to R and still remember being frustrated by <code>NA</code> handling every time I had to translate a <code>drop if</code> to a <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
. With <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter_out()</code></a>
, it feels like I can finally let go of a long term grudge I&rsquo;ve held over the past 6 years 🙂.</p>
<h3 id="combining-with-or-rather-than-and">Combining with <code>OR</code> rather than <code>AND</code>
</h3>
<p>So far, we&rsquo;ve talked a lot about <em>dropping</em> rows, but dplyr 1.2.0 also has a new feature to help with <em>keeping</em> rows using conditions combined with <code>|</code> - <a href="https://dplyr.tidyverse.org/reference/when-any-all.html" target="_blank" rel="noopener"><code>when_any()</code></a>
.</p>
<p>Our goal here is:</p>
<blockquote>
<p><em>Filter for</em> rows where &ldquo;US&rdquo; and &ldquo;CA&rdquo; have a score between 200-300, <em>or</em> rows where &ldquo;PR&rdquo; and &ldquo;RU&rdquo; have a score between 100-200.</p>
</blockquote>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>countries</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://tibble.tidyverse.org/reference/tibble.html'>tibble</a></span><span class='o'>(</span></span>
<span>  name <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"US"</span>, <span class='s'>"CA"</span>, <span class='s'>"PR"</span>, <span class='s'>"RU"</span>, <span class='s'>"US"</span>, <span class='kc'>NA</span>, <span class='s'>"CA"</span>, <span class='s'>"PR"</span><span class='o'>)</span>,</span>
<span>  score <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='m'>200</span>, <span class='m'>100</span>, <span class='m'>150</span>, <span class='kc'>NA</span>, <span class='m'>50</span>, <span class='m'>100</span>, <span class='m'>300</span>, <span class='m'>250</span><span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>countries</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 8 × 2</span></span></span>
<span><span class='c'>#&gt;   name  score</span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> US      200</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> CA      100</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> PR      150</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span> RU       <span style='color: #BB0000;'>NA</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span> US       50</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>6</span> <span style='color: #BB0000;'>NA</span>      100</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>7</span> CA      300</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>8</span> PR      250</span></span>
<span></span></code></pre>
</div>
<p>Here&rsquo;s a <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
 solution, note how we lose the ability to specify comma separated conditions, and in the process we&rsquo;ve introduced 3 operators, <code>&amp;</code>, <code>|</code>, and <code>()</code>, decreasing readability and increasing the mental gymnastics required to understand it:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>countries</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/filter.html'>filter</a></span><span class='o'>(</span></span>
<span>    <span class='o'>(</span><span class='nv'>name</span> <span class='o'><a href='https://rdrr.io/r/base/match.html'>%in%</a></span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"US"</span>, <span class='s'>"CA"</span><span class='o'>)</span> <span class='o'>&amp;</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/between.html'>between</a></span><span class='o'>(</span><span class='nv'>score</span>, <span class='m'>200</span>, <span class='m'>300</span><span class='o'>)</span><span class='o'>)</span> <span class='o'>|</span></span>
<span>      <span class='o'>(</span><span class='nv'>name</span> <span class='o'><a href='https://rdrr.io/r/base/match.html'>%in%</a></span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"PR"</span>, <span class='s'>"RU"</span><span class='o'>)</span> <span class='o'>&amp;</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/between.html'>between</a></span><span class='o'>(</span><span class='nv'>score</span>, <span class='m'>100</span>, <span class='m'>200</span><span class='o'>)</span><span class='o'>)</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 3 × 2</span></span></span>
<span><span class='c'>#&gt;   name  score</span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> US      200</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> PR      150</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> CA      300</span></span>
<span></span></code></pre>
</div>
<p>With <a href="https://dplyr.tidyverse.org/reference/when-any-all.html" target="_blank" rel="noopener"><code>when_any()</code></a>
, you specify comma separated conditions like you&rsquo;re used to, but they get combined with <code>|</code> rather than <code>&amp;</code>. This allows us to reduce the amount of operators introduced down to just <code>&amp;</code>, and it remains very readable:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>countries</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/filter.html'>filter</a></span><span class='o'>(</span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/when-any-all.html'>when_any</a></span><span class='o'>(</span></span>
<span>    <span class='nv'>name</span> <span class='o'><a href='https://rdrr.io/r/base/match.html'>%in%</a></span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"US"</span>, <span class='s'>"CA"</span><span class='o'>)</span> <span class='o'>&amp;</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/between.html'>between</a></span><span class='o'>(</span><span class='nv'>score</span>, <span class='m'>200</span>, <span class='m'>300</span><span class='o'>)</span>,</span>
<span>    <span class='nv'>name</span> <span class='o'><a href='https://rdrr.io/r/base/match.html'>%in%</a></span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"PR"</span>, <span class='s'>"RU"</span><span class='o'>)</span> <span class='o'>&amp;</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/between.html'>between</a></span><span class='o'>(</span><span class='nv'>score</span>, <span class='m'>100</span>, <span class='m'>200</span><span class='o'>)</span></span>
<span>  <span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 3 × 2</span></span></span>
<span><span class='c'>#&gt;   name  score</span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> US      200</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> PR      150</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> CA      300</span></span>
<span></span></code></pre>
</div>
<p><a href="https://dplyr.tidyverse.org/reference/when-any-all.html" target="_blank" rel="noopener"><code>when_any()</code></a>
 and its counterpart <a href="https://dplyr.tidyverse.org/reference/when-any-all.html" target="_blank" rel="noopener"><code>when_all()</code></a>
 aren&rsquo;t restricted to <a href="https://dplyr.tidyverse.org/reference/filter.html" target="_blank" rel="noopener"><code>filter()</code></a>
. They are normal vector functions that can be used anywhere. And if you&rsquo;re a package author, you might be interested in <a href="https://vctrs.r-lib.org/reference/parallel-operators.html" target="_blank" rel="noopener"><code>vctrs::vec_pany()</code></a>
 and <a href="https://vctrs.r-lib.org/reference/parallel-operators.html" target="_blank" rel="noopener"><code>vctrs::vec_pall()</code></a>
, the underlying low dependency functions that power the dplyr variants.</p>
<h2 id="reaching-recoding-nirvana">Reaching recoding nirvana
</h2>
<p>Over the years, we&rsquo;ve experimented with various ways of recoding columns and replacing values within them, including:</p>
<ul>
<li><a href="https://rdrr.io/pkg/plyr/man/mapvalues.html" target="_blank" rel="noopener"><code>plyr::mapvalues()</code></a>
</li>
<li><a href="https://rdrr.io/pkg/plyr/man/revalue.html" target="_blank" rel="noopener"><code>plyr::revalue()</code></a>
</li>
<li><a href="https://dplyr.tidyverse.org/reference/recode.html" target="_blank" rel="noopener"><code>dplyr::recode()</code></a>
</li>
<li><a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>dplyr::case_when()</code></a>
</li>
</ul>
<p>Despite all of our improvements, it&rsquo;s felt like there have always been holes in our solutions. Most recently, this came to the forefront in a post about <a href="https://www.linkedin.com/posts/libbyheeren_rstats-activity-7343291858275487744-XlPl?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAAy7IywB2qfaREGGoCca5XkthJ2hLjru6ts" target="_blank" rel="noopener">recoding using a lookup table</a>
, which is almost impossible to do with <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
, and had people resorting to confusing solutions using the superseded <a href="https://dplyr.tidyverse.org/reference/recode.html" target="_blank" rel="noopener"><code>dplyr::recode()</code></a>
 combined with <code>!!!</code> to splice in a lookup table.</p>
<p>After seeing this, we took a step back and were finally able to isolate the issues with our current solutions. The result of our <a href="https://github.com/tidyverse/tidyups/pull/29" target="_blank" rel="noopener">analysis</a>
 is three new functions that join <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 to form a powerful recoding and replacing family.</p>
<p>It&rsquo;ll be helpful to define exactly what we mean by recoding vs replacing:</p>
<ul>
<li>
<p><em>Recoding</em> a column creates an entirely new column using values from an existing column. The new column may have a different type from the original column.</p>
</li>
<li>
<p><em>Replacing</em> values within a column partially updates an existing column with new values. The result has the same type as the original column.</p>
</li>
</ul>
<p>The family of functions can be summarized by the following table:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th><strong>Recoding</strong></th>
          <th><strong>Replacing</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Match with conditions</strong></td>
          <td><a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
</td>
          <td><a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>replace_when()</code></a>
</td>
      </tr>
      <tr>
          <td><strong>Match with values</strong></td>
          <td><a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>recode_values()</code></a>
</td>
          <td><a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>replace_values()</code></a>
</td>
      </tr>
  </tbody>
</table>
<p>We&rsquo;ve written a <a href="https://dplyr.tidyverse.org/articles/recoding-replacing.html" target="_blank" rel="noopener">new vignette</a>
 that expands on all of these from first principles, and in the next few sections we&rsquo;ll look at some examples.</p>
<h3 id="recode_values"><code>recode_values()</code>
</h3>
<p>The goal of the post from above was to recode a numeric column of <a href="https://en.wikipedia.org/wiki/Likert_scale" target="_blank" rel="noopener">Likert scale</a>
 scores into their string counterparts.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>likert</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://tibble.tidyverse.org/reference/tibble.html'>tibble</a></span><span class='o'>(</span></span>
<span>  score <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='m'>1</span>, <span class='m'>2</span>, <span class='m'>3</span>, <span class='m'>4</span>, <span class='m'>5</span>, <span class='m'>2</span>, <span class='m'>3</span>, <span class='m'>1</span>, <span class='m'>4</span><span class='o'>)</span></span>
<span><span class='o'>)</span></span></code></pre>
</div>
<p>We could certainly try <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>likert</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/mutate.html'>mutate</a></span><span class='o'>(</span></span>
<span>    category <span class='o'>=</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>      <span class='nv'>score</span> <span class='o'>==</span> <span class='m'>1</span> <span class='o'>~</span> <span class='s'>"Strongly disagree"</span>,</span>
<span>      <span class='nv'>score</span> <span class='o'>==</span> <span class='m'>2</span> <span class='o'>~</span> <span class='s'>"Disagree"</span>,</span>
<span>      <span class='nv'>score</span> <span class='o'>==</span> <span class='m'>3</span> <span class='o'>~</span> <span class='s'>"Neutral"</span>,</span>
<span>      <span class='nv'>score</span> <span class='o'>==</span> <span class='m'>4</span> <span class='o'>~</span> <span class='s'>"Agree"</span>,</span>
<span>      <span class='nv'>score</span> <span class='o'>==</span> <span class='m'>5</span> <span class='o'>~</span> <span class='s'>"Strongly agree"</span></span>
<span>    <span class='o'>)</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 9 × 2</span></span></span>
<span><span class='c'>#&gt;   score category         </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>            </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span>     1 Strongly disagree</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span>     2 Disagree         </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span>     3 Neutral          </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span>     4 Agree            </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span>     5 Strongly agree   </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>6</span>     2 Disagree         </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>7</span>     3 Neutral          </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>8</span>     1 Strongly disagree</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>9</span>     4 Agree</span></span>
<span></span></code></pre>
</div>
<p>But <code>score ==</code> is repeated so many times! When you find yourself using <code>==</code> in this way, recognize that what you&rsquo;re really doing is matching on the <em>values</em> of a single column. In cases like these, you&rsquo;ll want to switch to <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>recode_values()</code></a>
. Rather than taking logical vectors, <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>recode_values()</code></a>
 takes <em>values</em> on the left-hand side to match against a single input that you&rsquo;ll provide as the first argument.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>likert</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/mutate.html'>mutate</a></span><span class='o'>(</span></span>
<span>    category <span class='o'>=</span> <span class='nv'>score</span> <span class='o'>|&gt;</span></span>
<span>      <span class='nf'><a href='https://dplyr.tidyverse.org/reference/recode-and-replace-values.html'>recode_values</a></span><span class='o'>(</span></span>
<span>        <span class='m'>1</span> <span class='o'>~</span> <span class='s'>"Strongly disagree"</span>,</span>
<span>        <span class='m'>2</span> <span class='o'>~</span> <span class='s'>"Disagree"</span>,</span>
<span>        <span class='m'>3</span> <span class='o'>~</span> <span class='s'>"Neutral"</span>,</span>
<span>        <span class='m'>4</span> <span class='o'>~</span> <span class='s'>"Agree"</span>,</span>
<span>        <span class='m'>5</span> <span class='o'>~</span> <span class='s'>"Strongly agree"</span></span>
<span>      <span class='o'>)</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 9 × 2</span></span></span>
<span><span class='c'>#&gt;   score category         </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>            </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span>     1 Strongly disagree</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span>     2 Disagree         </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span>     3 Neutral          </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span>     4 Agree            </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span>     5 Strongly agree   </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>6</span>     2 Disagree         </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>7</span>     3 Neutral          </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>8</span>     1 Strongly disagree</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>9</span>     4 Agree</span></span>
<span></span></code></pre>
</div>
<p>This removes all of the repetition, allowing you to focus on the mapping. And it should feel pretty familiar! This is the same formula interface of <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
.</p>
<p>If you squint, the mapping should look roughly like a lookup table between the numeric value and the Likert encoding. One of the novel features of <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>recode_values()</code></a>
 is that it has an alternate interface that allows you to make this lookup table more explicit. Using a <a href="https://tibble.tidyverse.org/reference/tribble.html" target="_blank" rel="noopener"><code>tribble()</code></a>
, we can extract out the lookup table into its own standalone data frame.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>lookup</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://tibble.tidyverse.org/reference/tribble.html'>tribble</a></span><span class='o'>(</span></span>
<span>  <span class='o'>~</span><span class='nv'>from</span> , <span class='o'>~</span><span class='nv'>to</span>                 ,</span>
<span>      <span class='m'>1</span> , <span class='s'>"Strongly disagree"</span> ,</span>
<span>      <span class='m'>2</span> , <span class='s'>"Disagree"</span>          ,</span>
<span>      <span class='m'>3</span> , <span class='s'>"Neutral"</span>           ,</span>
<span>      <span class='m'>4</span> , <span class='s'>"Agree"</span>             ,</span>
<span>      <span class='m'>5</span> , <span class='s'>"Strongly agree"</span>    ,</span>
<span><span class='o'>)</span></span></code></pre>
</div>
<p>We can then utilize the alternative <code>from</code> and <code>to</code> arguments of <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>recode_values()</code></a>
 rather than supplying formulas to specify how the values should be recoded:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>likert</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/mutate.html'>mutate</a></span><span class='o'>(</span>category <span class='o'>=</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/recode-and-replace-values.html'>recode_values</a></span><span class='o'>(</span><span class='nv'>score</span>, from <span class='o'>=</span> <span class='nv'>lookup</span><span class='o'>$</span><span class='nv'>from</span>, to <span class='o'>=</span> <span class='nv'>lookup</span><span class='o'>$</span><span class='nv'>to</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 9 × 2</span></span></span>
<span><span class='c'>#&gt;   score category         </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span> <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>            </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span>     1 Strongly disagree</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span>     2 Disagree         </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span>     3 Neutral          </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span>     4 Agree            </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span>     5 Strongly agree   </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>6</span>     2 Disagree         </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>7</span>     3 Neutral          </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>8</span>     1 Strongly disagree</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>9</span>     4 Agree</span></span>
<span></span></code></pre>
</div>
<p>Lifting the lookup table out to the top of the file is particularly nice when you have a long pipe chain. The details of the mapping get some room to breathe, and in the pipe chain you can focus on the actual data manipulations.</p>
<p>It&rsquo;s also very common for your <code>lookup</code> table to exist in a CSV file that you have to read in separately. In that case, you can replace the <a href="https://tibble.tidyverse.org/reference/tribble.html" target="_blank" rel="noopener"><code>tribble()</code></a>
 call with:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>lookup</span> <span class='o'>&lt;-</span> <span class='nf'>readr</span><span class='nf'>::</span><span class='nf'><a href='https://readr.tidyverse.org/reference/read_delim.html'>read_csv</a></span><span class='o'>(</span><span class='s'>"lookup.csv"</span><span class='o'>)</span></span></code></pre>
</div>
<p>Then everything else works the same.</p>
<h3 id="unmatched-cases">Unmatched cases
</h3>
<p>If you are confident that you&rsquo;ve captured every case during the recoding process, you can now supply <code>unmatched = &quot;error&quot;</code> as an alternative to <code>default</code>. <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>recode_values()</code></a>
 will error if that assertion doesn&rsquo;t hold. This is great for defensive programming!</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># Notice the `0` that we don't have a mapping for!</span></span>
<span><span class='nv'>likert</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://tibble.tidyverse.org/reference/tibble.html'>tibble</a></span><span class='o'>(</span></span>
<span>  score <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='m'>0</span>, <span class='m'>1</span>, <span class='m'>2</span>, <span class='m'>2</span>, <span class='m'>4</span>, <span class='m'>5</span>, <span class='m'>2</span>, <span class='m'>3</span>, <span class='m'>1</span>, <span class='m'>4</span><span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>likert</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/mutate.html'>mutate</a></span><span class='o'>(</span></span>
<span>    score <span class='o'>=</span> <span class='nv'>score</span> <span class='o'>|&gt;</span></span>
<span>      <span class='nf'><a href='https://dplyr.tidyverse.org/reference/recode-and-replace-values.html'>recode_values</a></span><span class='o'>(</span></span>
<span>        from <span class='o'>=</span> <span class='nv'>lookup</span><span class='o'>$</span><span class='nv'>from</span>,</span>
<span>        to <span class='o'>=</span> <span class='nv'>lookup</span><span class='o'>$</span><span class='nv'>to</span>,</span>
<span>        unmatched <span class='o'>=</span> <span class='s'>"error"</span></span>
<span>      <span class='o'>)</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00; font-weight: bold;'>Error</span><span style='font-weight: bold;'> in `mutate()`:</span></span></span>
<span><span class='c'>#&gt; <span style='color: #00BBBB;'>ℹ</span> In argument: `score = recode_values(score, from = lookup$from, to = lookup$to, unmatched = "error")`.</span></span>
<span><span class='c'>#&gt; <span style='font-weight: bold;'>Caused by error in `recode_values()`:</span></span></span>
<span><span class='c'>#&gt; <span style='color: #BBBB00;'>!</span> Each location must be matched.</span></span>
<span><span class='c'>#&gt; <span style='color: #BB0000;'>✖</span> Location 1 is unmatched.</span></span>
<span></span></code></pre>
</div>
<p>Note that missing values must be explicitly handled when setting <code>unmatched = &quot;error&quot;</code>, even if that&rsquo;s just setting <code>NA ~ NA</code>, otherwise they will trigger the unmatched error. This forces you to explicitly opt in to expecting missing values.</p>
<p>Similar to <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>recode_values()</code></a>
, <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 has also gained the <code>.unmatched</code> argument.</p>
<h3 id="replace_values"><code>replace_values()</code>
</h3>
<p>Out of all of the new things introduced in dplyr 1.2.0, I think I&rsquo;m most excited about <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>replace_values()</code></a>
.</p>
<p>While <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>recode_values()</code></a>
 is great for creating an entirely new column (possibly with a new type), if you just need to replace a few rows of an existing column, then <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>replace_values()</code></a>
 is the best tool for the job!</p>
<p>Imagine we&rsquo;d like to collapse some, but not all, of these school names into common buckets:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>schools</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://tibble.tidyverse.org/reference/tibble.html'>tibble</a></span><span class='o'>(</span></span>
<span>  name <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span></span>
<span>    <span class='s'>"UNC"</span>,</span>
<span>    <span class='s'>"Chapel Hill"</span>,</span>
<span>    <span class='kc'>NA</span>,</span>
<span>    <span class='s'>"Duke"</span>,</span>
<span>    <span class='s'>"Duke University"</span>,</span>
<span>    <span class='s'>"UNC"</span>,</span>
<span>    <span class='s'>"NC State"</span>,</span>
<span>    <span class='s'>"ECU"</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='o'>)</span></span></code></pre>
</div>
<p>We could use <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 or <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>recode_values()</code></a>
:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>schools</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/mutate.html'>mutate</a></span><span class='o'>(</span></span>
<span>    name <span class='o'>=</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span></span>
<span>      <span class='nv'>name</span> <span class='o'><a href='https://rdrr.io/r/base/match.html'>%in%</a></span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"UNC"</span>, <span class='s'>"Chapel Hill"</span><span class='o'>)</span> <span class='o'>~</span> <span class='s'>"UNC Chapel Hill"</span>,</span>
<span>      <span class='nv'>name</span> <span class='o'><a href='https://rdrr.io/r/base/match.html'>%in%</a></span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"Duke"</span>, <span class='s'>"Duke University"</span><span class='o'>)</span> <span class='o'>~</span> <span class='s'>"Duke"</span>,</span>
<span>      .default <span class='o'>=</span> <span class='nv'>name</span></span>
<span>    <span class='o'>)</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 8 × 1</span></span></span>
<span><span class='c'>#&gt;   name           </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>          </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> <span style='color: #BB0000;'>NA</span>             </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span> Duke           </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span> Duke           </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>6</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>7</span> NC State       </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>8</span> ECU</span></span>
<span></span><span></span>
<span><span class='nv'>schools</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/mutate.html'>mutate</a></span><span class='o'>(</span></span>
<span>    name <span class='o'>=</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/recode-and-replace-values.html'>recode_values</a></span><span class='o'>(</span></span>
<span>      <span class='nv'>name</span>,</span>
<span>      <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"UNC"</span>, <span class='s'>"Chapel Hill"</span><span class='o'>)</span> <span class='o'>~</span> <span class='s'>"UNC Chapel Hill"</span>,</span>
<span>      <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"Duke"</span>, <span class='s'>"Duke University"</span><span class='o'>)</span> <span class='o'>~</span> <span class='s'>"Duke"</span>,</span>
<span>      default <span class='o'>=</span> <span class='nv'>name</span></span>
<span>    <span class='o'>)</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 8 × 1</span></span></span>
<span><span class='c'>#&gt;   name           </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>          </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> <span style='color: #BB0000;'>NA</span>             </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span> Duke           </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span> Duke           </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>6</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>7</span> NC State       </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>8</span> ECU</span></span>
<span></span></code></pre>
</div>
<p>But this &ldquo;partial update&rdquo; operation is so common that it really deserves its own name that doesn&rsquo;t require you to specify <code>default</code> and is type stable on the input. For that, we have <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>replace_values()</code></a>
:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>schools</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/mutate.html'>mutate</a></span><span class='o'>(</span></span>
<span>    name <span class='o'>=</span> <span class='nv'>name</span> <span class='o'>|&gt;</span></span>
<span>      <span class='nf'><a href='https://dplyr.tidyverse.org/reference/recode-and-replace-values.html'>replace_values</a></span><span class='o'>(</span></span>
<span>        <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"UNC"</span>, <span class='s'>"Chapel Hill"</span><span class='o'>)</span> <span class='o'>~</span> <span class='s'>"UNC Chapel Hill"</span>,</span>
<span>        <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"Duke"</span>, <span class='s'>"Duke University"</span><span class='o'>)</span> <span class='o'>~</span> <span class='s'>"Duke"</span></span>
<span>      <span class='o'>)</span></span>
<span>  <span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 8 × 1</span></span></span>
<span><span class='c'>#&gt;   name           </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>          </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> <span style='color: #BB0000;'>NA</span>             </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span> Duke           </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span> Duke           </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>6</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>7</span> NC State       </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>8</span> ECU</span></span>
<span></span></code></pre>
</div>
<p>Notice how pipe friendly <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>replace_values()</code></a>
 is! The first input is your &ldquo;primary&rdquo; input, and you can expect the output to have the same type and size as that input.</p>
<p>Like <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>recode_values()</code></a>
, <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>replace_values()</code></a>
 has an alternative <code>from</code> and <code>to</code> API that works well with lookup tables and allows you to move your mapping out of the pipe chain:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>lookup</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://tibble.tidyverse.org/reference/tribble.html'>tribble</a></span><span class='o'>(</span></span>
<span>  <span class='o'>~</span><span class='nv'>from</span>             , <span class='o'>~</span><span class='nv'>to</span>               ,</span>
<span>  <span class='s'>"UNC"</span>             , <span class='s'>"UNC Chapel Hill"</span> ,</span>
<span>  <span class='s'>"Chapel Hill"</span>     , <span class='s'>"UNC Chapel Hill"</span> ,</span>
<span>  <span class='s'>"Duke"</span>            , <span class='s'>"Duke"</span>            ,</span>
<span>  <span class='s'>"Duke University"</span> , <span class='s'>"Duke"</span>            ,</span>
<span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>schools</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/mutate.html'>mutate</a></span><span class='o'>(</span>name <span class='o'>=</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/recode-and-replace-values.html'>replace_values</a></span><span class='o'>(</span><span class='nv'>name</span>, from <span class='o'>=</span> <span class='nv'>lookup</span><span class='o'>$</span><span class='nv'>from</span>, to <span class='o'>=</span> <span class='nv'>lookup</span><span class='o'>$</span><span class='nv'>to</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 8 × 1</span></span></span>
<span><span class='c'>#&gt;   name           </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>          </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> <span style='color: #BB0000;'>NA</span>             </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span> Duke           </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span> Duke           </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>6</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>7</span> NC State       </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>8</span> ECU</span></span>
<span></span></code></pre>
</div>
<p>An extremely neat feature of the <code>from</code> and <code>to</code> API is that they also take <em>lists</em> of vectors that describe the mapping, which has been designed to work elegantly with the fact that <a href="https://tibble.tidyverse.org/reference/tribble.html" target="_blank" rel="noopener"><code>tribble()</code></a>
 can create list columns, allowing you to further collapse this lookup table:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># Condensed lookup table with a `many:1` mapping per row</span></span>
<span><span class='nv'>lookup</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://tibble.tidyverse.org/reference/tribble.html'>tribble</a></span><span class='o'>(</span></span>
<span>  <span class='o'>~</span><span class='nv'>from</span>                        , <span class='o'>~</span><span class='nv'>to</span>               ,</span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"UNC"</span>, <span class='s'>"Chapel Hill"</span><span class='o'>)</span>      , <span class='s'>"UNC Chapel Hill"</span> ,</span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"Duke"</span>, <span class='s'>"Duke University"</span><span class='o'>)</span> , <span class='s'>"Duke"</span>            ,</span>
<span><span class='o'>)</span></span>
<span></span>
<span><span class='c'># Note that `from` is a list column</span></span>
<span><span class='nv'>lookup</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 2 × 2</span></span></span>
<span><span class='c'>#&gt;   from      to             </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;list&gt;</span>    <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>          </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> <span style='color: #555555;'>&lt;chr [2]&gt;</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> <span style='color: #555555;'>&lt;chr [2]&gt;</span> Duke</span></span>
<span></span><span></span>
<span><span class='nv'>lookup</span><span class='o'>$</span><span class='nv'>from</span></span>
<span><span class='c'>#&gt; [[1]]</span></span>
<span><span class='c'>#&gt; [1] "UNC"         "Chapel Hill"</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; [[2]]</span></span>
<span><span class='c'>#&gt; [1] "Duke"            "Duke University"</span></span>
<span></span><span></span>
<span><span class='c'># Works the same as before</span></span>
<span><span class='nv'>schools</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'><a href='https://dplyr.tidyverse.org/reference/mutate.html'>mutate</a></span><span class='o'>(</span>name <span class='o'>=</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/recode-and-replace-values.html'>replace_values</a></span><span class='o'>(</span><span class='nv'>name</span>, from <span class='o'>=</span> <span class='nv'>lookup</span><span class='o'>$</span><span class='nv'>from</span>, to <span class='o'>=</span> <span class='nv'>lookup</span><span class='o'>$</span><span class='nv'>to</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 8 × 1</span></span></span>
<span><span class='c'>#&gt;   name           </span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>          </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> <span style='color: #BB0000;'>NA</span>             </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>4</span> Duke           </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>5</span> Duke           </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>6</span> UNC Chapel Hill</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>7</span> NC State       </span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>8</span> ECU</span></span>
<span></span></code></pre>
</div>
<p>The formula interface of <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>replace_values()</code></a>
 is a bit of a Swiss Army knife for all manner of scenarios where you might have previously reached for <a href="https://dplyr.tidyverse.org/reference/coalesce.html" target="_blank" rel="noopener"><code>dplyr::coalesce()</code></a>
, <a href="https://dplyr.tidyverse.org/reference/na_if.html" target="_blank" rel="noopener"><code>dplyr::na_if()</code></a>
, or <a href="https://tidyr.tidyverse.org/reference/replace_na.html" target="_blank" rel="noopener"><code>tidyr::replace_na()</code></a>
:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>state</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"NC"</span>, <span class='s'>"NY"</span>, <span class='s'>"CA"</span>, <span class='kc'>NA</span>, <span class='s'>"NY"</span>, <span class='s'>"Unknown"</span>, <span class='kc'>NA</span><span class='o'>)</span></span>
<span></span>
<span><span class='c'># Replace missing values with a constant</span></span>
<span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/recode-and-replace-values.html'>replace_values</a></span><span class='o'>(</span><span class='nv'>state</span>, <span class='kc'>NA</span> <span class='o'>~</span> <span class='s'>"Unknown"</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] "NC"      "NY"      "CA"      "Unknown" "NY"      "Unknown" "Unknown"</span></span>
<span></span><span></span>
<span><span class='c'># Replace missing values with the corresponding value from another column</span></span>
<span><span class='nv'>region</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"South"</span>, <span class='s'>"North"</span>, <span class='s'>"West"</span>, <span class='s'>"East"</span>, <span class='s'>"North"</span>, <span class='s'>"Unknown"</span>, <span class='s'>"West"</span><span class='o'>)</span></span>
<span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/recode-and-replace-values.html'>replace_values</a></span><span class='o'>(</span><span class='nv'>state</span>, <span class='kc'>NA</span> <span class='o'>~</span> <span class='nv'>region</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] "NC"      "NY"      "CA"      "East"    "NY"      "Unknown" "West"</span></span>
<span></span><span></span>
<span><span class='c'># Replace problematic values with a missing value</span></span>
<span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/recode-and-replace-values.html'>replace_values</a></span><span class='o'>(</span><span class='nv'>state</span>, <span class='s'>"Unknown"</span> <span class='o'>~</span> <span class='kc'>NA</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] "NC" "NY" "CA" NA   "NY" NA   NA</span></span>
<span></span><span></span>
<span><span class='c'># Standardize multiple issues at once</span></span>
<span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/recode-and-replace-values.html'>replace_values</a></span><span class='o'>(</span><span class='nv'>state</span>, <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='kc'>NA</span>, <span class='s'>"Unknown"</span><span class='o'>)</span> <span class='o'>~</span> <span class='s'>"&lt;missing&gt;"</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] "NC"        "NY"        "CA"        "&lt;missing&gt;" "NY"        "&lt;missing&gt;"</span></span>
<span><span class='c'>#&gt; [7] "&lt;missing&gt;"</span></span>
<span></span></code></pre>
</div>
<p>We also think it better expresses intent than <a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
 or <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 when performing a partial update:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># - Type stable on `x`</span></span>
<span><span class='c'># - Intent of "partially updating" `state` is clear</span></span>
<span><span class='c'># - Pipe friendly</span></span>
<span><span class='nv'>state</span> <span class='o'>|&gt;</span> <span class='nf'><a href='https://dplyr.tidyverse.org/reference/recode-and-replace-values.html'>replace_values</a></span><span class='o'>(</span><span class='kc'>NA</span> <span class='o'>~</span> <span class='s'>"Unknown"</span><span class='o'>)</span></span>
<span></span>
<span><span class='c'># Historically this has been "the way" to do a partial update,</span></span>
<span><span class='c'># but it's odd that the "primary" input is at the end!</span></span>
<span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/if_else.html'>if_else</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/NA.html'>is.na</a></span><span class='o'>(</span><span class='nv'>state</span><span class='o'>)</span>, <span class='s'>"Unknown"</span>, <span class='nv'>state</span><span class='o'>)</span></span>
<span><span class='nf'><a href='https://dplyr.tidyverse.org/reference/case-and-replace-when.html'>case_when</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/NA.html'>is.na</a></span><span class='o'>(</span><span class='nv'>state</span><span class='o'>)</span> <span class='o'>~</span> <span class='s'>"Unknown"</span>, .default <span class='o'>=</span> <span class='nv'>state</span><span class='o'>)</span></span></code></pre>
</div>
<p>If you&rsquo;re a package author, you&rsquo;ll probably also be interested in <a href="https://vctrs.r-lib.org/reference/vec-recode-and-replace.html" target="_blank" rel="noopener"><code>vctrs::vec_recode_values()</code></a>
 and <a href="https://vctrs.r-lib.org/reference/vec-recode-and-replace.html" target="_blank" rel="noopener"><code>vctrs::vec_replace_values()</code></a>
, which are low dependency functions that power the dplyr variants.</p>
<h3 id="what-about-case_match">What about <code>case_match()</code>?
</h3>
<p>We&rsquo;ve soft-deprecated <a href="https://dplyr.tidyverse.org/reference/case_match.html" target="_blank" rel="noopener"><code>case_match()</code></a>
 in favor of <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>recode_values()</code></a>
, which is a drop in replacement.</p>
<p><a href="https://dplyr.tidyverse.org/reference/case_match.html" target="_blank" rel="noopener"><code>case_match()</code></a>
 was an incremental step towards this recoding family, but:</p>
<ul>
<li>It has a pretty confusing name compared with <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>recode_values()</code></a>
.</li>
<li>It lacked a way to work with lookup tables, like <code>from</code> and <code>to</code>.</li>
<li>It lacks a replacement variant, like <a href="https://dplyr.tidyverse.org/reference/recode-and-replace-values.html" target="_blank" rel="noopener"><code>replace_values()</code></a>
.</li>
</ul>
<p>Rather than keeping <a href="https://dplyr.tidyverse.org/reference/case_match.html" target="_blank" rel="noopener"><code>case_match()</code></a>
 around indefinitely, we&rsquo;ve decided to initiate the process of its removal since it was only introduced in dplyr 1.1.0.</p>
<h2 id="deprecations">Deprecations
</h2>
<p>dplyr 1.2.0 advances the lifecycle stage of many deprecated functions. These deprecations have been in the works for many years now, due to our slow and very deliberate deprecation process via the <a href="https://lifecycle.r-lib.org/" target="_blank" rel="noopener">lifecycle package</a>
. We&rsquo;ll cover the highlights, and you can find the full list <a href="https://github.com/tidyverse/dplyr/releases/tag/v1.2.0" target="_blank" rel="noopener">here</a>
.</p>
<p>For any packages that we broke via these deprecations, we provided a pull request (or at least an issue, for complex cases) and some advance warning. We semi-automated some of this process using Claude Code, which you can read about <a href="https://blog.davisvaughan.com/posts/2026-01-09-claude-200-pull-requests/" target="_blank" rel="noopener">here</a>
.</p>
<ul>
<li>
<p>All underscored verbs have moved from deprecated to defunct, such as <a href="https://dplyr.tidyverse.org/reference/defunct-lazyeval.html" target="_blank" rel="noopener"><code>mutate_()</code></a>
 and <a href="https://dplyr.tidyverse.org/reference/defunct-lazyeval.html" target="_blank" rel="noopener"><code>arrange_()</code></a>
. These have been deprecated since dplyr 0.7.0 back in 2017 (yes, 2017!!). Use the non-underscored versions, see <a href="https://dplyr.tidyverse.org/articles/programming.html" target="_blank" rel="noopener"><code>vignette(&quot;programming&quot;)</code></a>
 for details.</p>
</li>
<li>
<p><a href="https://dplyr.tidyverse.org/reference/defunct-each.html" target="_blank" rel="noopener"><code>mutate_each()</code></a>
 and <a href="https://dplyr.tidyverse.org/reference/defunct-each.html" target="_blank" rel="noopener"><code>summarise_each()</code></a>
 have moved from deprecated to defunct. These were also deprecated in dplyr 0.7.0. Use <a href="https://dplyr.tidyverse.org/reference/mutate.html" target="_blank" rel="noopener"><code>mutate()</code></a>
 and <a href="https://dplyr.tidyverse.org/reference/summarise.html" target="_blank" rel="noopener"><code>summarise()</code></a>
 with <a href="https://dplyr.tidyverse.org/reference/across.html" target="_blank" rel="noopener"><code>across()</code></a>
 instead.</p>
</li>
<li>
<p>Returning more or less than 1 row per group in <a href="https://dplyr.tidyverse.org/reference/summarise.html" target="_blank" rel="noopener"><code>summarise()</code></a>
 has moved from deprecated to defunct. This was deprecated in dplyr 1.1.0 in 2023 after we realized that this was an unsafe feature for <a href="https://dplyr.tidyverse.org/reference/summarise.html" target="_blank" rel="noopener"><code>summarise()</code></a>
, which you should always expect to return exactly 1 row per group. <a href="https://dplyr.tidyverse.org/reference/reframe.html" target="_blank" rel="noopener"><code>reframe()</code></a>
 is a drop in replacement when you need this.</p>
</li>
<li>
<p>In <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
, supplying all size 1 LHS inputs along with a size &gt;1 RHS input is now soft-deprecated. This is an improper usage of <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 that should instead be a series of if statements, like:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># Scalars!</span>
</span></span><span class="line"><span class="cl"><span class="n">code</span> <span class="o">&lt;-</span> <span class="m">1L</span>
</span></span><span class="line"><span class="cl"><span class="n">flavor</span> <span class="o">&lt;-</span> <span class="s">&#34;vanilla&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Improper usage:</span>
</span></span><span class="line"><span class="cl"><span class="nf">case_when</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">code</span> <span class="o">==</span> <span class="m">1L</span> <span class="o">&amp;&amp;</span> <span class="n">flavor</span> <span class="o">==</span> <span class="s">&#34;chocolate&#34;</span> <span class="o">~</span> <span class="n">x</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">code</span> <span class="o">==</span> <span class="m">1L</span> <span class="o">&amp;&amp;</span> <span class="n">flavor</span> <span class="o">==</span> <span class="s">&#34;vanilla&#34;</span> <span class="o">~</span> <span class="n">y</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">code</span> <span class="o">==</span> <span class="m">2L</span> <span class="o">&amp;&amp;</span> <span class="n">flavor</span> <span class="o">==</span> <span class="s">&#34;vanilla&#34;</span> <span class="o">~</span> <span class="n">z</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">.default</span> <span class="o">=</span> <span class="n">default</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Recommended:</span>
</span></span><span class="line"><span class="cl"><span class="kr">if</span> <span class="p">(</span><span class="n">code</span> <span class="o">==</span> <span class="m">1L</span> <span class="o">&amp;&amp;</span> <span class="n">flavor</span> <span class="o">==</span> <span class="s">&#34;chocolate&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">x</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">code</span> <span class="o">==</span> <span class="m">1L</span> <span class="o">&amp;&amp;</span> <span class="n">flavor</span> <span class="o">==</span> <span class="s">&#34;vanilla&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">y</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">code</span> <span class="o">==</span> <span class="m">2L</span> <span class="o">&amp;&amp;</span> <span class="n">flavor</span> <span class="o">==</span> <span class="s">&#34;vanilla&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">z</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kr">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">default</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The recycling behavior that allows this style of <a href="https://dplyr.tidyverse.org/reference/case-and-replace-when.html" target="_blank" rel="noopener"><code>case_when()</code></a>
 to work is unsafe, and can result in silent bugs that we&rsquo;d like to guard against with an error in the future. See <a href="https://github.com/tidyverse/dplyr/issues/7082" target="_blank" rel="noopener">this issue</a>
 for context.</p>
</li>
<li>
<p>The <code>dplyr.legacy_locale</code> global option is soft-deprecated. If you used this to affect the ordering of <a href="https://dplyr.tidyverse.org/reference/arrange.html" target="_blank" rel="noopener"><code>arrange()</code></a>
, use <code>arrange(.locale =)</code> instead. If you used this to affect the ordering of <code>group_by() |&gt; summarise()</code>, follow up with an additional call to <code>arrange(.locale =)</code> instead.</p>
</li>
<li>
<p><a href="https://dplyr.tidyverse.org/reference/if_else.html" target="_blank" rel="noopener"><code>if_else()</code></a>
 no longer allows <code>condition</code> to be a logical array. It must be a logical vector with no <code>dim</code> attribute.</p>
</li>
<li>
<p>We&rsquo;ve removed a number of previously defunct functions, shrinking the footprint of dplyr&rsquo;s API:</p>
<ul>
<li><code>id()</code></li>
<li><code>failwith()</code></li>
<li><code>select_vars()</code> and <code>select_vars_()</code></li>
<li><code>rename_vars()</code> and <code>rename_vars_()</code></li>
<li><code>select_var()</code></li>
<li><code>current_vars()</code></li>
<li><code>bench_tbls()</code>, <code>compare_tbls()</code>, <code>compare_tbls2()</code>, <code>eval_tbls()</code>, and <code>eval_tbls2()</code></li>
<li><code>location()</code> and <code>changes()</code></li>
</ul>
</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>We&rsquo;d like to thank all 177 contributors who help in someway, whether it was filing issues or contributing code and documentation: <a href="https://github.com/abalter" target="_blank" rel="noopener">@abalter</a>
, <a href="https://github.com/abichat" target="_blank" rel="noopener">@abichat</a>
, <a href="https://github.com/adupaix" target="_blank" rel="noopener">@adupaix</a>
, <a href="https://github.com/AlexBainton" target="_blank" rel="noopener">@AlexBainton</a>
, <a href="https://github.com/alexmcsw" target="_blank" rel="noopener">@alexmcsw</a>
, <a href="https://github.com/AltfunsMA" target="_blank" rel="noopener">@AltfunsMA</a>
, <a href="https://github.com/AmeliaMN" target="_blank" rel="noopener">@AmeliaMN</a>
, <a href="https://github.com/antdurrant" target="_blank" rel="noopener">@antdurrant</a>
, <a href="https://github.com/AnthonyEbert" target="_blank" rel="noopener">@AnthonyEbert</a>
, <a href="https://github.com/apalacio9502" target="_blank" rel="noopener">@apalacio9502</a>
, <a href="https://github.com/apeterson91" target="_blank" rel="noopener">@apeterson91</a>
, <a href="https://github.com/arnaudgallou" target="_blank" rel="noopener">@arnaudgallou</a>
, <a href="https://github.com/awpsoras" target="_blank" rel="noopener">@awpsoras</a>
, <a href="https://github.com/bakaburg1" target="_blank" rel="noopener">@bakaburg1</a>
, <a href="https://github.com/barnabasharris" target="_blank" rel="noopener">@barnabasharris</a>
, <a href="https://github.com/BHII-KSC" target="_blank" rel="noopener">@BHII-KSC</a>
, <a href="https://github.com/bholtemeyer" target="_blank" rel="noopener">@bholtemeyer</a>
, <a href="https://github.com/billdenney" target="_blank" rel="noopener">@billdenney</a>
, <a href="https://github.com/bounlu" target="_blank" rel="noopener">@bounlu</a>
, <a href="https://github.com/brendensm" target="_blank" rel="noopener">@brendensm</a>
, <a href="https://github.com/bridroberts1" target="_blank" rel="noopener">@bridroberts1</a>
, <a href="https://github.com/brookslogan" target="_blank" rel="noopener">@brookslogan</a>
, <a href="https://github.com/catalamarti" target="_blank" rel="noopener">@catalamarti</a>
, <a href="https://github.com/cboettig" target="_blank" rel="noopener">@cboettig</a>
, <a href="https://github.com/cbrnr" target="_blank" rel="noopener">@cbrnr</a>
, <a href="https://github.com/ccani007" target="_blank" rel="noopener">@ccani007</a>
, <a href="https://github.com/charliejhadley" target="_blank" rel="noopener">@charliejhadley</a>
, <a href="https://github.com/ChrisHIV" target="_blank" rel="noopener">@ChrisHIV</a>
, <a href="https://github.com/ChristianRohde" target="_blank" rel="noopener">@ChristianRohde</a>
, <a href="https://github.com/cobac" target="_blank" rel="noopener">@cobac</a>
, <a href="https://github.com/conig" target="_blank" rel="noopener">@conig</a>
, <a href="https://github.com/const-ae" target="_blank" rel="noopener">@const-ae</a>
, <a href="https://github.com/Copilot" target="_blank" rel="noopener">@Copilot</a>
, <a href="https://github.com/d-morrison" target="_blank" rel="noopener">@d-morrison</a>
, <a href="https://github.com/DanChaltiel" target="_blank" rel="noopener">@DanChaltiel</a>
, <a href="https://github.com/daniel-simeone" target="_blank" rel="noopener">@daniel-simeone</a>
, <a href="https://github.com/DanielBraddock" target="_blank" rel="noopener">@DanielBraddock</a>
, <a href="https://github.com/david-romano" target="_blank" rel="noopener">@david-romano</a>
, <a href="https://github.com/davidrsch" target="_blank" rel="noopener">@davidrsch</a>
, <a href="https://github.com/davidss101" target="_blank" rel="noopener">@davidss101</a>
, <a href="https://github.com/DavisVaughan" target="_blank" rel="noopener">@DavisVaughan</a>
, <a href="https://github.com/dcaud" target="_blank" rel="noopener">@dcaud</a>
, <a href="https://github.com/deschen1" target="_blank" rel="noopener">@deschen1</a>
, <a href="https://github.com/DesiQuintans" target="_blank" rel="noopener">@DesiQuintans</a>
, <a href="https://github.com/devster31" target="_blank" rel="noopener">@devster31</a>
, <a href="https://github.com/dkutner" target="_blank" rel="noopener">@dkutner</a>
, <a href="https://github.com/dmuenz" target="_blank" rel="noopener">@dmuenz</a>
, <a href="https://github.com/ds-jim" target="_blank" rel="noopener">@ds-jim</a>
, <a href="https://github.com/eitsupi" target="_blank" rel="noopener">@eitsupi</a>
, <a href="https://github.com/EmilHvitfeldt" target="_blank" rel="noopener">@EmilHvitfeldt</a>
, <a href="https://github.com/etiennebacher" target="_blank" rel="noopener">@etiennebacher</a>
, <a href="https://github.com/eutwt" target="_blank" rel="noopener">@eutwt</a>
, <a href="https://github.com/EvertonTLima" target="_blank" rel="noopener">@EvertonTLima</a>
, <a href="https://github.com/ferreirafm" target="_blank" rel="noopener">@ferreirafm</a>
, <a href="https://github.com/gaborcsardi" target="_blank" rel="noopener">@gaborcsardi</a>
, <a href="https://github.com/GabryS3" target="_blank" rel="noopener">@GabryS3</a>
, <a href="https://github.com/Gastonia02" target="_blank" rel="noopener">@Gastonia02</a>
, <a href="https://github.com/GBarnsley" target="_blank" rel="noopener">@GBarnsley</a>
, <a href="https://github.com/gevro" target="_blank" rel="noopener">@gevro</a>
, <a href="https://github.com/ggrothendieck" target="_blank" rel="noopener">@ggrothendieck</a>
, <a href="https://github.com/GischD" target="_blank" rel="noopener">@GischD</a>
, <a href="https://github.com/gks281263" target="_blank" rel="noopener">@gks281263</a>
, <a href="https://github.com/gracehartley" target="_blank" rel="noopener">@gracehartley</a>
, <a href="https://github.com/graphdr" target="_blank" rel="noopener">@graphdr</a>
, <a href="https://github.com/hadley" target="_blank" rel="noopener">@hadley</a>
, <a href="https://github.com/heliconone" target="_blank" rel="noopener">@heliconone</a>
, <a href="https://github.com/Hzanib" target="_blank" rel="noopener">@Hzanib</a>
, <a href="https://github.com/ilovemane" target="_blank" rel="noopener">@ilovemane</a>
, <a href="https://github.com/ja-ortiz-uniandes" target="_blank" rel="noopener">@ja-ortiz-uniandes</a>
, <a href="https://github.com/jack-davison" target="_blank" rel="noopener">@jack-davison</a>
, <a href="https://github.com/james-kilgour" target="_blank" rel="noopener">@james-kilgour</a>
, <a href="https://github.com/JamesHWade" target="_blank" rel="noopener">@JamesHWade</a>
, <a href="https://github.com/jaymicro" target="_blank" rel="noopener">@jaymicro</a>
, <a href="https://github.com/JBrandenburg02" target="_blank" rel="noopener">@JBrandenburg02</a>
, <a href="https://github.com/jc-usda" target="_blank" rel="noopener">@jc-usda</a>
, <a href="https://github.com/jennybc" target="_blank" rel="noopener">@jennybc</a>
, <a href="https://github.com/jeroenjanssens" target="_blank" rel="noopener">@jeroenjanssens</a>
, <a href="https://github.com/jestover" target="_blank" rel="noopener">@jestover</a>
, <a href="https://github.com/jl5000" target="_blank" rel="noopener">@jl5000</a>
, <a href="https://github.com/jmbarbone" target="_blank" rel="noopener">@jmbarbone</a>
, <a href="https://github.com/john-b-edwards" target="_blank" rel="noopener">@john-b-edwards</a>
, <a href="https://github.com/jordanmross" target="_blank" rel="noopener">@jordanmross</a>
, <a href="https://github.com/joshua-theisen" target="_blank" rel="noopener">@joshua-theisen</a>
, <a href="https://github.com/jrwinget" target="_blank" rel="noopener">@jrwinget</a>
, <a href="https://github.com/juliaapolonio" target="_blank" rel="noopener">@juliaapolonio</a>
, <a href="https://github.com/jxu" target="_blank" rel="noopener">@jxu</a>
, <a href="https://github.com/KaiAragaki" target="_blank" rel="noopener">@KaiAragaki</a>
, <a href="https://github.com/kiki830621" target="_blank" rel="noopener">@kiki830621</a>
, <a href="https://github.com/KittJonathan" target="_blank" rel="noopener">@KittJonathan</a>
, <a href="https://github.com/kleinerChemiker" target="_blank" rel="noopener">@kleinerChemiker</a>
, <a href="https://github.com/kletts" target="_blank" rel="noopener">@kletts</a>
, <a href="https://github.com/krlmlr" target="_blank" rel="noopener">@krlmlr</a>
, <a href="https://github.com/ks8997" target="_blank" rel="noopener">@ks8997</a>
, <a href="https://github.com/kylebutts" target="_blank" rel="noopener">@kylebutts</a>
, <a href="https://github.com/larsentom" target="_blank" rel="noopener">@larsentom</a>
, <a href="https://github.com/latot" target="_blank" rel="noopener">@latot</a>
, <a href="https://github.com/lboller-pwbm" target="_blank" rel="noopener">@lboller-pwbm</a>
, <a href="https://github.com/lionel-" target="_blank" rel="noopener">@lionel-</a>
, <a href="https://github.com/Longfei2" target="_blank" rel="noopener">@Longfei2</a>
, <a href="https://github.com/lschneiderbauer" target="_blank" rel="noopener">@lschneiderbauer</a>
, <a href="https://github.com/LukasTang" target="_blank" rel="noopener">@LukasTang</a>
, <a href="https://github.com/lukebandy" target="_blank" rel="noopener">@lukebandy</a>
, <a href="https://github.com/maciekbanas" target="_blank" rel="noopener">@maciekbanas</a>
, <a href="https://github.com/maelle" target="_blank" rel="noopener">@maelle</a>
, <a href="https://github.com/marcuslehr" target="_blank" rel="noopener">@marcuslehr</a>
, <a href="https://github.com/Mark-AP" target="_blank" rel="noopener">@Mark-AP</a>
, <a href="https://github.com/markwestcott34" target="_blank" rel="noopener">@markwestcott34</a>
, <a href="https://github.com/maskegger" target="_blank" rel="noopener">@maskegger</a>
, <a href="https://github.com/matiasandina" target="_blank" rel="noopener">@matiasandina</a>
, <a href="https://github.com/matthewjnield" target="_blank" rel="noopener">@matthewjnield</a>
, <a href="https://github.com/mbcann01" target="_blank" rel="noopener">@mbcann01</a>
, <a href="https://github.com/Meghansaha" target="_blank" rel="noopener">@Meghansaha</a>
, <a href="https://github.com/metanoid" target="_blank" rel="noopener">@metanoid</a>
, <a href="https://github.com/MichaelChirico" target="_blank" rel="noopener">@MichaelChirico</a>
, <a href="https://github.com/MikeJohnPage" target="_blank" rel="noopener">@MikeJohnPage</a>
, <a href="https://github.com/MilesMcBain" target="_blank" rel="noopener">@MilesMcBain</a>
, <a href="https://github.com/mine-cetinkaya-rundel" target="_blank" rel="noopener">@mine-cetinkaya-rundel</a>
, <a href="https://github.com/MohsenSoltanifar" target="_blank" rel="noopener">@MohsenSoltanifar</a>
, <a href="https://github.com/moodymudskipper" target="_blank" rel="noopener">@moodymudskipper</a>
, <a href="https://github.com/Moohan" target="_blank" rel="noopener">@Moohan</a>
, <a href="https://github.com/mp8" target="_blank" rel="noopener">@mp8</a>
, <a href="https://github.com/mpsturbo" target="_blank" rel="noopener">@mpsturbo</a>
, <a href="https://github.com/mr-c" target="_blank" rel="noopener">@mr-c</a>
, <a href="https://github.com/muschellij2" target="_blank" rel="noopener">@muschellij2</a>
, <a href="https://github.com/musvaage" target="_blank" rel="noopener">@musvaage</a>
, <a href="https://github.com/Mzhuk7" target="_blank" rel="noopener">@Mzhuk7</a>
, <a href="https://github.com/nalimilan" target="_blank" rel="noopener">@nalimilan</a>
, <a href="https://github.com/nathanhaigh" target="_blank" rel="noopener">@nathanhaigh</a>
, <a href="https://github.com/nirguk" target="_blank" rel="noopener">@nirguk</a>
, <a href="https://github.com/nmercadeb" target="_blank" rel="noopener">@nmercadeb</a>
, <a href="https://github.com/olivermagnanimous" target="_blank" rel="noopener">@olivermagnanimous</a>
, <a href="https://github.com/olivroy" target="_blank" rel="noopener">@olivroy</a>
, <a href="https://github.com/orgadish" target="_blank" rel="noopener">@orgadish</a>
, <a href="https://github.com/pangchaoran" target="_blank" rel="noopener">@pangchaoran</a>
, <a href="https://github.com/paschatz" target="_blank" rel="noopener">@paschatz</a>
, <a href="https://github.com/prubin73" target="_blank" rel="noopener">@prubin73</a>
, <a href="https://github.com/PStaus" target="_blank" rel="noopener">@PStaus</a>
, <a href="https://github.com/psychelzh" target="_blank" rel="noopener">@psychelzh</a>
, <a href="https://github.com/py9mrg" target="_blank" rel="noopener">@py9mrg</a>
, <a href="https://github.com/Raesu" target="_blank" rel="noopener">@Raesu</a>
, <a href="https://github.com/randyzwitch" target="_blank" rel="noopener">@randyzwitch</a>
, <a href="https://github.com/Raoul-Kima" target="_blank" rel="noopener">@Raoul-Kima</a>
, <a href="https://github.com/ReedMerrill" target="_blank" rel="noopener">@ReedMerrill</a>
, <a href="https://github.com/RodDalBen" target="_blank" rel="noopener">@RodDalBen</a>
, <a href="https://github.com/RodrigoZepeda" target="_blank" rel="noopener">@RodrigoZepeda</a>
, <a href="https://github.com/rossholmberg" target="_blank" rel="noopener">@rossholmberg</a>
, <a href="https://github.com/RoyalTS" target="_blank" rel="noopener">@RoyalTS</a>
, <a href="https://github.com/ryandward" target="_blank" rel="noopener">@ryandward</a>
, <a href="https://github.com/sbanville-delfi" target="_blank" rel="noopener">@sbanville-delfi</a>
, <a href="https://github.com/ScientiaFelis" target="_blank" rel="noopener">@ScientiaFelis</a>
, <a href="https://github.com/shirdekel" target="_blank" rel="noopener">@shirdekel</a>
, <a href="https://github.com/slager" target="_blank" rel="noopener">@slager</a>
, <a href="https://github.com/sschooler" target="_blank" rel="noopener">@sschooler</a>
, <a href="https://github.com/steffen-stell" target="_blank" rel="noopener">@steffen-stell</a>
, <a href="https://github.com/szimmer" target="_blank" rel="noopener">@szimmer</a>
, <a href="https://github.com/TheClownBongo" target="_blank" rel="noopener">@TheClownBongo</a>
, <a href="https://github.com/thomasjwood" target="_blank" rel="noopener">@thomasjwood</a>
, <a href="https://github.com/TimTaylor" target="_blank" rel="noopener">@TimTaylor</a>
, <a href="https://github.com/tlyons253" target="_blank" rel="noopener">@tlyons253</a>
, <a href="https://github.com/tomalrussell" target="_blank" rel="noopener">@tomalrussell</a>
, <a href="https://github.com/tomwagstaff-opml" target="_blank" rel="noopener">@tomwagstaff-opml</a>
, <a href="https://github.com/torfason" target="_blank" rel="noopener">@torfason</a>
, <a href="https://github.com/Tyrrx" target="_blank" rel="noopener">@Tyrrx</a>
, <a href="https://github.com/Unaimend" target="_blank" rel="noopener">@Unaimend</a>
, <a href="https://github.com/VisruthSK" target="_blank" rel="noopener">@VisruthSK</a>
, <a href="https://github.com/vorpalvorpal" target="_blank" rel="noopener">@vorpalvorpal</a>
, <a href="https://github.com/walkerjameschris" target="_blank" rel="noopener">@walkerjameschris</a>
, <a href="https://github.com/wbvguo" target="_blank" rel="noopener">@wbvguo</a>
, <a href="https://github.com/wbzyl" target="_blank" rel="noopener">@wbzyl</a>
, <a href="https://github.com/wkumler" target="_blank" rel="noopener">@wkumler</a>
, <a href="https://github.com/yaboody" target="_blank" rel="noopener">@yaboody</a>
, <a href="https://github.com/yjunechoe" target="_blank" rel="noopener">@yjunechoe</a>
, <a href="https://github.com/ynsec37" target="_blank" rel="noopener">@ynsec37</a>
, <a href="https://github.com/ywhcuhk" target="_blank" rel="noopener">@ywhcuhk</a>
, <a href="https://github.com/ZHBHSMILE" target="_blank" rel="noopener">@ZHBHSMILE</a>
, <a href="https://github.com/zhjx19" target="_blank" rel="noopener">@zhjx19</a>
, and <a href="https://github.com/ZIBOWANGKANGYU" target="_blank" rel="noopener">@ZIBOWANGKANGYU</a>
.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/dplyr-1-2-0/thumbnail-wd.jpg" length="338917" type="image/jpeg" />
    </item>
    <item>
      <title>ragnar 0.3.0</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/ragnar-0-3-0/</link>
      <pubDate>Tue, 27 Jan 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/ragnar-0-3-0/</guid>
      <dc:creator>Tomasz Kalinowski</dc:creator><description><![CDATA[<h1 id="ragnar-030">ragnar 0.3.0
</h1>
<p>We&rsquo;re happy to announce that <a href="https://ragnar.tidyverse.org/" target="_blank" rel="noopener">ragnar 0.3.0</a>
 is now available on CRAN. ragnar is a tidy, transparent toolkit for building trustworthy retrieval-augmented generation (RAG) workflows: ingest documents, build a store, retrieve relevant chunks, and inspect exactly what&rsquo;s being fed to a model.</p>
<p>If you&rsquo;re new to ragnar, the quickest way to get oriented is the <a href="https://ragnar.tidyverse.org/articles/ragnar.html" target="_blank" rel="noopener">Getting Started vignette</a>
. If you&rsquo;ve already built a store with ragnar 0.2, this release focuses on making it easier to scale ingestion, use more embedding providers, and connect your store to the tools you already use.</p>
<p>You can install ragnar from CRAN with:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/utils/install.packages.html'>install.packages</a></span><span class='o'>(</span><span class='s'>"ragnar"</span><span class='o'>)</span></span></code></pre>
</div>
<p>This post covers the biggest user-facing changes in ragnar 0.3.0. For a complete list of changes, see the <a href="https://github.com/tidyverse/ragnar/blob/main/NEWS.md" target="_blank" rel="noopener">NEWS</a>
.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://ragnar.tidyverse.org/'>ragnar</a></span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="a-quick-refresher">A quick refresher
</h2>
<p>If you&rsquo;re already familiar with ragnar, feel free to skip this section.</p>
<p>ragnar helps you build retrieval-augmented generation (RAG) workflows by turning your trusted documents into a local store that you can query with both vector search (embeddings) and keyword search (BM25).</p>
<p>At the &ldquo;front door&rdquo;, <a href="https://ragnar.tidyverse.org/reference/read_as_markdown.html" target="_blank" rel="noopener"><code>read_as_markdown()</code></a>
 can ingest web pages, PDFs, Office documents, images (via OCR), archives, and even YouTube URLs (via transcripts), so you can usually start from the same sources you&rsquo;d use for manual research.</p>
<p>At a high level, a typical ragnar workflow has three parts:</p>
<ol>
<li>Build a store:
<ul>
<li>Collect document sources (URLs or files) and convert them to Markdown with <a href="https://ragnar.tidyverse.org/reference/read_as_markdown.html" target="_blank" rel="noopener"><code>read_as_markdown()</code></a>
.</li>
<li>Split documents into chunks with <a href="https://ragnar.tidyverse.org/reference/markdown_chunk.html" target="_blank" rel="noopener"><code>markdown_chunk()</code></a>
 (optionally adding context).</li>
<li>Embed and store chunks in a DuckDB-backed <code>RagnarStore</code>.</li>
</ul>
</li>
<li>Query and inspect the store:
<ul>
<li>Retrieve chunks directly with <a href="https://ragnar.tidyverse.org/reference/ragnar_retrieve.html" target="_blank" rel="noopener"><code>ragnar_retrieve()</code></a>
. It returns a tibble with scores, source information, and the chunk text (including columns like <code>origin</code>, <code>cosine_distance</code>, <code>bm25</code>, <code>context</code>, and <code>text</code>), so you can inspect exactly what will be passed downstream.</li>
<li>Use the Store Inspector or Embedding Atlas (<a href="https://ragnar.tidyverse.org/reference/ragnar_store_inspect.html" target="_blank" rel="noopener"><code>ragnar_store_inspect()</code></a>
 and <a href="https://ragnar.tidyverse.org/reference/ragnar_store_atlas.html" target="_blank" rel="noopener"><code>ragnar_store_atlas()</code></a>
) to understand what&rsquo;s working, then iterate and go back to step 1 as needed.</li>
</ul>
</li>
<li>Connect the store to tools:
<ul>
<li>Register a retrieval tool with an ellmer chat so an agent can search the store on demand.</li>
<li>Serve retrieval over MCP so external tools and agents can query the store directly.</li>
<li>Write your own loop using <a href="https://ragnar.tidyverse.org/reference/ragnar_retrieve.html" target="_blank" rel="noopener"><code>ragnar_retrieve()</code></a>
 or lower-level helpers like <a href="https://ragnar.tidyverse.org/reference/ragnar_retrieve_vss.html" target="_blank" rel="noopener"><code>ragnar_retrieve_vss()</code></a>
 and <a href="https://ragnar.tidyverse.org/reference/ragnar_retrieve_bm25.html" target="_blank" rel="noopener"><code>ragnar_retrieve_bm25()</code></a>
.</li>
</ul>
</li>
</ol>
<h2 id="whats-new">What&rsquo;s new
</h2>
<p>This release focuses on four big improvements:</p>
<ul>
<li>Faster ingestion for large corpora with <a href="https://ragnar.tidyverse.org/reference/ragnar_store_ingest.html" target="_blank" rel="noopener"><code>ragnar_store_ingest()</code></a>
.</li>
<li>Better retrieval with multi-query support and better de-duplication and de-overlapping of results.</li>
<li>New embedding providers: Azure OpenAI and Snowflake.</li>
<li>New integrations and tooling: serve a store over MCP, plus improved inspection with the Store Inspector and embedding atlas.</li>
</ul>
<p>In the sections below, we&rsquo;ll walk through each change in more detail.</p>
<h3 id="faster-ingestion-with-ragnar_store_ingest">Faster ingestion with <code>ragnar_store_ingest()</code>
</h3>
<p>Ingestion is usually the slowest part of building a knowledge store. <a href="https://ragnar.tidyverse.org/reference/ragnar_store_ingest.html" target="_blank" rel="noopener"><code>ragnar_store_ingest()</code></a>
 parallelizes the document preparation step with <a href="https://mirai.r-lib.org" target="_blank" rel="noopener">mirai</a>
, and then writes prepared chunks to the store in the main process. It&rsquo;s designed to make it easy to ingest hundreds (or thousands) of pages without hand-rolling your own parallel pipeline.</p>
<p>Only preparation (reading, chunking, and optionally embedding) is parallelized; store writes still happen in the main process.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>store</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://ragnar.tidyverse.org/reference/ragnar_store_create.html'>ragnar_store_create</a></span><span class='o'>(</span></span>
<span>  <span class='s'>"docs.ragnar.duckdb"</span>,</span>
<span>  embed <span class='o'>=</span> \<span class='o'>(</span><span class='nv'>x</span><span class='o'>)</span> <span class='nf'>ragnar</span><span class='nf'>::</span><span class='nf'><a href='https://ragnar.tidyverse.org/reference/embed_ollama.html'>embed_openai</a></span><span class='o'>(</span><span class='nv'>x</span>, model <span class='o'>=</span> <span class='s'>"text-embedding-3-small"</span><span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>paths</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://ragnar.tidyverse.org/reference/ragnar_find_links.html'>ragnar_find_links</a></span><span class='o'>(</span><span class='s'>"https://quarto.org/sitemap.xml"</span><span class='o'>)</span></span>
<span></span>
<span><span class='nf'><a href='https://ragnar.tidyverse.org/reference/ragnar_store_ingest.html'>ragnar_store_ingest</a></span><span class='o'>(</span><span class='nv'>store</span>, <span class='nv'>paths</span>, n_workers <span class='o'>=</span> <span class='m'>4</span>, prepare <span class='o'>=</span> \<span class='o'>(</span><span class='nv'>path</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>  <span class='nv'>path</span> <span class='o'>|&gt;</span> <span class='nf'><a href='https://ragnar.tidyverse.org/reference/read_as_markdown.html'>read_as_markdown</a></span><span class='o'>(</span><span class='o'>)</span> <span class='o'>|&gt;</span> <span class='nf'><a href='https://ragnar.tidyverse.org/reference/markdown_chunk.html'>markdown_chunk</a></span><span class='o'>(</span><span class='o'>)</span></span>
<span><span class='o'>&#125;</span><span class='o'>)</span></span></code></pre>
</div>
<h3 id="better-retrieval-multiple-queries-and-fewer-duplicates">Better retrieval: multiple queries and fewer duplicates
</h3>
<p>Retrieval is where ragnar tries to be pragmatic: we run both semantic search (embeddings) and keyword search (BM25) because they fail in different ways. This release makes it easier to do that intentionally.</p>
<ul>
<li><a href="https://ragnar.tidyverse.org/reference/ragnar_retrieve.html" target="_blank" rel="noopener"><code>ragnar_retrieve()</code></a>
 now accepts a <em>vector of queries</em>, so you can pass one query tuned for semantic search and one tuned for keywords.</li>
<li><a href="https://ragnar.tidyverse.org/reference/ragnar_register_tool_retrieve.html" target="_blank" rel="noopener"><code>ragnar_register_tool_retrieve()</code></a>
 uses a new default tool name prefix: <code>search_{store@name}</code> (instead of <code>rag_retrieve_from_{store@name}</code>).</li>
<li>When registered with ellmer, ragnar&rsquo;s retrieval tool continues to avoid returning previously returned chunks, enabling deeper searches via repeated tool calls.</li>
<li>BM25 result ordering was corrected to sort by descending score.</li>
<li>Duplicate rows from <a href="https://ragnar.tidyverse.org/reference/ragnar_retrieve.html" target="_blank" rel="noopener"><code>ragnar_retrieve()</code></a>
 when running multiple queries were removed.</li>
</ul>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://ragnar.tidyverse.org/reference/ragnar_retrieve.html'>ragnar_retrieve</a></span><span class='o'>(</span></span>
<span>  <span class='nv'>store</span>,</span>
<span>  <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span></span>
<span>    <span class='s'>"How do I subset a data frame with a logical vector?"</span>,</span>
<span>    <span class='s'>"subset dataframe logical vector"</span></span>
<span>  <span class='o'>)</span>,</span>
<span>  top_k <span class='o'>=</span> <span class='m'>10</span></span>
<span><span class='o'>)</span></span></code></pre>
</div>
<h3 id="new-embedding-providers-azure-openai-and-snowflake">New embedding providers: Azure OpenAI and Snowflake
</h3>
<p>ragnar&rsquo;s embedding helpers continue to expand so you can use the infrastructure you already have:</p>
<ul>
<li><a href="https://ragnar.tidyverse.org/reference/embed_azure_openai.html" target="_blank" rel="noopener"><code>embed_azure_openai()</code></a>
 supports embeddings from Azure AI Foundry.</li>
<li><a href="https://ragnar.tidyverse.org/reference/embed_snowflake.html" target="_blank" rel="noopener"><code>embed_snowflake()</code></a>
 supports embeddings via the Snowflake Cortex Embedding API.</li>
</ul>
<p>These integrate the same way as the other providers: you choose an embed function when creating a store, and ragnar uses it during insertion and retrieval.</p>
<h3 id="better-document-reading-including-youtube-transcripts">Better document reading (including YouTube transcripts)
</h3>
<p><a href="https://ragnar.tidyverse.org/reference/read_as_markdown.html" target="_blank" rel="noopener"><code>read_as_markdown()</code></a>
 is now more robust across common inputs, so you get higher-quality documents without having to hand-fix edge cases.</p>
<ul>
<li>Substantial improvements to HTML-to-Markdown conversion, including correct handling of nested code blocks, plus a range of other robustness fixes driven by real-world failure cases.</li>
<li><a href="https://ragnar.tidyverse.org/reference/read_as_markdown.html" target="_blank" rel="noopener"><code>read_as_markdown()</code></a>
 once again fetches YouTube transcripts and now supports a <code>youtube_transcript_formatter</code> so you can include timestamps or links in the transcript output.</li>
<li>Reading plain text with non-ASCII content was fixed.</li>
<li><a href="https://ragnar.tidyverse.org/reference/read_as_markdown.html" target="_blank" rel="noopener"><code>read_as_markdown()</code></a>
 gained an <code>origin</code> argument to control what gets recorded on returned documents.</li>
</ul>
<p>Together, these changes make ingestion more reliable, which helps improve retrieval quality downstream.</p>
<h3 id="new-integrations-serve-a-store-over-mcp">New integrations: serve a store over MCP
</h3>
<p><a href="https://ragnar.tidyverse.org/reference/mcp_serve_store.html" target="_blank" rel="noopener"><code>mcp_serve_store()</code></a>
 lets you expose a <code>RagnarStore</code> as an MCP tool. This is particularly useful if you already have a local store and want an MCP-enabled client (like Codex CLI or Claude Code) to query it directly.</p>
<p>For example, with Codex CLI you can add something like this to <code>~/.codex/config.toml</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">mcp_servers</span><span class="p">.</span><span class="nx">my_store</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">command</span> <span class="p">=</span> <span class="s2">&#34;Rscript&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">args</span> <span class="p">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;-e&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;ragnar::mcp_serve_store(&#39;docs.ragnar.duckdb&#39;, top_k=10)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This runs a long-lived R process that exposes retrieval over MCP.</p>
<h3 id="new-ways-to-inspect-a-store">New ways to inspect a store
</h3>
<p>ragnar now has more tools to help you understand what your store contains and why retrieval is (or isn&rsquo;t) working:</p>
<ul>
<li>The Store Inspector received a number of usability improvements (keyboard shortcuts, improved preview, better metadata display, and general bug fixes).</li>
<li><a href="https://ragnar.tidyverse.org/reference/ragnar_store_atlas.html" target="_blank" rel="noopener"><code>ragnar_store_atlas()</code></a>
 integrates with the Embedding Atlas project to visualize your embedding space (via reticulate).</li>
</ul>
<p>The Store Inspector makes it easy to iterate on retrieval: try a query, compare vector search and BM25, and inspect the underlying chunks and metadata that were returned. The screenshots below show a store built from the Quarto documentation.</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/tidyverse/ragnar-0-3-0/ragnar-store-inspector.png" alt="The Store Inspector, showing retrieval results and a document preview." />
<figcaption aria-hidden="true">The Store Inspector, showing retrieval results and a document preview.</figcaption>
</figure>
<p>If you&rsquo;re not sure whether a store &ldquo;looks right&rdquo;, <a href="https://ragnar.tidyverse.org/reference/ragnar_store_atlas.html" target="_blank" rel="noopener"><code>ragnar_store_atlas()</code></a>
 gives you a high-level view of how your documents cluster in embedding space. It&rsquo;s a useful way to spot outliers, see which areas of the space match a query, and explore how clusters relate back to your sources.</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/tidyverse/ragnar-0-3-0/ragnar-store-atlas.png" alt="An embedding atlas view of a ragnar store, with query highlighting and metadata filters." />
<figcaption aria-hidden="true">An embedding atlas view of a ragnar store, with query highlighting and metadata filters.</figcaption>
</figure>
<h2 id="get-started">Get started
</h2>
<p>Install ragnar with <code>install.packages(&quot;ragnar&quot;)</code>, then work through the <a href="https://ragnar.tidyverse.org/articles/ragnar.html" target="_blank" rel="noopener">Getting Started vignette</a>
. For details on individual functions, see the <a href="https://ragnar.tidyverse.org/reference/" target="_blank" rel="noopener">function reference</a>
. For the full changelog, see <a href="https://github.com/tidyverse/ragnar/blob/main/NEWS.md" target="_blank" rel="noopener">NEWS</a>
.</p>
<p>ragnar is designed to help you build trustworthy RAG workflows by making it easy to inspect what gets retrieved and what ultimately gets sent to your model. If you try ragnar 0.3.0, we&rsquo;d love to hear what you&rsquo;re using it for in <a href="https://github.com/tidyverse/ragnar/discussions" target="_blank" rel="noopener">GitHub Discussions</a>
.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>Thanks to everyone who contributed to ragnar 0.3.0 through code, issues, testing, and feedback: <a href="https://github.com/agricolamz" target="_blank" rel="noopener">@agricolamz</a>
, <a href="https://github.com/AlekFisher" target="_blank" rel="noopener">@AlekFisher</a>
, <a href="https://github.com/bianchenhao" target="_blank" rel="noopener">@bianchenhao</a>
, <a href="https://github.com/brooklynbagel" target="_blank" rel="noopener">@brooklynbagel</a>
, <a href="https://github.com/bshashikadze" target="_blank" rel="noopener">@bshashikadze</a>
, <a href="https://github.com/christophscheuch" target="_blank" rel="noopener">@christophscheuch</a>
, <a href="https://github.com/cstubben" target="_blank" rel="noopener">@cstubben</a>
, <a href="https://github.com/dfalbel" target="_blank" rel="noopener">@dfalbel</a>
, <a href="https://github.com/eschillerstrom-usfws" target="_blank" rel="noopener">@eschillerstrom-usfws</a>
, <a href="https://github.com/grantmcdermott" target="_blank" rel="noopener">@grantmcdermott</a>
, <a href="https://github.com/howardbaik" target="_blank" rel="noopener">@howardbaik</a>
, <a href="https://github.com/jeroenjanssens" target="_blank" rel="noopener">@jeroenjanssens</a>
, <a href="https://github.com/jhbrut" target="_blank" rel="noopener">@jhbrut</a>
, <a href="https://github.com/JosiahParry" target="_blank" rel="noopener">@JosiahParry</a>
, <a href="https://github.com/jpmarindiaz" target="_blank" rel="noopener">@jpmarindiaz</a>
, <a href="https://github.com/luisDVA" target="_blank" rel="noopener">@luisDVA</a>
, <a href="https://github.com/mattwarkentin" target="_blank" rel="noopener">@mattwarkentin</a>
, <a href="https://github.com/Rednose22" target="_blank" rel="noopener">@Rednose22</a>
, <a href="https://github.com/shikokuchuo" target="_blank" rel="noopener">@shikokuchuo</a>
, <a href="https://github.com/smach" target="_blank" rel="noopener">@smach</a>
, <a href="https://github.com/SokolovAnatoliy" target="_blank" rel="noopener">@SokolovAnatoliy</a>
, <a href="https://github.com/t-kalinowski" target="_blank" rel="noopener">@t-kalinowski</a>
, <a href="https://github.com/thisisnic" target="_blank" rel="noopener">@thisisnic</a>
, and <a href="https://github.com/vrognas" target="_blank" rel="noopener">@vrognas</a>
.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/ragnar-0-3-0/thumbnail-wd.jpg" length="329090" type="image/jpeg" />
    </item>
    <item>
      <title>Where Questions Become Queries: Meet querychat</title>
      <link>https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/</link>
      <pubDate>Thu, 22 Jan 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/</guid>
      <dc:creator>Veerle Eeftink - Van Leemput</dc:creator><description><![CDATA[<p>You love data. And you love building dashboards with it, especially with your favourite tool Shiny. But even with a bullet-proof design, dozens of user stories, feedback loops, and adjustments, you also know that there are always questions that your dashboard leaves unanswered.</p>
<p>Let&rsquo;s say you developed a dashboard to display women&rsquo;s international soccer matches. You are proud of what you have built and you eagerly show it to a colleague:</p>
<p><strong>Colleague</strong>: &ldquo;Amazing! Can you show me the soccer matches for the FIFA World Cup only?&rdquo;</p>
<p><strong>You</strong>: &ldquo;Of course, let me filter it down for you and select the FIFA World Cup tournaments&rdquo;</p>
<p><strong>Colleague</strong>: &ldquo;Interesting, can you show me all the matches in which The Netherlands have played?&rdquo;</p>
<p><strong>You</strong>: &ldquo;Eh&hellip; Well, I could, but I just have to include a country filter in my dashboard then!&rdquo;</p>
<p>Right. How are we supposed to filter down to a specific country if there is no input for it? And what about getting summary statistics for countries or players? By now it becomes painfully clear that our soccer dashboard has its limits.</p>
<p>This is exactly the moment when <a href="https://posit-dev.github.io/querychat" target="_blank" rel="noopener"><code>querychat</code></a>
 becomes interesting. It is a multilingual package that allows you to chat with your data using natural language queries. No more clicking, no more limited filters, just you and your questions. And in this article, you&rsquo;re going to learn everything about it!</p>
<p>To bring <code>querychat</code> to life, we will keep returning to two examples:</p>
<ul>
<li>The classic diamonds dataset. After all, diamonds are a girl&rsquo;s best friend, and a data scientist&rsquo;s too! The familiar dataset offers a mix of variables such as cut, colour, clarity and price, which makes it ideal for all sorts of natural language questions. You might wonder about average prices for particular cuts, or you want to compare colours, look at how clarity affects value, or explore simple patterns in the data. In other words, it is a perfect playground for testing how well natural language queries behave on structured data.</li>
<li>SheScores, the soccer dashboard that you were so proud of earlier. This app originates from the shiny::conf(2024) workshop <a href="https://github.com/hypebright/shinyconf2024-shiny101" target="_blank" rel="noopener">&ldquo;Shiny 101: The Modular App Blueprint&rdquo;</a>
, although it has been tweaked to make it a bit more interesting and updated with matches through to November 2025.</li>
</ul>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-1" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-1-1">Python</a></li>
<li><a href="#tabset-1-2">R</a></li>
</ul>
<div id="tabset-1-1">
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/shescores-original-py.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
<div id="tabset-1-2">
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/shescores-original-r.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
</div>
<p>Both datasets set the stage nicely, so let&rsquo;s roll the ball and see how <code>querychat</code> plays. We&rsquo;re talking about soccer after all!</p>
<blockquote>
<p><strong>Full code available on GitHub</strong></p>
<p>Instead of copy-pasting the content of this blog into your favourite IDE, you can also <a href="https://github.com/hypebright/shescores-dashboard" target="_blank" rel="noopener">pull the project from GitHub</a>
 and follow along. All the code is available in both Python and R.</p>
</blockquote>
<blockquote>
<p><strong>Short on time?</strong></p>
<p>Jump straight to the <a href="#adding-querychat-to-your-existing-shiny-app">SheScores app with querychat</a>
 or visit the <a href="https://posit-dev.github.io/querychat" target="_blank" rel="noopener">querychat</a>
 website</p>
</blockquote>
<h1 id="hello-querychat">Hello, querychat
</h1>
<p>In short, <code>querychat</code> makes it easy to query data using natural language. It offers a drop-in component for Shiny, a console interface, and other programmatic building-blocks. You ask questions, <code>querychat</code> translates it to a SQL query, executes it, and returns the results. The results are available as a reactive data frame, which makes it easy to display or further process the data.</p>
<p><code>querychat</code> would solve the problem we encountered earlier. We can ask it any question we can imagine without constantly adding filters or other analysis. No country filter? No problem. And yes, that sounds as cool as it is!</p>
<p>So, what do we need?</p>
<p><code>querychat</code> is powered by a Large Language Model (LLM), so you need access to a model. You first need to register at an LLM provider that provides those models. You can choose any model you like, with two little &ldquo;restrictions&rdquo;: <a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener"><code>chatlas</code></a>
 (Python) or <a href="https://ellmer.tidyverse.org" target="_blank" rel="noopener"><code>ellmer</code></a>
 (R) supports it (which shouldn&rsquo;t be hard, because all the major models are) and the model has the ability to do tool calls.</p>
<blockquote>
<p><strong>Recommended models</strong></p>
<p>In this blog we&rsquo;ll use Claude Sonnet 4.5 from Anthropic. Other good choices would be GPT-4.1 (the current default for <code>querychat</code>) and Google Gemini 3.0 (as of November 2025).</p>
</blockquote>
<p>Once you&rsquo;ve made your choice and registered, you can get an API key. You need this key to authenticate with the LLM provider. One important note: never, ever hardcode the key directly into your script. You&rsquo;ll be amazed how many keys are publicly available on GitHub repos. Don&rsquo;t be that developer. As always with secrets, store it as an environment variable. Just note that the exact name of the key depends on the provider. For example, Anthropic expects <code>ANTHROPIC_API_KEY=yourkey</code>, while OpenAI uses <code>OPENAI_API_KEY=yourkey</code>.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-2" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-2-1">Python</a></li>
<li><a href="#tabset-2-2">R</a></li>
</ul>
<div id="tabset-2-1">
<p>In Python, the recommended approach is to create a <code>.env</code> file in your project folder:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">ANTHROPIC_API_KEY</span><span class="o">=</span><span class="n">yourkey</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>It&rsquo;s recommended to use the <code>dotenv</code> package to load the <code>.env</code> file into your environment:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">dotenv</span> <span class="kn">import</span> <span class="n">load_dotenv</span>
</span></span><span class="line"><span class="cl"><span class="n">load_dotenv</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>To keep the demo code concise we&rsquo;ll omit these lines from subsequent code examples.</p>
</div>
<div id="tabset-2-2">
<p>In R, environment variables are typically stored in a <code>.Renviron</code> file. You can create this file in your project root or in your home directory (<code>~/.Renviron</code>). Or, if you want to make it yourself really easy: you can also open/edit the relevant file with <code>usethis::edit_r_environ()</code>.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">ANTHROPIC_API_KEY</span><span class="o">=</span><span class="n">yourkey</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>Of course we can&rsquo;t use <code>querychat</code> without installing it, so that&rsquo;s the next step:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-3" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-3-1">Python</a></li>
<li><a href="#tabset-3-2">R</a></li>
</ul>
<div id="tabset-3-1">
<p>For Python, <code>querychat</code> is available on PyPI, so you can install it easily with <code>pip</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install querychat
</span></span></code></pre></td></tr></table>
</div>
</div><p>Or, if you&rsquo;re using <code>uv</code>, add it like so:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">uv add querychat
</span></span></code></pre></td></tr></table>
</div>
</div><p>Once installed, import it like this:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">querychat</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-3-2">
<p>You can get <code>querychat</code> from CRAN using:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;querychat&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Alternatively, if you want the latest development version, you can install <code>querychat</code> from GitHub using:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">pak</span><span class="o">::</span><span class="nf">pak</span><span class="p">(</span><span class="s">&#34;posit-dev/querychat/pkg-r&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Once installed, load the package as usual:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">querychat</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>Wouldn&rsquo;t it be great if you can use <code>querychat</code> straight away without much code? Just to see what it&rsquo;s all about? Luckily you can with the &ldquo;quick launch&rdquo; Shiny app! You can simply call <code>app()</code> which spins up an app with <code>querychat</code> chat interface. Let&rsquo;s try it out for our diamonds dataset:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-4" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-4-1">Python</a></li>
<li><a href="#tabset-4-2">R</a></li>
</ul>
<div id="tabset-4-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">seaborn</span> <span class="kn">import</span> <span class="n">load_dataset</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">querychat</span> <span class="kn">import</span> <span class="n">QueryChat</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">diamonds</span> <span class="o">=</span> <span class="n">load_dataset</span><span class="p">(</span><span class="s2">&#34;diamonds&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="n">diamonds</span><span class="p">,</span> <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span> <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">qc</span><span class="o">.</span><span class="n">app</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>qc = QueryChat(...)</code> creates an instance of the <code>QueryChat</code> class. You pass in the dataset, give it a name and specify the model client (powered by <a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener"><code>chatlas</code></a>
). <code>qc.app()</code> returns the web app that lets you explore the diamonds data using natural language questions.</p>
<p>Want to try this with a different provider and/or model? No problem, just change the <code>client</code> argument accordingly. For example, to use GPT-4.1 from OpenAI, you would write: <code>client=&quot;openai/gpt-4.1&quot;</code>. You can learn more about the different options in the <a href="https://posit-dev.github.io/querychat/py/models.html" target="_blank" rel="noopener"><code>querychat</code> documentation</a>
.</p>
<p>To run this app, you need to save the code above in a file (and call it <a href="http://diamonds-app.py" target="_blank" rel="noopener"><code>diamonds-app.py</code></a>
 for example) and run it like so:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">shiny</span> <span class="n">run</span> <span class="o">--</span><span class="n">reload</span> <span class="n">diamonds</span><span class="o">-</span><span class="n">app</span><span class="o">.</span><span class="n">py</span> 
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-4-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ellmer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ggplot2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">querychat_app</span><span class="p">(</span><span class="n">diamonds</span><span class="p">,</span> <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Or, alternatively, you could write:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ellmer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ggplot2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span><span class="o">$</span><span class="nf">app</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Both result in the same outcome, the first one is just a simplified version.</p>
<p><code>QueryChat$new()</code> creates the R6 object, taking the dataset, a table name and the model client (which will be passed to <code>ellmer::chat()</code>). Calling <code>qc$app()</code> then launches the Shiny app so you can query the diamonds dataset in plain English.</p>
<p>Want to change the provider and/or model? No problem, just change the <code>client</code> argument accordingly. For example, to use GPT-4.1 from OpenAI, you would write: <code>client = &quot;openai/gpt-4.1&quot;</code>. You can learn more about the different options in the <a href="https://posit-dev.github.io/querychat/r/index.html#use-a-different-llm-provider" target="_blank" rel="noopener"><code>querychat</code> documentation</a>
.</p>
</div>
</div>
<p>The result: a Shiny app that allows users to interact with a data source using natural language queries.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-5" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-5-1">Python</a></li>
<li><a href="#tabset-5-2">R</a></li>
</ul>
<div id="tabset-5-1">
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/diamonds-py.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
<div id="tabset-5-2">
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/diamonds-r.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<blockquote>
<p><strong>Custom branding</strong></p>
<p>Do you notice the nice green touches and custom font in this demo app? That&rsquo;s because the project we&rsquo;ll be using in this article uses <a href="https://posit-dev.github.io/brand-yml/" target="_blank" rel="noopener">brand.yml</a>
: a simple, portable YAML file that codifies brand guidelines into a format that can be used by Quarto, Python and R. And in this case, it works beautifully for Shiny. Curious to see what such a <code>_brand.yml</code> file looks like? You can check it out <a href="https://github.com/hypebright/shescores-dashboard/blob/0cd4e3f3ae52bcf4a39f7d63fb26e555de9a6b5e/_brand.yml" target="_blank" rel="noopener">here</a>
.</p>
</blockquote>
</div>
</div>
<p>You can ask the diamonds dataset some surprisingly rich questions, and <code>querychat</code> handles them with ease. A simple place to begin is something like &ldquo;show the 10 most expensive diamonds&rdquo;. It produces straightforward SQL and updates the table in the app instantly.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">diamonds</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">price</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>The question may be simple, but it already highlights the convenience of <code>querychat</code>. Without it, users would need to sort the table manually or rely on a picker, slider or some other input that filters the data for this very specific request.</p>
<p>Things get more interesting when we introduce a calculation. Asking &ldquo;can you show the 20 biggest diamonds, based on volume?&rdquo; still results in simple SQL, but the output now includes an extra column, volume, which appears neatly in the app.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Calculate volume (x * y * z) and sort by largest volume
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="p">,</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="n">x</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">z</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">volume</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">diamonds</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">volume</span><span class="w"> </span><span class="k">DESC</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="k">LIMIT</span><span class="w"> </span><span class="mi">20</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>From there, we can try some grouping and window functions. &ldquo;Within each cut, what is the most expensive diamond?&rdquo; works perfectly, showing the grouped results along with the necessary window function behind the scenes.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Find the most expensive diamond for each cut type
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">cut</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">MAX</span><span class="p">(</span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">max_price</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">-- Get the details of the diamond with the max price for each cut
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ARG_MAX</span><span class="p">(</span><span class="n">carat</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">carat</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ARG_MAX</span><span class="p">(</span><span class="n">color</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">color</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ARG_MAX</span><span class="p">(</span><span class="n">clarity</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">clarity</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ARG_MAX</span><span class="p">(</span><span class="n">depth</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">depth</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ARG_MAX</span><span class="p">(</span><span class="s2">&#34;table&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">table_pct</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">diamonds</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">cut</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">max_price</span><span class="w"> </span><span class="k">DESC</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">        cut max_price carat color clarity depth table_pct
</span></span><span class="line"><span class="cl">1   Premium     18823  2.29     I     VS2  60.8        60
</span></span><span class="line"><span class="cl">2 Very Good     18818  2.00     G     SI1  63.5        56
</span></span><span class="line"><span class="cl">3     Ideal     18806  1.51     G      IF  61.7        55
</span></span><span class="line"><span class="cl">4      Good     18788  2.80     G     SI2  63.8        58
</span></span><span class="line"><span class="cl">5      Fair     18574  2.01     G     SI1  70.6        64
</span></span></code></pre></td></tr></table>
</div>
</div><p>Now, for a grand finale, we can throw in a more analytical question: &ldquo;are larger physical dimensions always associated with higher price?&rdquo; This one generates a slightly more complex SQL query, but it also comes with a clear and helpful explanation.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Calculate correlation between physical dimensions and price
</span></span></span><span class="line"><span class="cl"><span class="c1">-- Also look for outliers where small diamonds cost more than large ones
</span></span></span><span class="line"><span class="cl"><span class="k">WITH</span><span class="w"> </span><span class="n">dimension_stats</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">SELECT</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">carat</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">x</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="p">,</span><span class="w"> </span><span class="n">z</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">-- Calculate volume as a proxy for overall size
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">x</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">z</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">volume</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">price</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">-- Rank by volume and price separately
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">RANK</span><span class="p">()</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">z</span><span class="w"> </span><span class="k">DESC</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">volume_rank</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">RANK</span><span class="p">()</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">price</span><span class="w"> </span><span class="k">DESC</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">price_rank</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">FROM</span><span class="w"> </span><span class="n">diamonds</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">WHERE</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">z</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w">  </span><span class="c1">-- Exclude malformed records
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">CORR</span><span class="p">(</span><span class="n">x</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">x_price_correlation</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">CORR</span><span class="p">(</span><span class="n">y</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">y_price_correlation</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">CORR</span><span class="p">(</span><span class="n">z</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">z_price_correlation</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">CORR</span><span class="p">(</span><span class="n">volume</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">volume_price_correlation</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c1">-- Count cases where rank differences are significant (outliers)
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">COUNT</span><span class="p">(</span><span class="k">CASE</span><span class="w"> </span><span class="k">WHEN</span><span class="w"> </span><span class="n">volume_rank</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">price_rank</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">1000</span><span class="w"> </span><span class="k">THEN</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">END</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">large_cheap_diamonds</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">COUNT</span><span class="p">(</span><span class="k">CASE</span><span class="w"> </span><span class="k">WHEN</span><span class="w"> </span><span class="n">price_rank</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">volume_rank</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">1000</span><span class="w"> </span><span class="k">THEN</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">END</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">small_expensive_diamonds</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">dimension_stats</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">x_price_correlation        : 0.8872314
</span></span><span class="line"><span class="cl">y_price_correlation        : 0.8678642
</span></span><span class="line"><span class="cl">z_price_correlation        : 0.8682064
</span></span><span class="line"><span class="cl">volume_price_correlation   : 0.9042547
</span></span><span class="line"><span class="cl">large_cheap_diamonds       : 21082
</span></span><span class="line"><span class="cl">small_expensive_diamonds   : 21065
</span></span></code></pre></td></tr></table>
</div>
</div><p>The conclusion?</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">Answer: No, larger physical dimensions are NOT always associated with higher price
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">While there&#39;s a strong positive correlation (~0.90 for overall volume), quality factors can override size.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Correlation Analysis
</span></span><span class="line"><span class="cl">Volume ↔ Price: 0.90 correlation
</span></span><span class="line"><span class="cl">X dimension ↔ Price: 0.89 correlation
</span></span><span class="line"><span class="cl">Y dimension ↔ Price: 0.87 correlation
</span></span><span class="line"><span class="cl">Z dimension ↔ Price: 0.87 correlation
</span></span><span class="line"><span class="cl">This means physical size explains about 80-90% of price variation, but not all of it.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The Key Finding
</span></span><span class="line"><span class="cl">I found over 21,000 cases where small diamonds are more expensive than much larger ones, and vice versa. This happens because quality factors (cut, color, clarity) matter significantly.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Concrete Example
</span></span><span class="line"><span class="cl">Here&#39;s a smaller diamond that costs more per unit volume than typical large diamonds:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Small, premium diamond: 74mm³ volume, 1.41 carat, $9,752 (Ideal cut, H color, VS1 clarity)
</span></span><span class="line"><span class="cl">This costs $131 per mm³ of volume
</span></span><span class="line"><span class="cl">Compare this to larger diamonds with lower quality that sell for much less per unit volume due to poor cut, color, or clarity grades.
</span></span></code></pre></td></tr></table>
</div>
</div><p>To encourage further exploration, <code>querychat</code> presents suggestions such as comparing how cut quality affects the price to carat ratio, finding diamonds where clarity has the biggest impact on price, or checking the price difference between the best and worst colour grades for similar sized stones. Yeah, that&rsquo;s right, you don&rsquo;t even have to come up with questions yourself.</p>
<p>Some questions result in a filtered table, others result in an explanation with results in the chat window. <code>querychat</code> figures out, based on your question, whether you want an answer straight away, or want to inspect the filtered data yourself. Pretty cool that this only took a few lines of code.</p>
<blockquote>
<p><strong>LLMs can make mistakes</strong></p>
<p>Note that it&rsquo;s still an LLM that generates these queries. LLMs can make mistakes. The nice thing about <code>querychat</code> though, is that you can inspect the SQL query yourself.</p>
<p>Most issues fall into two categories:</p>
<ul>
<li>Query errors: the SQL may fail to run or may not fully reflect what you intended. When it fails, the model will often try again. In this case, giving more context about the data can help.</li>
<li>Result errors: even when the query is correct, the model may misunderstand or oversimplify the results, especially if that result is large or complex. The result might be that key insights are missed or misinterpreted.</li>
</ul>
</blockquote>
<h1 id="why-this-matters-reliability-transparency-reproducibility">Why this matters: reliability, transparency, reproducibility
</h1>
<p>What makes the &ldquo;quick launch&rdquo; app so powerful is that it is far more than a chat window sitting on top of a dataset. Think back to the questions we explored earlier. We filtered, sorted, computed new columns, grouped data and used window functions. We also looked at analytical relationships without writing a single line of code. And that is only the beginning. If you want to go further, you can hunt for anomalies, create categories, build benchmarks or explore almost any analysis you can imagine. The key is that you never have to think about <em>how</em> to do it. You just ask.</p>
<p>And yes, you could ask all those questions in a typical LLM chat tool. But <code>querychat</code> is different. You are not relying on the model to <em>invent</em> answers or reason about the data internally. Instead, every single question is translated into SQL, executed on the actual dataset and returned exactly as the data dictates. And crucially, the SQL is always shown, so you can see precisely what is being run.</p>
<p>This brings four important benefits:</p>
<ul>
<li><strong>Reliability:</strong> the LLM does not analyse or transform the raw data itself. It only generates SQL text. <code>querychat</code> handles the execution of that SQL via tool calling so all results come from the real data engine, not from the model&rsquo;s internal guesswork.</li>
<li><strong>Transparency:</strong> every query reveals the full SQL statement. Nothing is hidden, nothing is adjusted, and you always know how the answer was produced.</li>
<li><strong>Reproducibility:</strong> since every SQL query is visible, analyses can be reused, shared, and audited.</li>
<li><strong>Safety</strong>: <code>querychat</code>&rsquo;s tools are designed with read-only actions in mind, meaning the LLM is essentially unable to perform destructive actions. However, to fully guarantee no destructive actions on your production database, make sure <code>querychat</code>&rsquo;s database permissions are read-only!</li>
</ul>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-6" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-6-1">Python</a></li>
<li><a href="#tabset-6-2">R</a></li>
</ul>
<div id="tabset-6-1">
<img src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/diamonds-drop-py.png" style="width:50.0%" data-fig-align="center" />
</div>
<div id="tabset-6-2">
<img src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/diamonds-drop-r.png" style="width:50.0%" data-fig-align="center" />
</div>
</div>
<h1 id="how-it-works-tool-calling">How it works: tool calling
</h1>
<p>If you read <a href="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/../../../blog/shiny/shiny-side-of-llms-part-2/#when-llms-guess-tools-know">The Shiny Side of LLMs</a>
 blog series, you already know a bit about tool calling. In that series we explored how LLMs can call external tools instead of trying to do everything themselves, and <code>querychat</code> is a very practical example of this idea in action.</p>
<p>Tool calling is essentially a bridge between an LLM and your Python or R session. The model does not execute code. Instead, it requests your Python or R session execute a certain function with certain inputs (e.g., a SQL statement). Once Python or R performs the execution, the result is then passed back to the model for interpretation.</p>
<p>So how does tool calling help us here? Well, LLMs have their strengths and weaknesses. They are not great at counting things, creating data summaries or doing basic calculations. But they <em>are</em> excellent at taking natural language and turning it into structured code. SQL that is. This SQL is then executed through a tool call: a function that executes the (read only) SQL. In both Python and R this means the LLM can express your question as a request to call a function with precise arguments, and the host language performs the real work. This makes it all reliable, reproducible, and safe (read only SQL).</p>
<p>Given that, generally speaking, LLMs are very good at writing SQL, it makes perfect sense to ask one to translate your natural language questions into SQL queries. In order to generate SQL that can be executed, the LLM does need to know something about your data: which columns are there, what do they mean, and what type are they? This <strong>schema information</strong> is shared with the model, but not the raw data. With this information, it produces an SQL query as a tool call. Now, to run SQL you need a database engine. <code>querychat</code>&rsquo;s weapon of choice: <a href="https://duckdb.org" target="_blank" rel="noopener">DuckDB</a>
. Basically, our diamonds dataset gets turned into a DuckDB database, and generated SQL queries are executed on this database. Then the results are passed back to the LLM so it can say some interesting things about it.</p>
<p>To summarise:</p>
<p>prompt → SQL query → tool call → execute SQL query → return results<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>Tool calling is worth emphasising because it gives us a controlled and predictable interface between LLMs and real code execution. Instead of writing and maintaining your own custom tools, you can turn to <code>querychat</code>. It already provides the functions needed to turn natural language into reliable SQL that Python or R can execute with confidence.</p>
<h1 id="customising-querychat-from-chat-to-toolkit">Customising <code>querychat</code>: from chat to toolkit
</h1>
<p>Alright, enough talking. You now know what <code>querychat</code> can do, and how it does it (high-level). You might even have brilliant ideas for your next app&hellip; In that case it would be nice to know how to build your own app with <code>querychat</code>. The Diamonds &ldquo;quick launch&rdquo; app from earlier, that you run with <code>qc.app()</code> (Python) or <code>qc$app()</code> (R), consists of a handful of methods that you can find in <code>querychat</code>, and we&rsquo;re going to use them directly.</p>
<p>The main component is the <code>QueryChat</code> object, which has different arguments and methods.</p>
<h2 id="querychat-object">QueryChat object
</h2>
<p>You call <code>QueryChat</code> to initialise a <code>QueryChat</code> object (often called <code>qc</code>), like so:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-7" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-7-1">Python</a></li>
<li><a href="#tabset-7-2">R</a></li>
</ul>
<div id="tabset-7-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-7-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span><span class="kc">...</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>You can pass <code>QueryChat</code> several arguments:</p>
<ul>
<li>
<p><code>data_source</code> and <code>table_name</code></p>
<p>These are the two most important arguments: they specify your data source and the name of your table that can be used for the SQL queries. The data source can be your data frame, a tibble, a table or any other Python or R data object, and the table name is usually the variable name of your data frame. In our example our <code>data_source</code> was <code>diamonds</code>, which we also stored in a variable called <code>diamonds</code>.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-8" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-8-1">Python</a></li>
<li><a href="#tabset-8-2">R</a></li>
</ul>
<div id="tabset-8-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="n">diamonds</span><span class="p">,</span> <span class="s2">&#34;diamonds&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-8-2">
<p>Generally, in R, the table name isn&rsquo;t required as it can be inferred from the variable name. However, it is required when you use a database connection, which we&rsquo;ll use later.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>You&rsquo;re not limited to data objects: you can also pass a database connection to <code>data_source</code>. We&rsquo;ll come back to that later.</p>
</li>
<li>
<p><code>client</code></p>
<p>We used the <code>client</code> argument before: we use it to tell <code>querychat</code> that we want to use Claude Sonnet 4.5 (or any other model). This gets us back at the starting point of our Diamonds &ldquo;quick launch&rdquo; app.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-9" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-9-1">Python</a></li>
<li><a href="#tabset-9-2">R</a></li>
</ul>
<div id="tabset-9-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="n">diamonds</span><span class="p">,</span> <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span> <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Alternatively, you can set the client in options the <code>QUERYCHAT_CLIENT</code> environment variable.</p>
</div>
<div id="tabset-9-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Alternatively, you can set the client in options with <code>options(querychat.client = &quot;claude/claude-sonnet-4-5&quot;)</code>.</p>
</div>
</div>
</li>
<li>
<p><code>id</code></p>
<p>This is an optional argument, and if it&rsquo;s not given it&rsquo;s derived from the <code>table_name</code>. When to use it? If you want to work with <a href="https://posit-dev.github.io/querychat/py/build.html#multiple-datasets" target="_blank" rel="noopener">multiple QueryChat instances</a>
, for example.</p>
</li>
<li>
<p><code>greeting</code></p>
<p>A nice greeting message to display to your users. It&rsquo;s the first thing your users see, so you better make it good! If not provided, one is generated at the start. While this one looks fine on first sight, it&rsquo;s rather slow and wasteful (it costs extra tokens because it&rsquo;s generated every single time). Also, because it&rsquo;s generated on the fly, it&rsquo;s far from consistent. Earlier, when we ran the &ldquo;quick launch&rdquo; app, you already might have noticed that it generated a warning message:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-10" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-10-1">Python</a></li>
<li><a href="#tabset-10-2">R</a></li>
</ul>
<div id="tabset-10-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">Warning: No greeting provided; the LLM will be invoked at conversation start to generate one. For faster startup, lower cost, and determinism, please save a greeting and pass it to init(). You can also use `querychat.greeting()` to help generate a greeting.
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-10-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">Warning message:
</span></span><span class="line"><span class="cl">No greeting provided; the LLM will be invoked at conversation start to generate one.
</span></span><span class="line"><span class="cl">• For faster startup, lower cost, and determinism, please save a greeting and pass it to QueryChat$new().
</span></span><span class="line"><span class="cl">ℹ You can generate a greeting with $generate_greeting(). 
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>So yes, we need a greeting! You can add your own greeting by providing a string in Markdown format.</p>
<p>Some inspiration on what you can put in there: basic instructions, suggestions for filtering, sorting or analysing the data, addressing data privacy concerns, or letting people know where they can get support if something goes wrong.</p>
<p>And if you don&rsquo;t feel like writing your own greeting, or if you feel uninspired, you can let <code>querychat</code> handle it! Simply use <code>generate_greeting()</code>:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-11" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-11-1">Python</a></li>
<li><a href="#tabset-11-2">R</a></li>
</ul>
<div id="tabset-11-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="n">diamonds</span><span class="p">,</span> <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span> <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Generate a greeting with help from the LLM</span>
</span></span><span class="line"><span class="cl"><span class="n">greeting_text</span> <span class="o">=</span> <span class="n">qc</span><span class="o">.</span><span class="n">generate_greeting</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save it</span>
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;diamonds_greeting.md&#34;</span><span class="p">,</span> <span class="s2">&#34;w&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">greeting_text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Then use the saved greeting in your app</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="n">diamonds</span><span class="p">,</span> <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span> <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">,</span> <span class="n">greeting</span><span class="o">=</span><span class="n">Path</span><span class="p">(</span><span class="s2">&#34;diamonds_greeting.md&#34;</span><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Which give us this nice greeting:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gh"># Welcome! 👋
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">I&#39;m here to help you explore and understand your diamonds dataset. 
</span></span><span class="line"><span class="cl">I can filter and sort the data, answer questions with SQL queries, 
</span></span><span class="line"><span class="cl">and help you discover insights.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Here are some ideas to get started:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Explore the data
</span></span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Show me the most expensive diamonds&lt;/span&gt;
</span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;What&#39;s the average price of diamonds by cut quality?&lt;/span&gt;
</span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;How many diamonds are in each clarity category?&lt;/span&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Filter and analyze
</span></span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Show only ideal cut diamonds over 2 carats&lt;/span&gt;
</span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Filter to diamonds with the best color grades (D, E, F)&lt;/span&gt;
</span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Which cut has the highest average price per carat?&lt;/span&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">What would you like to explore first?
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-11-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Generate a greeting with help from the LLM</span>
</span></span><span class="line"><span class="cl"><span class="n">greeting_text</span> <span class="o">&lt;-</span> <span class="n">qc</span><span class="o">$</span><span class="nf">generate_greeting</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save it</span>
</span></span><span class="line"><span class="cl"><span class="nf">writeLines</span><span class="p">(</span><span class="n">greeting_text</span><span class="p">,</span> <span class="s">&#34;diamonds_greeting.md&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Then use the saved greeting in your app</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">greeting</span> <span class="o">=</span> <span class="s">&#34;diamonds_greeting.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Which give us this nice greeting:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gh"># Welcome to the Diamond Dashboard! 💎
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">I&#39;m here to help you explore and analyze this dataset of diamond characteristics and prices. 
</span></span><span class="line"><span class="cl">I can filter and sort the data, answer questions, and help you discover interesting patterns.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Here are some ideas to get started:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Explore the Data**</span>
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;What&#39;s the average price of diamonds in this dataset?&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;How many diamonds are there in each clarity category?&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Which diamond has the highest price?&lt;/span&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Filter and Sort**</span>
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Show me only Ideal cut diamonds&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Filter to diamonds over 2 carats and sort by price&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Show me the most expensive diamonds with VS1 clarity&lt;/span&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">What would you like to explore?
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>You can see that the generated greeting contains a span HTML tag: <code>&lt;span class=&quot;suggestion&quot;&gt;…&lt;/span&gt;</code>. If you make your own greeting, you can use this tag to automatically populate the chatbox when it&rsquo;s being clicked.</p>
</li>
<li>
<p><code>data_description</code></p>
<p><code>querychat</code> automatically helps the LLM by providing things like the column names and datatypes of your data (the <strong>schema information</strong>), but results can be even more accurate when you provide additional context in the data description. There&rsquo;s no specific format needed, and you can add whatever information you like. To give some inspiration, this is what we could say about the diamonds dataset:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gh"># Diamonds Dataset Description
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">A structured dataset describing physical and quality attributes of individual diamonds,
</span></span><span class="line"><span class="cl">commonly used to model or predict price.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Fields
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> carat (float) — Diamond weight  
</span></span><span class="line"><span class="cl"><span class="k">-</span> cut (category) — Cut quality: Fair, Good, Very Good, Premium, Ideal  
</span></span><span class="line"><span class="cl"><span class="k">-</span> color (category) — Color grade from D (best) to J (worst)  
</span></span><span class="line"><span class="cl"><span class="k">-</span> clarity (category) — Clarity grades: I1, SI2, SI1, VS2, VS1, VVS2, VVS1, IF  
</span></span><span class="line"><span class="cl"><span class="k">-</span> depth (float) — Total depth percentage  
</span></span><span class="line"><span class="cl"><span class="k">-</span> table (float) — Table width percentage  
</span></span><span class="line"><span class="cl"><span class="k">-</span> price (int) — Price in USD  
</span></span><span class="line"><span class="cl"><span class="k">-</span> x (float) — Length in mm  
</span></span><span class="line"><span class="cl"><span class="k">-</span> y (float) — Width in mm  
</span></span><span class="line"><span class="cl"><span class="k">-</span> z (float) — Depth in mm
</span></span></code></pre></td></tr></table>
</div>
</div><p>We can save this in a Markdown file and pass it on to <code>querychat</code>:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-12" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-12-1">Python</a></li>
<li><a href="#tabset-12-2">R</a></li>
</ul>
<div id="tabset-12-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">greeting</span><span class="o">=</span><span class="n">Path</span><span class="p">(</span><span class="s2">&#34;diamonds_greeting.md&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_description</span><span class="o">=</span><span class="n">Path</span><span class="p">(</span><span class="s2">&#34;diamonds_data_description.md&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-12-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">greeting</span> <span class="o">=</span> <span class="s">&#34;diamonds_greeting.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">data_description</span> <span class="o">=</span> <span class="s">&#34;diamonds_data_description.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
</li>
<li>
<p><code>extra_instructions</code></p>
<p>For further tweaking the LLMs behaviour you can use <code>extra_instructions</code>. You can go nuts here: make it talk like a pirate, use an emoji in every sentence, or use an annoying amount of diamond-related phrases. You can also use this section for more practical guidance like notes on preferred spelling, tone, or handling of sensitive terms. For example:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="k">-</span> Assume the user doesn&#39;t know much about diamonds: 
</span></span><span class="line"><span class="cl">  keep explanations simple and accessible.
</span></span><span class="line"><span class="cl"><span class="k">-</span> When describing diamond attributes, default to plain English. 
</span></span><span class="line"><span class="cl">  If a term is highly technical, include a short clarification.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Maintain consistent spelling in British English.
</span></span></code></pre></td></tr></table>
</div>
</div><div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-13" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-13-1">Python</a></li>
<li><a href="#tabset-13-2">R</a></li>
</ul>
<div id="tabset-13-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">greeting</span><span class="o">=</span><span class="n">Path</span><span class="p">(</span><span class="s2">&#34;diamonds_greeting.md&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_description</span><span class="o">=</span><span class="n">Path</span><span class="p">(</span><span class="s2">&#34;diamonds_data_description.md&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">extra_instructions</span><span class="o">=</span><span class="n">Path</span><span class="p">(</span><span class="s2">&#34;diamonds_extra_instructions.md&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-13-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">greeting</span> <span class="o">=</span> <span class="s">&#34;diamonds_greeting.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">data_description</span> <span class="o">=</span> <span class="s">&#34;diamonds_data_description.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">extra_instructions</span> <span class="o">=</span> <span class="s">&#34;diamonds_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
</li>
<li>
<p><code>categorical_threshold</code></p>
<p>This threshold applies to text columns, and sets the maximum number of unique values to consider it as a categorical variable. The default is 20.</p>
</li>
<li>
<p><code>prompt_template</code></p>
<p>The <code>prompt_template</code> is a more advanced parameter to provide a custom prompt template. If you don&rsquo;t provide it, <code>querychat</code> will use the built-in prompt, which we&rsquo;ll inspect a little bit closer later.</p>
</li>
</ul>
<p>Besides arguments, you can also call methods on the <code>QueryChat</code> object. One of them is <code>cleanup()</code>, which releases any resources (e.g. database connections) associated with the data source. You should call this when you are done using the <code>QueryChat</code> object to avoid resource leaks:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-14" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-14-1">Python</a></li>
<li><a href="#tabset-14-2">R</a></li>
</ul>
<div id="tabset-14-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span><span class="o">.</span><span class="n">cleanup</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-14-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span><span class="o">$</span><span class="nf">cleanup</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>That&rsquo;s&hellip; A lot! And all you need to chat safely with your data. As you&rsquo;ve seen in our earlier examples, you don&rsquo;t need a lot to get started (<code>data_source</code> and <code>table_name</code> are enough, and in R you can even omit the <code>table_name</code>). But knowing the possibilities makes it easier to customise <code>querychat</code> to your liking.</p>
<h1 id="beyond-chat-bespoke-interfaces">Beyond chat: bespoke interfaces
</h1>
<p>Now you know everything there is to know about the <code>QueryChat</code> object. You know how to add a greeting, additional context, and your favourite LLM. However, it&rsquo;s time to dream bigger and time to get building! Because chatting with your data safely is one thing, but if you truly want to amaze your users you can build an entire dashboard around it. Plots, maps, tables, and value boxes that all update based on the user&rsquo;s questions. Your own bespoke interface. Before we dive into that, let&rsquo;s first take a step back and see if we can reconstruct the &ldquo;quick launch&rdquo; app.</p>
<p>You need two things if you want to build a Shiny app with <code>querychat</code>:</p>
<ul>
<li>The UI component (the chat window)</li>
<li>A server method that deals with the results</li>
</ul>
<p>For the UI component, there are two choices: <code>sidebar()</code> or <code>ui()</code>. The difference? <code>ui</code> creates a basic chat interface, while <code>sidebar</code> wraps the chat interface in a (<code>bslib</code>, for the R lovers) sidebar component designed to be used as the <code>sidebar</code> argument to <code>page_sidebar</code>.</p>
<p>If we want to do something with the results that get returned by <code>querychat</code>, we need to make use of the <code>server()</code> method. The server method returns:</p>
<ul>
<li><code>sql</code>: a reactive that returns the current SQL query. And, if you want to run your own queries, you can also call the <code>$sql()</code> method on the <code>QueryChat</code> object to run queries.</li>
<li><code>title</code>: a reactive that returns the current title.</li>
<li><code>df</code>: a reactive that returns the data frame, filtered and sorted by the current SQL query.</li>
</ul>
<p>Let&rsquo;s take a look at a minimal example that rebuilds the &ldquo;quick launch&rdquo; app:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-15" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-15-1">Python</a></li>
<li><a href="#tabset-15-2">R</a></li>
</ul>
<div id="tabset-15-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span><span class="lnt">55
</span><span class="lnt">56
</span><span class="lnt">57
</span><span class="lnt">58
</span><span class="lnt">59
</span><span class="lnt">60
</span><span class="lnt">61
</span><span class="lnt">62
</span><span class="lnt">63
</span><span class="lnt">64
</span><span class="lnt">65
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny</span> <span class="kn">import</span> <span class="n">App</span><span class="p">,</span> <span class="n">render</span><span class="p">,</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">seaborn</span> <span class="kn">import</span> <span class="n">load_dataset</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">querychat</span> <span class="kn">import</span> <span class="n">QueryChat</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Initialize QueryChat with custom files</span>
</span></span><span class="line"><span class="cl"><span class="n">diamonds</span> <span class="o">=</span> <span class="n">load_dataset</span><span class="p">(</span><span class="s2">&#34;diamonds&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">diamonds_greeting</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;diamonds_greeting.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">diamonds_data_description</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;diamonds_data_description.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">diamonds_extra_instructions</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;diamonds_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">greeting</span><span class="o">=</span><span class="n">diamonds_greeting</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_description</span><span class="o">=</span><span class="n">diamonds_data_description</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">extra_instructions</span><span class="o">=</span><span class="n">diamonds_extra_instructions</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># UI</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">app_ui</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">page_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 2. QueryChat sidebar UI component</span>
</span></span><span class="line"><span class="cl">    <span class="n">qc</span><span class="o">.</span><span class="n">sidebar</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">card</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">card_header</span><span class="p">(</span><span class="s2">&#34;SQL Query&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">output_text_verbatim</span><span class="p">(</span><span class="s2">&#34;sql_output&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">fill</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">card</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">card_header</span><span class="p">(</span><span class="n">ui</span><span class="o">.</span><span class="n">output_text</span><span class="p">(</span><span class="s2">&#34;title&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">output_data_frame</span><span class="p">(</span><span class="s2">&#34;data_table&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">fill</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">fillable</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">theme</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">Theme</span><span class="o">.</span><span class="n">from_brand</span><span class="p">(</span><span class="vm">__file__</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">server</span><span class="p">(</span><span class="nb">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 3. QueryChat server component</span>
</span></span><span class="line"><span class="cl">    <span class="n">vals</span> <span class="o">=</span> <span class="n">qc</span><span class="o">.</span><span class="n">server</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 4. Use the filtered/sorted data frame reactively</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@render.data_frame</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">data_table</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">vals</span><span class="o">.</span><span class="n">df</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@render.text</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">title</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">vals</span><span class="o">.</span><span class="n">title</span><span class="p">()</span> <span class="ow">or</span> <span class="s2">&#34;Diamonds&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 5. Display the generated SQL query</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@render.text</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">sql_output</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">vals</span><span class="o">.</span><span class="n">sql</span><span class="p">()</span> <span class="ow">or</span> <span class="s2">&#34;SELECT * FROM diamonds;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>To keep things simple, we opted for a simple verbatim text output, but we also could&rsquo;ve chosen for this combination, which is from the <a href="https://posit-dev.github.io/shinychat/py/" target="_blank" rel="noopener"><code>shinychat</code></a>
 package:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">ui</span><span class="o">.</span><span class="n">output_ui</span><span class="p">(</span><span class="s2">&#34;sql_output&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@render.ui</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">sql_output</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">sql_value</span> <span class="o">=</span> <span class="n">vals</span><span class="o">.</span><span class="n">sql</span><span class="p">()</span> <span class="ow">or</span> <span class="sa">f</span><span class="s2">&#34;SELECT * FROM </span><span class="si">{</span><span class="n">table_name</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sql_code</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;```sql</span><span class="se">\n</span><span class="si">{</span><span class="n">sql_value</span><span class="si">}</span><span class="se">\n</span><span class="s2">```&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">output_markdown_stream</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;sql_code&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">content</span><span class="o">=</span><span class="n">sql_code</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">auto_scroll</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">width</span><span class="o">=</span><span class="s2">&#34;100%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This actually happens in the source code for the quick launch app. It would give us the nice &ldquo;copy to clipboard&rdquo; feature and nice formatting. Another alternative would be the native <a href="https://shiny.posit.co/py/api/core/ui.output_markdown_stream.html" target="_blank" rel="noopener">markdown stream component in Shiny</a>
.</p>
<blockquote>
<p><strong>brand.yml</strong></p>
<p>If you want to make use of brand.yml, you need to add a theme argument: <code>theme=ui.Theme.from_brand(**file**)</code>. Make sure you have installed the latest version of shiny with the <code>theme</code> extra! You can simply add it with: <code>uv add &quot;shiny[theme]&quot;</code> (if using <code>uv</code>), or <code>pip install &quot;shiny[theme]&quot;</code></p>
</blockquote>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/diamonds-bespoke-py.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
<div id="tabset-15-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span><span class="lnt">55
</span><span class="lnt">56
</span><span class="lnt">57
</span><span class="lnt">58
</span><span class="lnt">59
</span><span class="lnt">60
</span><span class="lnt">61
</span><span class="lnt">62
</span><span class="lnt">63
</span><span class="lnt">64
</span><span class="lnt">65
</span><span class="lnt">66
</span><span class="lnt">67
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shiny</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">bslib</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">DT</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">querychat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ellmer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ggplot2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Initialize QueryChat with custom files</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">greeting</span> <span class="o">=</span> <span class="s">&#34;diamonds_greeting.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">data_description</span> <span class="o">=</span> <span class="s">&#34;diamonds_data_description.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">extra_instructions</span> <span class="o">=</span> <span class="s">&#34;diamonds_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># UI</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="nf">page_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;Diamonds Explorer&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># 2. QueryChat sidebar UI component</span>
</span></span><span class="line"><span class="cl">  <span class="n">sidebar</span> <span class="o">=</span> <span class="n">qc</span><span class="o">$</span><span class="nf">sidebar</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">  <span class="nf">card</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">card_header</span><span class="p">(</span><span class="s">&#34;SQL Query&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="nf">verbatimTextOutput</span><span class="p">(</span><span class="s">&#34;sql_query&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="nf">card</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">card_header</span><span class="p">(</span><span class="nf">textOutput</span><span class="p">(</span><span class="s">&#34;title&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">    <span class="n">DT</span><span class="o">::</span><span class="nf">DTOutput</span><span class="p">(</span><span class="s">&#34;data_table&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># 3. QueryChat server component</span>
</span></span><span class="line"><span class="cl">  <span class="n">vals</span> <span class="o">&lt;-</span> <span class="n">qc</span><span class="o">$</span><span class="nf">server</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 3. Display generated SQL query</span>
</span></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">sql_query</span> <span class="o">&lt;-</span> <span class="nf">renderText</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="kr">if</span> <span class="p">(</span><span class="nf">is.null</span><span class="p">(</span><span class="n">vals</span><span class="o">$</span><span class="nf">sql</span><span class="p">()))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="kr">return</span><span class="p">(</span><span class="s">&#34;SELECT * FROM diamonds;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">vals</span><span class="o">$</span><span class="nf">sql</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 4. Display data table based on user query</span>
</span></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">data_table</span> <span class="o">&lt;-</span> <span class="n">DT</span><span class="o">::</span><span class="nf">renderDT</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="n">vals</span><span class="o">$</span><span class="nf">df</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 5. Dynamic title based on user query</span>
</span></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">title</span> <span class="o">&lt;-</span> <span class="nf">renderText</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="kr">if</span> <span class="p">(</span><span class="nf">is.null</span><span class="p">(</span><span class="n">vals</span><span class="o">$</span><span class="nf">title</span><span class="p">()))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="kr">return</span><span class="p">(</span><span class="s">&#34;Diamonds Data&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">vals</span><span class="o">$</span><span class="nf">title</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/diamonds-bespoke-r.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
</div>
<p>Looks pretty similar to the quick launch app, right?! So that&rsquo;s how it was build. Note that there a few aesthetic differences though. The quick launch app has a few extra sparks here and there, and our app makes use of custom theming with <code>brand.yml</code>.</p>
<p>So far in our diamonds adventure we have only looked at a simple table, but we can extent this idea much further and build an entire dashboard around it: value boxes, graphs, tables, maps, you name it! This is also what <a href="https://shiny.posit.co/py/templates/sidebot/" target="_blank" rel="noopener">sidebot</a>
 does, and this template is available to get you started quickly. A nice touch is the inclusion of the ✨ icon, which sends a screenshot of the visuals to the LLM for an explanation. How cool is that!</p>
<h1 id="adding-querychat-to-your-existing-shiny-app">Adding querychat to your existing Shiny app
</h1>
<p>The idea of <a href="https://shiny.posit.co/py/templates/sidebot/" target="_blank" rel="noopener">sidebot</a>
 is certainly interesting: why build a dashboard with all kind of filters when you can just add a chat window with access to a smart LLM. You ask it questions, <code>querychat</code> returns some SQL and reactive filtered data, and you make sure you update the entire dashboard. Unlimited filter possibilities. And it doesn&rsquo;t have to be complicated to achieve that.</p>
<p>To demonstrate how easy it is, we are going to use an existing dashboard (SheScores), that currently has a number of filters in it: a slider for the year(s), a dropdown for the continent where the matches took place, the tournaments that took place on those continents, and a switch that filters the data to include only data with known scorers, or not.</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/shescores-original-py.gif" alt="Python version of SheScores" />
<figcaption aria-hidden="true">Python version of SheScores</figcaption>
</figure>
<p>So what does SheScores look like behind the scenes? We&rsquo;re not going into the nitty gritty details of the SheScores dashboard, and we don&rsquo;t have to if we want to add <code>querychat</code> to it. The most important bit of logic is stored in a reactive that contains the filtered data. It reacts to changes in any of the inputs (year, continent, tournament, scorer only or not).</p>
<p>The reactive, <code>filtered_data()</code>, forms the basis for all the elements in the dashboard: the value boxes, the map, the graph, and the table.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-16" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-16-1">Python</a></li>
<li><a href="#tabset-16-2">R</a></li>
</ul>
<div id="tabset-16-1">
<blockquote>
<p><strong>Tip</strong></p>
<p>See <a href="https://github.com/hypebright/shescores-dashboard/blob/68f34785f3217d005497f4719b1f5c64af00ac4d/Python/shescores-app.py" target="_blank" rel="noopener">GitHub</a>
 for the full source code.</p>
</blockquote>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span><span class="lnt">55
</span><span class="lnt">56
</span><span class="lnt">57
</span><span class="lnt">58
</span><span class="lnt">59
</span><span class="lnt">60
</span><span class="lnt">61
</span><span class="lnt">62
</span><span class="lnt">63
</span><span class="lnt">64
</span><span class="lnt">65
</span><span class="lnt">66
</span><span class="lnt">67
</span><span class="lnt">68
</span><span class="lnt">69
</span><span class="lnt">70
</span><span class="lnt">71
</span><span class="lnt">72
</span><span class="lnt">73
</span><span class="lnt">74
</span><span class="lnt">75
</span><span class="lnt">76
</span><span class="lnt">77
</span><span class="lnt">78
</span><span class="lnt">79
</span><span class="lnt">80
</span><span class="lnt">81
</span><span class="lnt">82
</span><span class="lnt">83
</span><span class="lnt">84
</span><span class="lnt">85
</span><span class="lnt">86
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;data/results_with_scorers.csv&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">to_datetime</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">=</span> <span class="n">results_with_scorers</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;tournament&#34;</span><span class="p">]</span> <span class="o">!=</span> <span class="s2">&#34;Friendly&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">&amp;</span> <span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="s2">&#34;2000-01-01&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># UI</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">app_ui</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">page_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">h4</span><span class="p">(</span><span class="s2">&#34;Filters&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">input_slider</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;year_filter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Select year range:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nb">min</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dt</span><span class="o">.</span><span class="n">year</span><span class="o">.</span><span class="n">min</span><span class="p">()),</span>
</span></span><span class="line"><span class="cl">            <span class="nb">max</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dt</span><span class="o">.</span><span class="n">year</span><span class="o">.</span><span class="n">max</span><span class="p">()),</span>
</span></span><span class="line"><span class="cl">            <span class="n">value</span><span class="o">=</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">                <span class="nb">int</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dt</span><span class="o">.</span><span class="n">year</span><span class="o">.</span><span class="n">min</span><span class="p">()),</span>
</span></span><span class="line"><span class="cl">                <span class="nb">int</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dt</span><span class="o">.</span><span class="n">year</span><span class="o">.</span><span class="n">max</span><span class="p">()),</span>
</span></span><span class="line"><span class="cl">            <span class="p">],</span>
</span></span><span class="line"><span class="cl">            <span class="n">sep</span><span class="o">=</span><span class="s2">&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">input_selectize</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;continent_filter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Select continents:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">choices</span><span class="o">=</span><span class="nb">sorted</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;continent&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dropna</span><span class="p">()</span><span class="o">.</span><span class="n">unique</span><span class="p">()</span><span class="o">.</span><span class="n">tolist</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">selected</span><span class="o">=</span><span class="s2">&#34;Europe&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">multiple</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">input_select</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;tournament_filter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Select tournaments:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">choices</span><span class="o">=</span><span class="p">[],</span>
</span></span><span class="line"><span class="cl">            <span class="n">selected</span><span class="o">=</span><span class="p">[],</span>
</span></span><span class="line"><span class="cl">            <span class="n">multiple</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">input_switch</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;scorer_only&#34;</span><span class="p">,</span> <span class="s2">&#34;Show matches with scorer data only&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="kc">False</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">width</span><span class="o">=</span><span class="s2">&#34;30%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Other UI content</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span><span class="o">=</span><span class="s2">&#34;She Scores ⚽️: Women&#39;s International Soccer Matches&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">fillable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">theme</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">Theme</span><span class="o">.</span><span class="n">from_brand</span><span class="p">(</span><span class="vm">__file__</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">server</span><span class="p">(</span><span class="nb">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Reactive filtered data based on inputs</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@reactive.calc</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">filtered_data</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">req</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">continent_filter</span><span class="p">())</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">req</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">tournament_filter</span><span class="p">())</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">data</span> <span class="o">=</span> <span class="n">results_with_scorers</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">data</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">            <span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dt</span><span class="o">.</span><span class="n">year</span> <span class="o">&gt;=</span> <span class="nb">input</span><span class="o">.</span><span class="n">year_filter</span><span class="p">()[</span><span class="mi">0</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">            <span class="o">&amp;</span> <span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dt</span><span class="o">.</span><span class="n">year</span> <span class="o">&lt;=</span> <span class="nb">input</span><span class="o">.</span><span class="n">year_filter</span><span class="p">()[</span><span class="mi">1</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">            <span class="o">&amp;</span> <span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;continent&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">isin</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">continent_filter</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">            <span class="o">&amp;</span> <span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;tournament&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">isin</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">tournament_filter</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">        <span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">input</span><span class="o">.</span><span class="n">scorer_only</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">            <span class="n">data</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;scorer&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">notna</span><span class="p">()]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">data</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># Other server logic</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-16-2">
<blockquote>
<p><strong>Tip</strong></p>
<p>Check out the full source code on <a href="https://github.com/hypebright/shescores-dashboard/blob/9c8b20d64adfb67566272c587e158dbf2a5052d8/R/shescores-app.R" target="_blank" rel="noopener">GitHub</a>
.</p>
</blockquote>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span><span class="lnt">55
</span><span class="lnt">56
</span><span class="lnt">57
</span><span class="lnt">58
</span><span class="lnt">59
</span><span class="lnt">60
</span><span class="lnt">61
</span><span class="lnt">62
</span><span class="lnt">63
</span><span class="lnt">64
</span><span class="lnt">65
</span><span class="lnt">66
</span><span class="lnt">67
</span><span class="lnt">68
</span><span class="lnt">69
</span><span class="lnt">70
</span><span class="lnt">71
</span><span class="lnt">72
</span><span class="lnt">73
</span><span class="lnt">74
</span><span class="lnt">75
</span><span class="lnt">76
</span><span class="lnt">77
</span><span class="lnt">78
</span><span class="lnt">79
</span><span class="lnt">80
</span><span class="lnt">81
</span><span class="lnt">82
</span><span class="lnt">83
</span><span class="lnt">84
</span><span class="lnt">85
</span><span class="lnt">86
</span><span class="lnt">87
</span><span class="lnt">88
</span><span class="lnt">89
</span><span class="lnt">90
</span><span class="lnt">91
</span><span class="lnt">92
</span><span class="lnt">93
</span><span class="lnt">94
</span><span class="lnt">95
</span><span class="lnt">96
</span><span class="lnt">97
</span><span class="lnt">98
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">&lt;-</span> <span class="nf">read.csv</span><span class="p">(</span><span class="s">&#34;../data/results_with_scorers.csv&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">filter</span><span class="p">(</span><span class="n">tournament</span> <span class="o">!=</span> <span class="s">&#34;Friendly&#34;</span><span class="p">,</span> <span class="n">date</span> <span class="o">&gt;=</span> <span class="s">&#34;2000-01-01&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Other setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># UI</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="nf">page_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">fillable</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;She Scores ⚽️: Women&#39;s International Soccer Matches&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">sidebar</span> <span class="o">=</span> <span class="nf">sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;Filters&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">width</span> <span class="o">=</span> <span class="s">&#34;30%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Year filter</span>
</span></span><span class="line"><span class="cl">    <span class="nf">sliderInput</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">inputId</span> <span class="o">=</span> <span class="s">&#34;year_filter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">label</span> <span class="o">=</span> <span class="s">&#34;Select year range:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">min</span> <span class="o">=</span> <span class="nf">year</span><span class="p">(</span><span class="nf">min</span><span class="p">(</span><span class="nf">as.Date</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="o">$</span><span class="n">date</span><span class="p">))),</span>
</span></span><span class="line"><span class="cl">      <span class="n">max</span> <span class="o">=</span> <span class="nf">year</span><span class="p">(</span><span class="nf">max</span><span class="p">(</span><span class="nf">as.Date</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="o">$</span><span class="n">date</span><span class="p">))),</span>
</span></span><span class="line"><span class="cl">      <span class="n">value</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="nf">year</span><span class="p">(</span><span class="nf">min</span><span class="p">(</span><span class="nf">as.Date</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="o">$</span><span class="n">date</span><span class="p">))),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">year</span><span class="p">(</span><span class="nf">max</span><span class="p">(</span><span class="nf">as.Date</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="o">$</span><span class="n">date</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="n">sep</span> <span class="o">=</span> <span class="s">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Continent filter (dropdown)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">pickerInput</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">inputId</span> <span class="o">=</span> <span class="s">&#34;continent_filter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">label</span> <span class="o">=</span> <span class="s">&#34;Select continents:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">choices</span> <span class="o">=</span> <span class="nf">sort</span><span class="p">(</span><span class="nf">unique</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="o">$</span><span class="n">continent</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">      <span class="n">selected</span> <span class="o">=</span> <span class="nf">sort</span><span class="p">(</span><span class="nf">unique</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="o">$</span><span class="n">continent</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">      <span class="n">options</span> <span class="o">=</span> <span class="nf">pickerOptions</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">actionsBox</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">selectedTextFormat</span> <span class="o">=</span> <span class="s">&#34;count &gt; 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">countSelectedText</span> <span class="o">=</span> <span class="s">&#34;{0} continents selected&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="n">multiple</span> <span class="o">=</span> <span class="kc">TRUE</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Tournament filter (dropdown)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">pickerInput</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">inputId</span> <span class="o">=</span> <span class="s">&#34;tournament_filter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">label</span> <span class="o">=</span> <span class="s">&#34;Select tournaments:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">choices</span> <span class="o">=</span> <span class="kc">NULL</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">selected</span> <span class="o">=</span> <span class="kc">NULL</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">options</span> <span class="o">=</span> <span class="nf">pickerOptions</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">actionsBox</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">liveSearch</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">liveSearchPlaceholder</span> <span class="o">=</span> <span class="s">&#34;Search for a tournament&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">selectedTextFormat</span> <span class="o">=</span> <span class="s">&#34;count &gt; 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">countSelectedText</span> <span class="o">=</span> <span class="s">&#34;{0} tournaments selected&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="n">multiple</span> <span class="o">=</span> <span class="kc">TRUE</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Switch to show data with scorers only</span>
</span></span><span class="line"><span class="cl">    <span class="nf">input_switch</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">id</span> <span class="o">=</span> <span class="s">&#34;scorer_only&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">label</span> <span class="o">=</span> <span class="s">&#34;Show matches with scorer data only&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">value</span> <span class="o">=</span> <span class="kc">FALSE</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Other UI content</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Reactive filtered data based on inputs</span>
</span></span><span class="line"><span class="cl">  <span class="n">filtered_data</span> <span class="o">&lt;-</span> <span class="nf">reactive</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nf">req</span><span class="p">(</span><span class="nf">length</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">continent_filter</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">req</span><span class="p">(</span><span class="nf">length</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">tournament_filter</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">results_with_scorers</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">mutate</span><span class="p">(</span><span class="n">date</span> <span class="o">=</span> <span class="nf">as.Date</span><span class="p">(</span><span class="n">date</span><span class="p">))</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">filter</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="nf">year</span><span class="p">(</span><span class="n">date</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">input</span><span class="o">$</span><span class="n">year_filter[1]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nf">year</span><span class="p">(</span><span class="n">date</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="n">input</span><span class="o">$</span><span class="n">year_filter[2]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">continent</span> <span class="o">%in%</span> <span class="n">input</span><span class="o">$</span><span class="n">continent_filter</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">tournament</span> <span class="o">%in%</span> <span class="n">input</span><span class="o">$</span><span class="n">tournament_filter</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="kr">if</span> <span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">scorer_only</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="o">!</span><span class="nf">is.na</span><span class="p">(</span><span class="n">scorer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span> <span class="kr">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="kc">TRUE</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Other server logic</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>Now we want to get rid of all those filters. We want a chat window instead. What do we need to change in order to use <code>querychat</code>? Spoiler alert: not much.</p>
<p>Of course we need to initialise our <code>QueryChat</code> object. And since we&rsquo;re not talking about diamonds, we need to make sure to provide a proper soccer-themed greeting, a data description, and extra instructions:</p>
<p><code>shescores_greeting.md</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gh"># Welcome to SheScores! ⚽
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">I&#39;m here to help you explore international women&#39;s soccer match data. 
</span></span><span class="line"><span class="cl">You can ask me to filter and sort the dashboard, answer questions about the data, 
</span></span><span class="line"><span class="cl">or provide insights about teams, players, tournaments, and more.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Here are some ideas to get started:
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Explore match data:**</span>
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Show me the highest-scoring matches in World Cup tournaments&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Which teams have played the most matches against each other?&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Filter to matches from the 2025 UEFA Euro&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Which team has the best win rate in the Canada vs United States rivalry?&lt;/span&gt;
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl"><span class="gs">**Analyze player performance:**</span>
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Who are the top scorers in World Cup history?&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Which players have scored the most penalty goals?&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Which matches had the most own goals?&lt;/span&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">What would you like to explore?
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>shescores_data_description.md</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gh"># Dataset description
</span></span></span><span class="line"><span class="cl">This dataset contains international women’s football match results. 
</span></span><span class="line"><span class="cl">It includes match metadata (date, location, teams), outcomes (scores), 
</span></span><span class="line"><span class="cl">plus optional event-level information such as individual scorers. 
</span></span><span class="line"><span class="cl">Not all friendly matches are represented; 
</span></span><span class="line"><span class="cl">major tournaments are mostly complete.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># Column descriptions
</span></span></span><span class="line"><span class="cl"><span class="k">-</span> date (string, YYYY-MM-DD): The calendar date on which the match was played.
</span></span><span class="line"><span class="cl"><span class="k">-</span> home_team (string): Name of the home team.
</span></span><span class="line"><span class="cl"><span class="k">-</span> date (string, YYYY-MM-DD): The calendar date on which the match was played.
</span></span><span class="line"><span class="cl"><span class="k">-</span> home_team (string): Name of the home team.
</span></span><span class="line"><span class="cl"><span class="k">-</span> away_team (string): Name of the away team.
</span></span><span class="line"><span class="cl"><span class="k">-</span> home_score (integer): Goals scored by the home team at full time (extra time included, - penalty shoot-outs excluded).
</span></span><span class="line"><span class="cl"><span class="k">-</span> away_score (integer): Goals scored by the away team at full time (extra time included, - penalty shoot-outs excluded).
</span></span><span class="line"><span class="cl"><span class="k">-</span> tournament (string): Name of the competition or event.
</span></span><span class="line"><span class="cl"><span class="k">-</span> city (string): City or administrative area where the match was played.
</span></span><span class="line"><span class="cl"><span class="k">-</span> country (string): Country where the match was played.
</span></span><span class="line"><span class="cl"><span class="k">-</span> neutral (boolean): Indicates whether the match took place at a neutral venue.
</span></span><span class="line"><span class="cl"><span class="k">-</span> team (string, optional): Team associated with a recorded scoring event.
</span></span><span class="line"><span class="cl"><span class="k">-</span> scorer (string, optional): Player who scored the goal.
</span></span><span class="line"><span class="cl"><span class="k">-</span> minute (integer, optional): Match minute in which the goal occurred.
</span></span><span class="line"><span class="cl"><span class="k">-</span> own_goal (boolean, optional): Indicates whether the goal was an own goal.
</span></span><span class="line"><span class="cl"><span class="k">-</span> penalty (boolean, optional): Indicates whether the goal was scored from a penalty kick.
</span></span><span class="line"><span class="cl"><span class="k">-</span> country_flag_home (string): Emoji or symbol representing the home country.
</span></span><span class="line"><span class="cl"><span class="k">-</span> country_flag_away (string): Emoji or symbol representing the away country.
</span></span><span class="line"><span class="cl"><span class="k">-</span> continent (string): Continent associated with the home country.
</span></span><span class="line"><span class="cl"><span class="k">-</span> country_code (string): Country code associated with the home team (e.g., ISO-like).
</span></span><span class="line"><span class="cl"><span class="k">-</span> latitude (float): Latitude of the match location.
</span></span><span class="line"><span class="cl"><span class="k">-</span> longitude (float): Longitude of the match location.
</span></span><span class="line"><span class="cl"><span class="k">-</span> match_id (string): Unique identifier for the match, typically based on date and team names.
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>shescores_extra_instrucions.md</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="k">-</span> Maintain consistent spelling in British English.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Don&#39;t add any extra columns to the dataset. You may use them internally 
</span></span><span class="line"><span class="cl">  for calculations, but the final output should only include the original 
</span></span><span class="line"><span class="cl">  columns with the original column names.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Soccer terminology should be used throughout the analysis 
</span></span><span class="line"><span class="cl">  (e.g., &#34;goal&#34; instead of &#34;point&#34;).
</span></span></code></pre></td></tr></table>
</div>
</div><p>Now, adding <code>querychat</code> into the mix is as simple as replacing our inputs in the sidebar with the <code>querychat</code> sidebar component (<code>sidebar()</code>), and our reactive with the results of <code>server()</code>.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-17" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-17-1">Python</a></li>
<li><a href="#tabset-17-2">R</a></li>
</ul>
<div id="tabset-17-1">
<blockquote>
<p><strong>Tip</strong></p>
<p>See <a href="https://github.com/hypebright/shescores-dashboard/blob/963d2b72c600ee9f30ce04da170b05a01c1dc31c/Python/shescores-querychat-app.py" target="_blank" rel="noopener">GitHub</a>
 for the full source code</p>
</blockquote>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;data/results_with_scorers.csv&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">to_datetime</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">=</span> <span class="n">results_with_scorers</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;tournament&#34;</span><span class="p">]</span> <span class="o">!=</span> <span class="s2">&#34;Friendly&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">&amp;</span> <span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="s2">&#34;2000-01-01&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Initialize QueryChat with custom files</span>
</span></span><span class="line"><span class="cl"><span class="n">shescores_greeting</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;shescores_greeting.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">shescores_data_description</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;shescores_data_description.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">shescores_extra_instructions</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;shescores_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">results_with_scorers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">greeting</span><span class="o">=</span><span class="n">shescores_greeting</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_description</span><span class="o">=</span><span class="n">shescores_data_description</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">extra_instructions</span><span class="o">=</span><span class="n">shescores_extra_instructions</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Other setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># UI</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">app_ui</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">page_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">qc</span><span class="o">.</span><span class="n">sidebar</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Other UI components</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span><span class="o">=</span><span class="s2">&#34;She Scores ⚽️: Women&#39;s International Soccer Matches&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">fillable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">theme</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">Theme</span><span class="o">.</span><span class="n">from_brand</span><span class="p">(</span><span class="vm">__file__</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">server</span><span class="p">(</span><span class="nb">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Reactive filtered data based on query</span>
</span></span><span class="line"><span class="cl">    <span class="n">filtered_data</span> <span class="o">=</span> <span class="n">qc</span><span class="o">.</span><span class="n">server</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># Other server logic</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-17-2">
<blockquote>
<p><strong>Tip</strong></p>
<p>Check out the full code on <a href="https://github.com/hypebright/shescores-dashboard/blob/9c8b20d64adfb67566272c587e158dbf2a5052d8/R/shescores-querychat-app.R" target="_blank" rel="noopener">GitHub</a>
.</p>
</blockquote>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">&lt;-</span> <span class="nf">read.csv</span><span class="p">(</span><span class="s">&#34;../data/results_with_scorers.csv&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">filter</span><span class="p">(</span><span class="n">tournament</span> <span class="o">!=</span> <span class="s">&#34;Friendly&#34;</span><span class="p">,</span> <span class="n">date</span> <span class="o">&gt;=</span> <span class="s">&#34;2000-01-01&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Initialize QueryChat with custom files</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">results_with_scorers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">greeting</span> <span class="o">=</span> <span class="s">&#34;shescores_greeting.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">data_description</span> <span class="o">=</span> <span class="s">&#34;shescores_data_description.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">extra_instructions</span> <span class="o">=</span> <span class="s">&#34;shescores_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Other setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># UI</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="nf">page_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">fillable</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;She Scores ⚽️: Women&#39;s International Soccer Matches&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">sidebar</span> <span class="o">=</span> <span class="n">qc</span><span class="o">$</span><span class="nf">sidebar</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Other UI components</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Reactive filtered data based on query</span>
</span></span><span class="line"><span class="cl">  <span class="n">filtered_data</span> <span class="o">&lt;-</span> <span class="n">qc</span><span class="o">$</span><span class="nf">server</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Other server logic</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>It results in a lot less code and logic too. Win-win. Thanks <code>querychat</code> !</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/shescores-querychat-py.gif" alt="Python version of SheScores with querychat" />
<figcaption aria-hidden="true">Python version of SheScores with querychat</figcaption>
</figure>
<blockquote>
<p><strong>Note</strong></p>
<p>While we don&rsquo;t have a reset button in the app, <code>querychat</code> knows very well what to do when you ask it to reset the dashboard. In this case, it will display the unfiltered data, just like we started when we launched the app.</p>
</blockquote>
<h1 id="database-options">Database options
</h1>
<p>So far we&rsquo;ve only worked with simple datasets: the <code>diamonds</code> dataset that ships with a package, and our soccer data loaded from a <code>.csv</code>. But here&rsquo;s how it works under the hood: even in those examples, you weren&rsquo;t really querying a data frame directly. <code>querychat</code> hands everything off to DuckDB, which becomes the engine that executes all generated SQL. And DuckDB does so quickly and efficiently. Your data frame or <code>.csv</code> is effectively registered inside DuckDB, and every answer comes from real SQL running on that engine.</p>
<p>But what if you don&rsquo;t want to work with in-memory tables at all? What if you already have a database you want to query directly? Maybe a DuckDB file, a SQLite database, Postgres, or even BigQuery? That&rsquo;s exactly what the <code>data_source</code> argument is for. Earlier we used it with plain data frames, but it also accepts database connections. In Python, that means any <a href="https://www.sqlalchemy.org" target="_blank" rel="noopener">SQLAlchemy-supported database</a>
; in R, anything that <a href="https://dbi.r-dbi.org" target="_blank" rel="noopener"><code>DBI</code></a>
 can handle. <code>querychat</code> will inspect the schema of whatever you connect, and from that moment on the workflow is identical as before, only now you&rsquo;re interacting with your own database.</p>
<p>Let&rsquo;s take a look at how to set up <code>querychat</code> with another backend (SQLite) using the <code>data_source</code> argument.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-18" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-18-1">Python</a></li>
<li><a href="#tabset-18-2">R</a></li>
</ul>
<div id="tabset-18-1">
<p>For demonstration purposes, we&rsquo;ll create a SQLite database from the SheScores data (<code>results_with_scorers.csv</code>).</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy</span> <span class="kn">import</span> <span class="n">create_engine</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># From results_with_scorers.csv, create a SQLite database named shescores.db</span>
</span></span><span class="line"><span class="cl"><span class="n">df_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;data/results_with_scorers.csv&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span><span class="n">df_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create the SQLite database and store the DataFrame in it</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Save database in top-level /data directory</span>
</span></span><span class="line"><span class="cl"><span class="n">df</span><span class="o">.</span><span class="n">to_sql</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">con</span><span class="o">=</span><span class="n">create_engine</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;sqlite:///&#34;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;data/shescores.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">if_exists</span><span class="o">=</span><span class="s2">&#34;replace&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">index</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>We can then use this database in our <code>QueryChat</code> instance like so:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy</span> <span class="kn">import</span> <span class="n">create_engine</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">querychat</span> <span class="kn">import</span> <span class="n">QueryChat</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">dotenv</span> <span class="kn">import</span> <span class="n">load_dotenv</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">load_dotenv</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Custom files for SheScores</span>
</span></span><span class="line"><span class="cl"><span class="n">shescores_greeting</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;shescores_greeting.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">shescores_data_description</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;shescores_data_description.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">shescores_extra_instructions</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;shescores_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Now create a QueryChat instance to interact with the database</span>
</span></span><span class="line"><span class="cl"><span class="n">db_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;data/shescores.db&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">engine</span> <span class="o">=</span> <span class="n">create_engine</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;sqlite:///</span><span class="si">{</span><span class="n">db_path</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">engine</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">greeting</span><span class="o">=</span><span class="n">shescores_greeting</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_description</span><span class="o">=</span><span class="n">shescores_data_description</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">extra_instructions</span><span class="o">=</span><span class="n">shescores_extra_instructions</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">qc</span><span class="o">.</span><span class="n">app</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>You can also create a DuckDB database from a CSV file or a pandas DataFrame, which is definitely nice for larger datasets. For more examples you can check out the package documentation on <a href="https://posit-dev.github.io/querychat/py/data-sources.html" target="_blank" rel="noopener">data sources</a>
.</p>
<p>Even if you have a database that isn&rsquo;t supported by SQLAlchemy or isn&rsquo;t suited for DuckDB, you can still let <code>querychat</code> access it. In that case, you need to implement the <a href="https://posit-dev.github.io/querychat/py/reference/types.DataSource.html" target="_blank" rel="noopener">DataSource</a>
 interface/protocol.</p>
</div>
<div id="tabset-18-2">
<p>For demonstration purposes, we&rsquo;ll create a SQLite database from the SheScores data (<code>results_with_scorers.csv</code>). To create a new SQLite database, you simply supply the filename to <a href="https://dbi.r-dbi.org/reference/dbConnect.html" target="_blank" rel="noopener"><code>dbConnect()</code></a>
. And with <code>dbWriteTable(</code>), you can easily copy an R dataframe into that newly generated SQLite database:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">DBI</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># From results_with_scorers.csv, create a SQLite database named shescores.db</span>
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">&lt;-</span> <span class="nf">read.csv</span><span class="p">(</span><span class="s">&#34;data/results_with_scorers.csv&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create a connection to a new SQLite database</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Save database in top-level /data directory</span>
</span></span><span class="line"><span class="cl"><span class="n">conn</span> <span class="o">&lt;-</span> <span class="nf">dbConnect</span><span class="p">(</span><span class="n">RSQLite</span><span class="o">::</span><span class="nf">SQLite</span><span class="p">(),</span> <span class="s">&#34;data/shescores.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Write the data frame to a table named results_with_scorers</span>
</span></span><span class="line"><span class="cl"><span class="nf">dbWriteTable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">conn</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">results_with_scorers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">overwrite</span> <span class="o">=</span> <span class="kc">TRUE</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">dbDisconnect</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>If you have a SQLite database, connecting to it works in the same manner:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">querychat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">DBI</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create a connection to a SQLite database</span>
</span></span><span class="line"><span class="cl"><span class="n">conn</span> <span class="o">&lt;-</span> <span class="nf">dbConnect</span><span class="p">(</span><span class="n">RSQLite</span><span class="o">::</span><span class="nf">SQLite</span><span class="p">(),</span> <span class="s">&#34;data/shescores.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Write the data frame to a table named results_with_scorers</span>
</span></span><span class="line"><span class="cl"><span class="nf">dbWriteTable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">conn</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">results_with_scorers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">overwrite</span> <span class="o">=</span> <span class="kc">TRUE</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Now create a QueryChat instance to interact with the database</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">conn</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">greeting</span> <span class="o">=</span> <span class="s">&#34;shescores_greeting.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">data_description</span> <span class="o">=</span> <span class="s">&#34;shescores_data_description.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">extra_instructions</span> <span class="o">=</span> <span class="s">&#34;shescores_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span><span class="o">$</span><span class="nf">app</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Looking for more examples? Check out these <a href="https://github.com/posit-dev/querychat/tree/main/pkg-r/inst/examples-shiny/sqlite" target="_blank" rel="noopener">database setup examples for querychat</a>
.</p>
</div>
</div>
<p><code>querychat</code> knows how to deal with databases, and it has some convenient features for it too, especially when things go wrong: it validates whether tables actually exist and handles any issues gracefully (without cryptic error messages).</p>
<p>One thing to keep in mind when you move from in-memory data to real databases, especially inside Shiny apps, is proper connection management. Whenever your app opens a database connection, it also needs to close it. In Python that usually means calling <code>engine.dispose()</code> when the app shuts down. In R you would use <code>dbDisconnect(conn)</code>, or rely on a connection pool. SQLAlchemy already provides pooling on the Python side, but in R you&rsquo;ll want the <code>pool</code> package to handle this in a nice manner.</p>
<h1 id="for-the-curious-how-does-querychat-know-what-to-do">For the curious: how does querychat know what to do?
</h1>
<p>You&rsquo;ve seen what <code>querychat</code> can do, and you know a bit how it works conceptually. But behind all those concepts is of course some real code. So, for the curious amongst us, here&rsquo;s a little peek into the <code>querychat</code> code!</p>
<p>To talk with an LLM you need a good prompt: prompt design is crucial for a good outcome. A prompt contains context and instructions that an LLM will use to come up with its answer. <code>querychat</code> has a set of instructions for the LLM too, the system prompt, which is stored in a Markdown file (<code>prompt.md</code>).</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-19" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-19-1">Python</a></li>
<li><a href="#tabset-19-2">R</a></li>
</ul>
<div id="tabset-19-1">
<p>You can check out the <code>prompt.md</code> file <a href="https://github.com/posit-dev/querychat/blob/fea52e4e2b56a2cc0a042140dbe5ce194aca8ac6/pkg-py/src/querychat/prompts/prompt.md" target="_blank" rel="noopener">here</a>
, or you can simply print it out:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">qc</span><span class="o">.</span><span class="n">system_prompt</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-19-2">
<p>You can check out the <code>prompt.md</code> file <a href="https://github.com/posit-dev/querychat/blob/main/pkg-r/inst/prompts/prompt.md" target="_blank" rel="noopener">here</a>
, or you can simply print it out:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">print</span><span class="p">(</span><span class="n">qc</span><span class="o">$</span><span class="nf">system_prompt</span><span class="p">())</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>So, what&rsquo;s in this prompt? Let&rsquo;s highlight a few bits:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">You have access to a {{db_type}} SQL database with the following schema:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;database_schema&gt;
</span></span><span class="line"><span class="cl">{{schema}}
</span></span><span class="line"><span class="cl">&lt;/database_schema&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">{{<span class="ni">#data_description</span>}}
</span></span><span class="line"><span class="cl">Here is additional information about the data:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;data_description&gt;
</span></span><span class="line"><span class="cl">{{data_description}}
</span></span><span class="line"><span class="cl">&lt;/data_description&gt;
</span></span><span class="line"><span class="cl">{{/data_description}}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">For security reasons, you may only query this specific table.
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">{{<span class="ni">#extra_instructions</span>}}
</span></span><span class="line"><span class="cl"><span class="gu">## Additional Instructions
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">{{extra_instructions}}
</span></span><span class="line"><span class="cl">{{/extra_instructions}}
</span></span></code></pre></td></tr></table>
</div>
</div><p>The prompt is a <a href="https://mustache.github.io" target="_blank" rel="noopener">Mustache</a>
 template. It&rsquo;s a fill-in-the-blanks template: the <code>{name}</code> parts get replaced with real values, and the <code>{#something} ... {{/something}}</code> blocks only appear if that &ldquo;something&rdquo; actually exists. When you call QueryChat with corresponding arguments, everything gets filled in.</p>
<p>We talked about tool calling earlier, and there was a little note that said that there&rsquo;s not just one tool. There are multiple, for different tasks. You can see that back clearly in the prompt, where we instruct the LLM to call a certain tool (e.g. <code>querychat_update_dashboard</code>) when it receives a request:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">You can handle three types of requests:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### 1. Filtering and Sorting Data
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Call <span class="sb">`querychat_update_dashboard`</span> with the query and a descriptive title
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The user may ask to &#34;reset&#34; or &#34;start over&#34;; that means clearing the filter and title. Do this by calling querychat_reset_dashboard().
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### 2. Answering Questions About Data
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Use the <span class="sb">`querychat_query`</span> tool to run SQL queries
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### 3. Providing Suggestions for Next Steps
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...
</span></span></code></pre></td></tr></table>
</div>
</div><p>There are three tools in <code>querychat</code>:</p>
<ul>
<li><code>querychat_query</code>: used whenever the user asks a question that requires data analysis, aggregation, or calculations.</li>
<li><code>querychat_update_dashboard</code>: used whenever the user requests filtering, sorting, or data manipulation on the dashboard with questions like &ldquo;Show me&hellip;&rdquo; or &ldquo;Which records have&hellip;&rdquo;. Basically any request that involves showing a subset of the data or reordering it.</li>
<li><code>querychat_reset_dashboard</code>: if the user asks to reset the dashboard</li>
</ul>
<p>All the tools are written as <code>chatlas</code> or <code>ellmer</code> tools. As a user, you don&rsquo;t have to worry about this though. The LLM makes sure to use the rights tools, which will make sure the SQL gets executed and the data gets filtered accordingly. But hey, this section was for the curious amongst us!</p>
<h1 id="safety-control-and-confidence">Safety, control, and confidence
</h1>
<p>At some point, everyone asks the same question: is this safe? And it&rsquo;s a fair one. Luckily, <code>querychat</code> is designed entirely around control. The LLM never executes anything itself, never touches your data(base) and never sees raw data. Its only job is to propose <em>read-only</em> SQL.</p>
<p>Remember the moment we asked it to drop a table? It refused. Not because it&rsquo;s polite, but because it&rsquo;s instructed to do so. Combine that with an underlying database (the built-in DuckDB temporary database or your own) that only provides read-only access, and your data will always be left untouched. </p>
<p>It&rsquo;s not a black box either: every generated query can be logged, inspected or audited at any time. In Shiny v1.12.0 this becomes even easier thanks to built in OpenTelemetry support via <code>otel</code>. If you&rsquo;re curious about what that looks like in practice, you can read more in this <a href="https://shiny.posit.co/r/articles/improve/opentelemetry/" target="_blank" rel="noopener">article</a>
.</p>
<p>The safety, control, and (hopefully) the confidence you&rsquo;ve gained by now, make it also suitable for enterprise and regulated environments. If you need to use private or managed LLMs, you&rsquo;re covered: Azure, AWS Bedrock and Google Vertex AI all provide versions of popular models that support tool calling and can work with <code>querychat</code>.</p>
<h1 id="other-querychat-apps-in-the-wild">Other querychat apps in the wild
</h1>
<p>It&rsquo;s always nice to see what others have done with <code>querychat</code>. So here are few sources of inspiration:</p>
<ul>
<li>Do you like trail running? This <a href="https://posit.co/blog/race-stats-dashboard-querychat/" target="_blank" rel="noopener">Race Stats dashboard</a>
 is for you!</li>
<li>Is the American football league more your thing? This <a href="https://www.infoworld.com/article/4040535/chat-with-your-data-the-easy-way-in-r-or-python.html" target="_blank" rel="noopener">Shiny for Python app</a>
 shows you a lot of stats.</li>
<li>Joe Cheng and Garrick Aden-Buie hosted a workshop at posit::conf(2025) called &ldquo;Programming with LLMs&rdquo; that also contains some <a href="https://github.com/posit-conf-2025/llm" target="_blank" rel="noopener">examples</a>
.</li>
<li>And one that we mentioned before: <a href="https://www.infoworld.com/article/4040535/chat-with-your-data-the-easy-way-in-r-or-python.html" target="_blank" rel="noopener">sidebot</a>
, a dashboard analysing restaurant tipping, which is a template you can use very easily.</li>
</ul>
<p>Whether you&rsquo;re playing with a small example dataset or building something much bigger, <code>querychat</code> can be the companion in your app that you&rsquo;re users will love. Build a whole dashboard around the chatbot, or add a touch of LLM magic for those extra side questions. With all this knowledge under your belt, you can build it all!</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>While it seems like there is only one tool call, there&rsquo;s not. In <code>querychat</code> there are different tools for, surprise, different tasks. For the curious there&rsquo;s a deep dive into <code>querychat</code>&rsquo;s source code later in this article.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/querychat-python-r-header.png" length="871482" type="image/png" />
    </item>
    <item>
      <title>plumber2 0.2.0</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/plumber2-0-2-0/</link>
      <pubDate>Tue, 20 Jan 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/plumber2-0-2-0/</guid>
      <dc:creator>Thomas Lin Pedersen</dc:creator><description><![CDATA[<!--
TODO:
* [x] Look over / edit the post's title in the yaml
* [x] Edit (or delete) the description; note this appears in the Twitter card
* [x] Pick category and tags (see existing with [`hugodown::tidy_show_meta()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html))
* [x] Find photo & update yaml metadata
* [x] Create `thumbnail-sq.jpg`; height and width should be equal
* [x] Create `thumbnail-wd.jpg`; width should be >5x height
* [x] [`hugodown::use_tidy_thumbnails()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html)
* [ ] Add intro sentence, e.g. the standard tagline for the package
* [ ] [`usethis::use_tidy_thanks()`](https://usethis.r-lib.org/reference/use_tidy_thanks.html)
-->
<p>We&rsquo;re stoked to announce the release of <a href="https://plumber2.posit.co" target="_blank" rel="noopener">plumber2</a>
 0.2.0. plumber2 is a package for creating webservers in R based on either an annotation-based or programmatic workflow. It is the successor to the <a href="https://www.rplumber.io/" target="_blank" rel="noopener">plumber</a>
 package who has empowered the R community for 10 years and allowed them to share their R based functionalities with their organizations and the world.</p>
<p>You can install it from CRAN with:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>pak</span><span class='nf'>::</span><span class='nf'><a href='https://pak.r-lib.org/reference/pak.html'>pak</a></span><span class='o'>(</span><span class='s'>"plumber2"</span><span class='o'>)</span></span></code></pre>
</div>
<p>This release covers both a bunch of new features as well as some tangible improvements to performance. The headlining features are OpenTelemetry (OTEL) support and support for authentication which we will dive into below. In the end we will also provide a grab-bag of miscellaneous improvements for your enjoyment.</p>
<p>You can see a full list of changes in the <a href="https://plumber2.posit.co/news/index.html" target="_blank" rel="noopener">release notes</a>
</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://plumber2.posit.co/'>plumber2</a></span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="otel-support">OTEL support
</h2>
<p>We have been hard at work at adding support for <a href="https://opentelemetry.io/" target="_blank" rel="noopener">OpenTelemetry (OTEL)</a>
 for our tools to allow easy instrumentation across our offerings, see e.g. the <a href="https://shiny.posit.co/blog/posts/shiny-r-1.12/" target="_blank" rel="noopener">shiny blog post</a>
 announcing support for it there. If you do not know what OTEL is, here is a short introduction to the subject:</p>
<p>OTEL describes itself as &ldquo;high-quality, ubiquitous, and portable telemetry to enable effective observability&rdquo;. In simpler terms, OpenTelemetry is a set of tools, APIs, and SDKs that help you collect and export telemetry data (like traces, logs, and metrics) from your applications. This data provides insights into how your applications are performing and behaving in real-world scenarios.</p>
<p>It captures three key types of data:</p>
<ol>
<li><strong>Traces:</strong> These show the path of a request through your application.</li>
<li><strong>Logs:</strong> These are detailed event records that capture what happened at specific moments.</li>
<li><strong>Metrics:</strong> These are numerical measurements over time, like how many users are connected or how long outputs take to render.</li>
</ol>
<p>These data types were standardized under the OTEL project, <a href="https://opentelemetry.io/community/marketing-guidelines/#i-opentelemetry-is-a-joint-effort" target="_blank" rel="noopener">which is supported by a large community and many companies</a>
. The goal is to provide a consistent way to collect and export observability data, making it easier to monitor and troubleshoot applications.</p>
<p>OTEL is vendor-neutral, meaning you can send your telemetry data to various local backends like <a href="https://www.jaegertracing.io/" target="_blank" rel="noopener">Jaeger</a>
, <a href="https://zipkin.io/" target="_blank" rel="noopener">Zipkin</a>
, <a href="https://prometheus.io/" target="_blank" rel="noopener">Prometheus</a>
, or cloud-based services like <a href="https://grafana.com/products/cloud/" target="_blank" rel="noopener">Grafana Cloud</a>
, <a href="https://pydantic.dev/logfire" target="_blank" rel="noopener">Logfire</a>
, and <a href="https://langfuse.com/" target="_blank" rel="noopener">Langfuse</a>
. This flexibility means you&rsquo;re not locked into any particular monitoring solution.</p>
<p>While that may be somewhat of a mouthful the <em>tldr;</em> is that with OTEL you can capture what goes on in your application and use a variety of services to explore this data. This is great especially for code that is meant to be deployed and thus not readily available for introspection.</p>
<p>A great thing about OTEL is that traces are linked across applications. If you have multiple linked microservices based on plumber2, then you can follow a request trace as it travels between the different APIs. The same goes for a shiny app that calls into a plumber2 api or the other way around. As we build out support across our tools this benefit will only get more profound.</p>
<h3 id="otel-in-plumber2">OTEL in plumber2
</h3>
<p>While OTEL is integrated into plumber2 it is not activated by default. To set it up you need the otel and otelsdk installed and configured:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>pak</span><span class='nf'>::</span><span class='nf'><a href='https://pak.r-lib.org/reference/pak.html'>pak</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"otel"</span>, <span class='s'>"otelsdk"</span><span class='o'>)</span><span class='o'>)</span></span></code></pre>
</div>
<p>Configuration is completely code free and based on environment variables. You can e.g. add the lines below to your <code>.Renviron</code> file to setup OTEL with Logfire</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Enable OpenTelemetry by setting Collector environment variables</span>
</span></span><span class="line"><span class="cl"><span class="nv">OTEL_TRACES_EXPORTER</span><span class="o">=</span>http
</span></span><span class="line"><span class="cl"><span class="nv">OTEL_LOGS_EXPORTER</span><span class="o">=</span>http
</span></span><span class="line"><span class="cl"><span class="nv">OTEL_LOG_LEVEL</span><span class="o">=</span>debug
</span></span><span class="line"><span class="cl"><span class="nv">OTEL_METRICS_EXPORTER</span><span class="o">=</span>http
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">OTEL_EXPORTER_OTLP_ENDPOINT</span><span class="o">=</span><span class="s2">&#34;https://logfire-us.pydantic.dev&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">OTEL_EXPORTER_OTLP_HEADERS</span><span class="o">=</span><span class="s2">&#34;Authorization=&lt;your-write-token&gt;&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>You can verify that everything is set up by calling <a href="https://otel.r-lib.org/reference/is_tracing_enabled.html" target="_blank" rel="noopener"><code>otel::is_tracing_enabled()</code></a>
 which should return <code>TRUE</code> in that case.</p>
<p>OTEL has an extensive list of semantic conventions for telemetry of various domains so that information is captured in a standardised way. plumber2 adheres to the HTTP server conventions and supports all the required and most of the recommended <a href="https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server-span" target="_blank" rel="noopener">trace attributes</a>
 and <a href="https://opentelemetry.io/docs/specs/semconv/http/http-metrics/" target="_blank" rel="noopener">metrics</a>
.</p>
<p>Within a plumber2 API, a trace span is started the moment a request is received. The span is populated with the following information:</p>
<ul>
<li><code>http.request.method</code>: The method of the request (e.g. <code>GET</code>, <code>POST</code>, etc)</li>
<li><code>url.path</code>: The exact path requested</li>
<li><code>url.scheme</code>: The protocol used for the request</li>
<li><code>http.route</code>: The route pattern of the last of the route handlers the request went through</li>
<li><code>network.protocol.name</code>: The internal protocol used. Always <code>http</code></li>
<li><code>network.protocol.version</code>: The version of the protocol. Always <code>1.1</code></li>
<li><code>server.port</code>: The port the server is listening on. Can be used to distinguish multiple concurrent servers</li>
<li><code>url.query</code>: The querystring of the request</li>
<li><code>client.address</code>: The IP address the request comes from</li>
<li><code>server.address</code>: The address the request was send to</li>
<li><code>user_agent.original</code>: The user agent of the client sending the request</li>
<li><code>http.request.header.&lt;header-name&gt;</code>: The value of <code>header-name</code> in the request. E.g. <code>http.request.header.date</code> will contain the value of the <code>Date</code> header</li>
</ul>
<p>Once the request has been handled it will further append the following information:</p>
<ul>
<li><code>http.response.status_code</code>: The status code of the response</li>
<li><code>http.response.header.&lt;header-name&gt;</code>: The value of <code>header-name</code> in the response. E.g. <code>http.response.header.content-type</code> will contain the value of the <code>Content-Type</code> header</li>
</ul>
<p>In addition to the trace attributes above, a number of OTEL metrics are also recorded:</p>
<ul>
<li><code>http.server.request.duration</code>: The duration of the request handling from it is received to it is send back</li>
<li><code>http.server.active_requests</code>: The number of active requests being handled at the given time</li>
<li><code>http.server.request.body.size</code>: The size of the request body</li>
<li><code>http.server.response.body.size</code>: The size of the response body</li>
</ul>
<p>As a child of this parent span each handler in your API will also initiate a span with the following attributes:</p>
<ul>
<li><code>routr.route</code>: The path pattern of the handler. This will be recorded in the routr representation which uses <code>:param</code> instead of <code>{param}</code> format (e.g. <code>users/:username</code> instead of <code>users/{username}</code>)</li>
<li><code>routr.path.param.&lt;param-name&gt;</code>: The value of the <code>param-name</code> path parameter. E.g. a request for <code>users/thomas</code> will get a <code>routr.path.param.username</code> attribute with the value <code>thomas</code> for the route <code>users/{username}</code>.</li>
</ul>
<p>Any span you initiate inside a handler will become a child of the handler span and through that be linked to the parent request span.</p>
<p>As you can see, the integration provides extensive information for you to use when figuring out what is going on in your application. On top of that, you can also use OTEL as your logging solution by setting <code>logger_otel</code> as your logging solution:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://plumber2.posit.co/reference/api.html'>api</a></span><span class='o'>(</span><span class='o'>)</span> <span class='o'>|&gt;</span> </span>
<span>  <span class='nf'><a href='https://plumber2.posit.co/reference/api_logger.html'>api_logger</a></span><span class='o'>(</span><span class='nv'>logger_otel</span><span class='o'>)</span></span></code></pre>
</div>
<p>This ensures that all the logs from errors, warnings, etc all end up in the same place as your other recordings and further gets linked to the exact request that gave rise to the log.</p>
<p>We truly believe extensive OTEL support across the ecosystem will be a game changer for deployed R code and we can&rsquo;t wait for our users to take advantage of it!</p>
<h2 id="auth-support">Auth support
</h2>
<p>The second headliner is support for various authentication schemes out of the box. This comes courtesy of of the <a href="https://fireproof.data-imaginist.com" target="_blank" rel="noopener">fireproof</a>
 package which provides an auth plugin for fiery.</p>
<p>Setting up authentication is twofold: creating guards and attaching guards to routes.</p>
<p>First, you need to define one or more guards to use. A guard is an adaption of a specific authentication scheme such as e.g. OAuth. Currently, fireproof supports the Basic and Bearer HTTP authorization schemes, a custom key based scheme, as well as OAuth 2.0 and OpenID Connect. Setting up a guard can be done both programmatically and with annotations:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># Programmatic</span></span>
<span><span class='nv'>api</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://plumber2.posit.co/reference/api.html'>api</a></span><span class='o'>(</span><span class='o'>)</span> <span class='o'>|&gt;</span> </span>
<span>  <span class='nf'><a href='https://plumber2.posit.co/reference/api_auth_guard.html'>api_auth_guard</a></span><span class='o'>(</span></span>
<span>    guard <span class='o'>=</span> <span class='nf'>fireproof</span><span class='nf'>::</span><span class='nf'><a href='https://fireproof.data-imaginist.com/reference/guard_key.html'>guard_key</a></span><span class='o'>(</span></span>
<span>      key_name <span class='o'>=</span> <span class='s'>"X-API-KEY"</span>,</span>
<span>      validate <span class='o'>=</span> <span class='s'>"MY_VERY_SECRET_KEY"</span></span>
<span>    <span class='o'>)</span>,</span>
<span>    name <span class='o'>=</span> <span class='s'>"key_guard"</span></span>
<span>  <span class='o'>)</span></span></code></pre>
</div>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># Annotation</span></span>
<span></span>
<span><span class='c'>#* @authGuard key_guard</span></span>
<span><span class='nf'>fireproof</span><span class='nf'>::</span><span class='nf'><a href='https://fireproof.data-imaginist.com/reference/guard_key.html'>guard_key</a></span><span class='o'>(</span></span>
<span>  key_name <span class='o'>=</span> <span class='s'>"X-API-KEY"</span>,</span>
<span>  validate <span class='o'>=</span> <span class='s'>"MY_VERY_SECRET_KEY"</span></span>
<span><span class='o'>)</span></span></code></pre>
</div>
<p>Both of these pieces of code yields the same result. You API now has a guard registered under the name <code>key_guard</code> which will (if called upon) check a request for the existence of a cookie named <code>X-API-KEY</code> with the value <code>MY_VERY_SECRET_KEY</code>.</p>
<p>Secondly, your handlers can now integrate the guards to protect access to the requested path. Again, this can be done both programmatically and in annotation and will generally be handled when the request handler is created:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># Programmatic</span></span>
<span><span class='nv'>api</span> <span class='o'>|&gt;</span> </span>
<span>  <span class='nf'><a href='https://plumber2.posit.co/reference/api_request_handlers.html'>api_get</a></span><span class='o'>(</span></span>
<span>    path <span class='o'>=</span> <span class='s'>"/admin"</span>,</span>
<span>    <span class='kr'>function</span><span class='o'>(</span><span class='nv'>...</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>      <span class='c'># whatever you wish to protect</span></span>
<span>    <span class='o'>&#125;</span>,</span>
<span>    auth_flow <span class='o'>=</span> <span class='nv'>key_guard</span></span>
<span>  <span class='o'>)</span></span>
<span></span></code></pre>
</div>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># Annotation</span></span>
<span></span>
<span><span class='c'>#* An example endpoint with auth</span></span>
<span><span class='c'>#* </span></span>
<span><span class='c'>#* @get /admin</span></span>
<span><span class='c'>#* @auth key_guard</span></span>
<span><span class='kr'>function</span><span class='o'>(</span><span class='nv'>...</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>  <span class='c'># whatever you wish to protect</span></span>
<span><span class='o'>&#125;</span></span></code></pre>
</div>
<p>Again, both code chunks achieve the same thing. They set up the endpoint to require the <code>key_guard</code> to be passed before further handling takes place.</p>
<h3 id="multiple-guards-and-requirements">Multiple guards and requirements
</h3>
<p>The previous section demonstrates the most basic authentication setup as it only uses the key guard&mdash;the simplest guard to configure. We can imagine a situation where we both want to allow users to log in with a username and password <em>or</em> authorize with a key and a google login. This requires defining multiple guards which can be done in sequence:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'>#* @authGuard key</span></span>
<span><span class='nf'>fireproof</span><span class='nf'>::</span><span class='nf'><a href='https://fireproof.data-imaginist.com/reference/guard_key.html'>guard_key</a></span><span class='o'>(</span></span>
<span>  key_name <span class='o'>=</span> <span class='s'>"X-API-KEY"</span>,</span>
<span>  validate <span class='o'>=</span> <span class='s'>"MY_VERY_SECRET_KEY"</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#* @authGuard basic</span></span>
<span><span class='nf'>fireproof</span><span class='nf'>::</span><span class='nf'><a href='https://fireproof.data-imaginist.com/reference/guard_basic.html'>guard_basic</a></span><span class='o'>(</span></span>
<span>  validate <span class='o'>=</span> <span class='kr'>function</span><span class='o'>(</span><span class='nv'>username</span>, <span class='nv'>password</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>    <span class='nv'>username</span> <span class='o'>==</span> <span class='s'>"thomas"</span> <span class='o'>&amp;&amp;</span> <span class='nv'>password</span> <span class='o'>==</span> <span class='s'>"xrCy45rWrgwq"</span></span>
<span>  <span class='o'>&#125;</span></span>
<span><span class='o'>)</span></span>
<span><span class='c'>#* @authGuard google</span></span>
<span><span class='nf'>fireproof</span><span class='nf'>::</span><span class='nf'><a href='https://fireproof.data-imaginist.com/reference/guard_google.html'>guard_google</a></span><span class='o'>(</span></span>
<span>  redirect_url <span class='o'>=</span> <span class='s'>"https://example.com/auth"</span>,</span>
<span>  client_id <span class='o'>=</span> <span class='s'>"MY_APP_ID"</span>,</span>
<span>  client_secret <span class='o'>=</span> <span class='s'>"SUCHASECRET"</span></span>
<span><span class='o'>)</span></span></code></pre>
</div>
<p>We now have 3 guards (of dubious quality) that we can attach to our handler. How do we capture the relationship of requiring either the basic to pass or the key and google to pass? Simple, with a logical expression:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'>#* An example endpoint with auth</span></span>
<span><span class='c'>#* </span></span>
<span><span class='c'>#* @get /admin</span></span>
<span><span class='c'>#* @auth basic || (key &amp;&amp; google)</span></span>
<span><span class='kr'>function</span><span class='o'>(</span><span class='nv'>...</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>  <span class='c'># whatever you wish to protect</span></span>
<span><span class='o'>&#125;</span></span></code></pre>
</div>
<p>The names of the guards act as booleans and can be composed with the basic boolean operators (<code>||</code>, <code>&amp;&amp;</code>, and <code>(</code>/<code>)</code>). The combinations are endless!</p>
<h3 id="scopes">Scopes
</h3>
<p>Sometimes you need more granularity in your authentication. Some users may only read while others may read and write to resources. This could be solved with multiple guards but it quickly becomes unwieldy. Instead you can set scope requirements on an endpoint. Guards can then grant scopes to a user in their <code>validate</code> function by returning a character vector instead of a boolean, like this:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'>#* @authGuard basic</span></span>
<span><span class='nf'>fireproof</span><span class='nf'>::</span><span class='nf'><a href='https://fireproof.data-imaginist.com/reference/guard_basic.html'>guard_basic</a></span><span class='o'>(</span></span>
<span>  validate <span class='o'>=</span> <span class='kr'>function</span><span class='o'>(</span><span class='nv'>username</span>, <span class='nv'>password</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>    <span class='kr'>if</span> <span class='o'>(</span><span class='nv'>username</span> <span class='o'>==</span> <span class='s'>"guest"</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>      <span class='kr'><a href='https://rdrr.io/r/base/function.html'>return</a></span><span class='o'>(</span><span class='s'>"read"</span><span class='o'>)</span></span>
<span>    <span class='o'>&#125;</span></span>
<span>    <span class='kr'>if</span> <span class='o'>(</span><span class='nv'>username</span> <span class='o'>==</span> <span class='s'>"thomas"</span> <span class='o'>&amp;&amp;</span> <span class='nv'>password</span> <span class='o'>==</span> <span class='s'>"xrCy45rWrgwq"</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>      <span class='kr'><a href='https://rdrr.io/r/base/function.html'>return</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"read"</span>, <span class='s'>"write"</span><span class='o'>)</span><span class='o'>)</span></span>
<span>    <span class='o'>&#125;</span></span>
<span>    <span class='kc'>FALSE</span></span>
<span>  <span class='o'>&#125;</span></span>
<span><span class='o'>)</span></span>
<span></span>
<span><span class='c'>#* Read the calendar entries</span></span>
<span><span class='c'>#* </span></span>
<span><span class='c'>#* @get /calendar</span></span>
<span><span class='c'>#* @auth basic</span></span>
<span><span class='c'>#* @authScope read</span></span>
<span><span class='c'>#* </span></span>
<span><span class='kr'>function</span><span class='o'>(</span><span class='nv'>...</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>  <span class='c'># return calendar entries</span></span>
<span><span class='o'>&#125;</span></span>
<span></span>
<span><span class='c'>#* Add a new calendar entry</span></span>
<span><span class='c'>#* </span></span>
<span><span class='c'>#* @post /calendar</span></span>
<span><span class='c'>#* @auth basic</span></span>
<span><span class='c'>#* @authScope write</span></span>
<span><span class='c'>#* </span></span>
<span><span class='kr'>function</span><span class='o'>(</span><span class='nv'>...</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>  <span class='c'># update the calendar</span></span>
<span><span class='o'>&#125;</span></span></code></pre>
</div>
<p>The authentication that can be integrated is very flexible and will only grow as more guards are added to fireproof.</p>
<h2 id="other-news">Other news
</h2>
<h3 id="annotation-for-datastores">Annotation for datastores
</h3>
<p>While datastores through the <a href="https://github.com/thomasp85/firesale" target="_blank" rel="noopener">firesale</a>
 package was supported upon release, they could only be set up programmatically. This has now been corrected with the addition of the <code>@datastore</code> tag. It works like this:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'>#* @datastore my_store</span></span>
<span><span class='nf'>storr</span><span class='nf'>::</span><span class='nf'><a href='https://richfitz.github.io/storr/reference/storr_environment.html'>driver_environment</a></span><span class='o'>(</span><span class='o'>)</span></span></code></pre>
</div>
<p>The <code>my_store</code> proceeding the key is optional and gives the name of the datastore (defaults to <code>datastore</code>). Below the block you provide a <a href="https://richfitz.github.io/storr/" target="_blank" rel="noopener">storr</a>
 driver and then you are good to go.</p>
<p>Authentication requires a datastore in order to work as it facilitates persistent session login. Below, you can see an annotation implementation of a single guard that leverages a storr datastore.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'>#* @datastore ds</span></span>
<span><span class='nf'>storr</span><span class='nf'>::</span><span class='nf'><a href='https://richfitz.github.io/storr/reference/storr_environment.html'>driver_environment</a></span><span class='o'>(</span><span class='o'>)</span></span>
<span></span>
<span><span class='c'>#* @authGuard github</span></span>
<span><span class='nf'>fireproof</span><span class='nf'>::</span><span class='nf'><a href='https://fireproof.data-imaginist.com/reference/guard_github.html'>guard_github</a></span><span class='o'>(</span></span>
<span>  redirect_url <span class='o'>=</span> <span class='s'>"https://example.com/auth"</span>,</span>
<span>  client_id <span class='o'>=</span> <span class='s'>"MY_APP_ID"</span>,</span>
<span>  client_secret <span class='o'>=</span> <span class='s'>"SUCHASECRET"</span></span>
<span><span class='o'>)</span></span>
<span></span>
<span><span class='c'>#* Get a summary of your github commit history</span></span>
<span><span class='c'>#* </span></span>
<span><span class='c'>#* @auth github</span></span>
<span><span class='kr'>function</span><span class='o'>(</span><span class='nv'>ds</span><span class='o'>)</span> <span class='o'>&#123;</span></span>
<span>  <span class='nv'>github_token</span> <span class='o'>&lt;-</span> <span class='nv'>ds</span><span class='o'>$</span><span class='nv'>session</span><span class='o'>$</span><span class='nv'>github</span><span class='o'>$</span><span class='nv'>token</span><span class='o'>$</span><span class='nv'>access_token</span></span>
<span>  <span class='c'># Use the access token to fetch commit history and do some fun things</span></span>
<span><span class='o'>&#125;</span></span></code></pre>
</div>
<h3 id="more-powerful-report-support">More powerful report support
</h3>
<p>The report endpoint has gotten even more powerful in this release in a number of ways:</p>
<ul>
<li>Report endpoints can now be added programmatically as well using <a href="https://plumber2.posit.co/reference/api_report.html" target="_blank" rel="noopener"><code>api_report()</code></a>
</li>
<li>There is now support for quarto documents using the jupyter engine</li>
<li>OpenAPI documentation is now generated automatically for the report and incorporates the standard annotation known from request handler blocks.</li>
<li>Parameterised reports now has their parameters type checked and casted based on the type of the default values or on explicit type specification in the <code>@param</code> tags.</li>
<li>You can now request specific named output formats through the <code>/{output_format}</code> subpath. This is in addition to the content negotiation already available. E.g. <code>/report/revealjs</code> will request the revealjs format of the report served at <code>/report</code>.</li>
<li>Caches can now be user specific if the rendering includes information specific to the user requesting it</li>
<li>Caches can now be cleared using a <code>DELETE</code> request</li>
</ul>
<h2 id="thank-you">Thank you
</h2>
<p>I want to say thanks to everyone who has given plumber2 a spin. It takes some time to reach maturity when replacing a decade old package and every test spin brings more insight. With the addition of OTEL integration and auth support plumber2 has now reached the feature set I was planning for during the initial development and the next phase will be about refinement, performance, and bug fixes. Your input and experiences will be critical there.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/plumber2-0-2-0/thumbnail-wd.jpg" length="482139" type="image/jpeg" />
    </item>
    <item>
      <title>orbital 0.4.0</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/orbital-0-4-0/</link>
      <pubDate>Mon, 12 Jan 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/orbital-0-4-0/</guid>
      <dc:creator>Emil Hvitfeldt</dc:creator><description><![CDATA[<!--
TODO:
* [x] Look over / edit the post's title in the yaml
* [x] Edit (or delete) the description; note this appears in the Twitter card
* [x] Pick category and tags (see existing with [`hugodown::tidy_show_meta()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html))
* [x] Find photo & update yaml metadata
* [x] Create `thumbnail-sq.jpg`; height and width should be equal
* [x] Create `thumbnail-wd.jpg`; width should be >5x height
* [x] [`hugodown::use_tidy_thumbnails()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html)
* [x] Add intro sentence, e.g. the standard tagline for the package
* [ ] [`usethis::use_tidy_thanks()`](https://usethis.r-lib.org/reference/use_tidy_thanks.html)
-->
<p>We&rsquo;re over the moon to announce the release of <a href="https://orbital.tidymodels.org/" target="_blank" rel="noopener">orbital</a>
 0.4.0. orbital lets you predict in databases using tidymodels workflows.</p>
<p>You can install it from CRAN with:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/utils/install.packages.html'>install.packages</a></span><span class='o'>(</span><span class='s'>"orbital"</span><span class='o'>)</span></span></code></pre>
</div>
<p>This blog post will cover the highlights, which are post processing support and the new <code>show_query()</code> method.</p>
<p>You can see a full list of changes in the <a href="https://orbital.tidymodels.org/news/index.html#orbital-040" target="_blank" rel="noopener">release notes</a>
.</p>
<h2 id="post-processing-support">Post processing support
</h2>
<p>The biggest improvement in this version is that <a href="https://orbital.tidymodels.org/reference/orbital.html" target="_blank" rel="noopener"><code>orbital()</code></a>
 now works for supported <a href="https://tailor.tidymodels.org/" target="_blank" rel="noopener">tailor</a>
 methods. See <a href="https://orbital.tidymodels.org/articles/supported-models.html#tailor-adjustments" target="_blank" rel="noopener">vignette</a>
 for a list of all supported post-processors.</p>
<p>Let&rsquo;s start by fitting a classification model on the <code>penguins</code> data set, using {xgboost} as the engine. We will be showcasing using an adjustment that only works on binary classification and will thus recode <code>species</code> to have levels <code>&quot;Adelie&quot;</code> and <code>&quot;not_Adelie&quot;</code>.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>penguins</span><span class='o'>$</span><span class='nv'>species</span> <span class='o'>&lt;-</span> <span class='nf'>forcats</span><span class='nf'>::</span><span class='nf'><a href='https://forcats.tidyverse.org/reference/fct_recode.html'>fct_recode</a></span><span class='o'>(</span></span>
<span> <span class='nv'>penguins</span><span class='o'>$</span><span class='nv'>species</span>,</span>
<span> not_Adelie <span class='o'>=</span> <span class='s'>"Chinstrap"</span>, not_Adelie <span class='o'>=</span> <span class='s'>"Gentoo"</span></span>
<span><span class='o'>)</span></span></code></pre>
</div>
<p>After we have modified the data, we set up a simple workflow, with a preprocessor using recipes and the model specification using parsnip.</p>
<p>We also set up a post processor using the tailor package. A single adjustment will be done by adding <code>adjust_equivocal_zone()</code>. This will apply an equivocal zone to our binary classification model. Stopping predictions that are too close to the thresholds by labeling them as <code>&quot;[EQ]&quot;</code>. Setting the argument <code>value = 0.2</code> means that any predictions with a predicted probability of between 0.3 and 0.7 will be predicted as <code>&quot;[EQ]&quot;</code> instead.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>rec_spec</span> <span class='o'>&lt;-</span> <span class='nf'>recipe</span><span class='o'>(</span><span class='nv'>species</span> <span class='o'>~</span> <span class='nv'>.</span>, data <span class='o'>=</span> <span class='nv'>penguins</span><span class='o'>)</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'>step_unknown</span><span class='o'>(</span><span class='nf'>all_nominal_predictors</span><span class='o'>(</span><span class='o'>)</span><span class='o'>)</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'>step_dummy</span><span class='o'>(</span><span class='nf'>all_nominal_predictors</span><span class='o'>(</span><span class='o'>)</span><span class='o'>)</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'>step_impute_mean</span><span class='o'>(</span><span class='nf'>all_numeric_predictors</span><span class='o'>(</span><span class='o'>)</span><span class='o'>)</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'>step_zv</span><span class='o'>(</span><span class='nf'>all_predictors</span><span class='o'>(</span><span class='o'>)</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>lr_spec</span> <span class='o'>&lt;-</span> <span class='nf'>boost_tree</span><span class='o'>(</span>tree_depth <span class='o'>=</span> <span class='m'>1</span>, trees <span class='o'>=</span> <span class='m'>5</span><span class='o'>)</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'>set_mode</span><span class='o'>(</span><span class='s'>"classification"</span><span class='o'>)</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'>set_engine</span><span class='o'>(</span><span class='s'>"xgboost"</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>tlr_spec</span> <span class='o'>&lt;-</span> <span class='nf'>tailor</span><span class='o'>(</span><span class='o'>)</span> <span class='o'>|&gt;</span></span>
<span>  <span class='nf'>adjust_equivocal_zone</span><span class='o'>(</span>value <span class='o'>=</span> <span class='m'>0.2</span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>wf_spec</span> <span class='o'>&lt;-</span> <span class='nf'>workflow</span><span class='o'>(</span><span class='nv'>rec_spec</span>, <span class='nv'>lr_spec</span>, <span class='nv'>tlr_spec</span><span class='o'>)</span></span>
<span><span class='nv'>wf_fit</span> <span class='o'>&lt;-</span> <span class='nf'>fit</span><span class='o'>(</span><span class='nv'>wf_spec</span>, data <span class='o'>=</span> <span class='nv'>penguins</span><span class='o'>)</span></span></code></pre>
</div>
<p>With this fitted workflow object, we can call <a href="https://orbital.tidymodels.org/reference/orbital.html" target="_blank" rel="noopener"><code>orbital()</code></a>
 on it to create an orbital object. Notice that for <code>adjust_equivocal_zone()</code> to work, we need to set <code>type = c(&quot;class&quot;, &quot;prob&quot;)</code> as both are required for the <code>adjust_equivocal_zone()</code> transformation.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>orbital_obj</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://orbital.tidymodels.org/reference/orbital.html'>orbital</a></span><span class='o'>(</span><span class='nv'>wf_fit</span>, type <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/c.html'>c</a></span><span class='o'>(</span><span class='s'>"class"</span>, <span class='s'>"prob"</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='nv'>orbital_obj</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; <span style='color: #00BBBB;'>──</span> <span style='font-weight: bold;'>orbital Object</span> <span style='color: #00BBBB;'>───────────────────────────────────────────────────────</span></span></span>
<span><span class='c'>#&gt; • bill_length_mm = dplyr::if_else(is.na(bill_length_mm), 43.92193, ...</span></span>
<span><span class='c'>#&gt; • flipper_length_mm = dplyr::if_else(is.na(flipper_length_mm), 201 ...</span></span>
<span><span class='c'>#&gt; • .pred_class = dplyr::case_when(1 - 1/(1 + exp(dplyr::case_when(b ...</span></span>
<span><span class='c'>#&gt; • .pred_Adelie = 1 - 1/(1 + exp(dplyr::case_when(bill_length_mm &lt; ...</span></span>
<span><span class='c'>#&gt; • .pred_not_Adelie = 1 - (1 - 1/(1 + exp(dplyr::case_when(bill_len ...</span></span>
<span><span class='c'>#&gt; • .pred_class = dplyr::case_when( .pred_Adelie &gt; 0.5 + 0.2 ~ 'Adel ...</span></span>
<span><span class='c'>#&gt; ─────────────────────────────────────────────────────────────────────────</span></span>
<span><span class='c'>#&gt; 6 equations in total.</span></span>
<span></span></code></pre>
</div>
<p>This object contains all the information that is needed to produce predictions. Which we can produce with <a href="https://rdrr.io/r/stats/predict.html" target="_blank" rel="noopener"><code>predict()</code></a>
.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>preds</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/stats/predict.html'>predict</a></span><span class='o'>(</span><span class='nv'>orbital_obj</span>, <span class='nv'>penguins</span><span class='o'>)</span></span>
<span><span class='nv'>preds</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 344 × 3</span></span></span>
<span><span class='c'>#&gt;    .pred_class .pred_Adelie .pred_not_Adelie</span></span>
<span><span class='c'>#&gt;    <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>              <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span>            <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 1</span> Adelie             0.845            0.155</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 2</span> Adelie             0.845            0.155</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 3</span> Adelie             0.845            0.155</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 4</span> not_Adelie         0.291            0.709</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 5</span> Adelie             0.845            0.155</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 6</span> Adelie             0.845            0.155</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 7</span> Adelie             0.845            0.155</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 8</span> Adelie             0.845            0.155</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 9</span> Adelie             0.845            0.155</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>10</span> Adelie             0.845            0.155</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># ℹ 334 more rows</span></span></span>
<span></span></code></pre>
</div>
<p>The predictions are working; however, we don&rsquo;t see any evidence that <code>adjust_equivocal_zone()</code> is working. A call to <code>count()</code> reveals that a couple of observation lands in the equivocal zone.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>count</span><span class='o'>(</span><span class='nv'>preds</span>, <span class='nv'>.pred_class</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 3 × 2</span></span></span>
<span><span class='c'>#&gt;   .pred_class     n</span></span>
<span><span class='c'>#&gt;   <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>       <span style='color: #555555; font-style: italic;'>&lt;int&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>1</span> Adelie        144</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>2</span> [EQ]           15</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>3</span> not_Adelie    185</span></span>
<span></span></code></pre>
</div>
<p>And we can further verify that they are correct.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/stats/filter.html'>filter</a></span><span class='o'>(</span><span class='nv'>preds</span>, <span class='nv'>.pred_class</span> <span class='o'>==</span> <span class='s'>'[EQ]'</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'># A tibble: 15 × 3</span></span></span>
<span><span class='c'>#&gt;    .pred_class .pred_Adelie .pred_not_Adelie</span></span>
<span><span class='c'>#&gt;    <span style='color: #555555; font-style: italic;'>&lt;chr&gt;</span>              <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span>            <span style='color: #555555; font-style: italic;'>&lt;dbl&gt;</span></span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 1</span> [EQ]               0.483            0.517</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 2</span> [EQ]               0.483            0.517</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 3</span> [EQ]               0.483            0.517</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 4</span> [EQ]               0.483            0.517</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 5</span> [EQ]               0.483            0.517</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 6</span> [EQ]               0.483            0.517</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 7</span> [EQ]               0.483            0.517</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 8</span> [EQ]               0.348            0.652</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'> 9</span> [EQ]               0.348            0.652</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>10</span> [EQ]               0.348            0.652</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>11</span> [EQ]               0.348            0.652</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>12</span> [EQ]               0.348            0.652</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>13</span> [EQ]               0.483            0.517</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>14</span> [EQ]               0.483            0.517</span></span>
<span><span class='c'>#&gt; <span style='color: #555555;'>15</span> [EQ]               0.483            0.517</span></span>
<span></span></code></pre>
</div>
<h2 id="new-show_query-method">New show_query method
</h2>
<p>One of the main purposes of orbital is to allow for predictions in databases.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://dbi.r-dbi.org'>DBI</a></span><span class='o'>)</span></span>
<span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://rsqlite.r-dbi.org'>RSQLite</a></span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>con_sqlite</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://dbi.r-dbi.org/reference/dbConnect.html'>dbConnect</a></span><span class='o'>(</span><span class='nf'><a href='https://rsqlite.r-dbi.org/reference/SQLite.html'>SQLite</a></span><span class='o'>(</span><span class='o'>)</span>, path <span class='o'>=</span> <span class='s'>":memory:"</span><span class='o'>)</span></span>
<span><span class='nv'>penguins_sqlite</span> <span class='o'>&lt;-</span> <span class='nf'>copy_to</span><span class='o'>(</span><span class='nv'>con_sqlite</span>, <span class='nv'>penguins</span>, name <span class='o'>=</span> <span class='s'>"penguins_table"</span><span class='o'>)</span></span></code></pre>
</div>
<p>Having set up a database we could have used <a href="https://orbital.tidymodels.org/reference/orbital_sql.html" target="_blank" rel="noopener"><code>orbital_sql()</code></a>
 to show what the SQL query would have looked like. For quick testing, the output isn&rsquo;t immediately ready to be pasted into its own file due to the <code>&lt;SQL&gt;</code> fragments within the output.</p>
<p>The <code>show_query()</code> method has been implemented to see exactly what the generated SQL looks like.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'>show_query</span><span class='o'>(</span><span class='nv'>orbital_obj</span>, <span class='nv'>con_sqlite</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; CASE WHEN ((`bill_length_mm` IS NULL)) THEN 43.9219298245614 WHEN NOT ((`bill_length_mm` IS NULL)) THEN `bill_length_mm` END AS bill_length_mm</span></span>
<span><span class='c'>#&gt; CASE WHEN ((`flipper_length_mm` IS NULL)) THEN 201.0 WHEN NOT ((`flipper_length_mm` IS NULL)) THEN `flipper_length_mm` END AS flipper_length_mm</span></span>
<span><span class='c'>#&gt; CASE</span></span>
<span><span class='c'>#&gt; WHEN ((1.0 - 1.0 / (1.0 + EXP(((((CASE</span></span>
<span><span class='c'>#&gt; WHEN (`bill_length_mm` &lt; 42.4000015) THEN 0.627138138</span></span>
<span><span class='c'>#&gt; WHEN ((`bill_length_mm` &gt;= 42.4000015 OR (`bill_length_mm` IS NULL))) THEN (-0.449751347)</span></span>
<span><span class='c'>#&gt; END + CASE</span></span>
<span><span class='c'>#&gt; WHEN (`bill_length_mm` &lt; 43.2999992) THEN 0.425288886</span></span>
<span><span class='c'>#&gt; WHEN ((`bill_length_mm` &gt;= 43.2999992 OR (`bill_length_mm` IS NULL))) THEN (-0.398178101)</span></span>
<span><span class='c'>#&gt; END) + CASE</span></span>
<span><span class='c'>#&gt; WHEN (`bill_length_mm` &lt; 42.4000015) THEN 0.380251437</span></span>
<span><span class='c'>#&gt; WHEN ((`bill_length_mm` &gt;= 42.4000015 OR (`bill_length_mm` IS NULL))) THEN (-0.306771189)</span></span>
<span><span class='c'>#&gt; END) + CASE</span></span>
<span><span class='c'>#&gt; WHEN (`bill_length_mm` &lt; 44.4000015) THEN 0.286071777</span></span>
<span><span class='c'>#&gt; WHEN ((`bill_length_mm` &gt;= 44.4000015 OR (`bill_length_mm` IS NULL))) THEN (-0.330096036)</span></span>
<span><span class='c'>#&gt; END) + CASE</span></span>
<span><span class='c'>#&gt; WHEN (`flipper_length_mm` &lt; 203.0) THEN 0.209298179</span></span>
<span><span class='c'>#&gt; WHEN ((`flipper_length_mm` &gt;= 203.0 OR (`flipper_length_mm` IS NULL))) THEN (-0.348002464)</span></span>
<span><span class='c'>#&gt; END) + LOG(0.44186047 / (1.0 - 0.44186047))))) &gt; 0.5) THEN 'Adelie'</span></span>
<span><span class='c'>#&gt; ELSE 'not_Adelie'</span></span>
<span><span class='c'>#&gt; END AS .pred_class</span></span>
<span><span class='c'>#&gt; 1.0 - 1.0 / (1.0 + EXP(((((CASE</span></span>
<span><span class='c'>#&gt; WHEN (`bill_length_mm` &lt; 42.4000015) THEN 0.627138138</span></span>
<span><span class='c'>#&gt; WHEN ((`bill_length_mm` &gt;= 42.4000015 OR (`bill_length_mm` IS NULL))) THEN (-0.449751347)</span></span>
<span><span class='c'>#&gt; END + CASE</span></span>
<span><span class='c'>#&gt; WHEN (`bill_length_mm` &lt; 43.2999992) THEN 0.425288886</span></span>
<span><span class='c'>#&gt; WHEN ((`bill_length_mm` &gt;= 43.2999992 OR (`bill_length_mm` IS NULL))) THEN (-0.398178101)</span></span>
<span><span class='c'>#&gt; END) + CASE</span></span>
<span><span class='c'>#&gt; WHEN (`bill_length_mm` &lt; 42.4000015) THEN 0.380251437</span></span>
<span><span class='c'>#&gt; WHEN ((`bill_length_mm` &gt;= 42.4000015 OR (`bill_length_mm` IS NULL))) THEN (-0.306771189)</span></span>
<span><span class='c'>#&gt; END) + CASE</span></span>
<span><span class='c'>#&gt; WHEN (`bill_length_mm` &lt; 44.4000015) THEN 0.286071777</span></span>
<span><span class='c'>#&gt; WHEN ((`bill_length_mm` &gt;= 44.4000015 OR (`bill_length_mm` IS NULL))) THEN (-0.330096036)</span></span>
<span><span class='c'>#&gt; END) + CASE</span></span>
<span><span class='c'>#&gt; WHEN (`flipper_length_mm` &lt; 203.0) THEN 0.209298179</span></span>
<span><span class='c'>#&gt; WHEN ((`flipper_length_mm` &gt;= 203.0 OR (`flipper_length_mm` IS NULL))) THEN (-0.348002464)</span></span>
<span><span class='c'>#&gt; END) + LOG(0.44186047 / (1.0 - 0.44186047)))) AS .pred_Adelie</span></span>
<span><span class='c'>#&gt; 1.0 - (1.0 - 1.0 / (1.0 + EXP(((((CASE</span></span>
<span><span class='c'>#&gt; WHEN (`bill_length_mm` &lt; 42.4000015) THEN 0.627138138</span></span>
<span><span class='c'>#&gt; WHEN ((`bill_length_mm` &gt;= 42.4000015 OR (`bill_length_mm` IS NULL))) THEN (-0.449751347)</span></span>
<span><span class='c'>#&gt; END + CASE</span></span>
<span><span class='c'>#&gt; WHEN (`bill_length_mm` &lt; 43.2999992) THEN 0.425288886</span></span>
<span><span class='c'>#&gt; WHEN ((`bill_length_mm` &gt;= 43.2999992 OR (`bill_length_mm` IS NULL))) THEN (-0.398178101)</span></span>
<span><span class='c'>#&gt; END) + CASE</span></span>
<span><span class='c'>#&gt; WHEN (`bill_length_mm` &lt; 42.4000015) THEN 0.380251437</span></span>
<span><span class='c'>#&gt; WHEN ((`bill_length_mm` &gt;= 42.4000015 OR (`bill_length_mm` IS NULL))) THEN (-0.306771189)</span></span>
<span><span class='c'>#&gt; END) + CASE</span></span>
<span><span class='c'>#&gt; WHEN (`bill_length_mm` &lt; 44.4000015) THEN 0.286071777</span></span>
<span><span class='c'>#&gt; WHEN ((`bill_length_mm` &gt;= 44.4000015 OR (`bill_length_mm` IS NULL))) THEN (-0.330096036)</span></span>
<span><span class='c'>#&gt; END) + CASE</span></span>
<span><span class='c'>#&gt; WHEN (`flipper_length_mm` &lt; 203.0) THEN 0.209298179</span></span>
<span><span class='c'>#&gt; WHEN ((`flipper_length_mm` &gt;= 203.0 OR (`flipper_length_mm` IS NULL))) THEN (-0.348002464)</span></span>
<span><span class='c'>#&gt; END) + LOG(0.44186047 / (1.0 - 0.44186047))))) AS .pred_not_Adelie</span></span>
<span><span class='c'>#&gt; CASE</span></span>
<span><span class='c'>#&gt; WHEN (`.pred_Adelie` &gt; (0.5 + 0.2)) THEN 'Adelie'</span></span>
<span><span class='c'>#&gt; WHEN (`.pred_Adelie` &lt; (0.5 - 0.2)) THEN 'not_Adelie'</span></span>
<span><span class='c'>#&gt; ELSE '[EQ]'</span></span>
<span><span class='c'>#&gt; END AS .pred_class</span></span>
<span></span></code></pre>
</div>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thank you to all the people who have contributed to orbital since the release of v0.4.0:</p>
<p><a href="https://github.com/EmilHvitfeldt" target="_blank" rel="noopener">@EmilHvitfeldt</a>
, <a href="https://github.com/frankiethull" target="_blank" rel="noopener">@frankiethull</a>
, <a href="https://github.com/jeroenjanssens" target="_blank" rel="noopener">@jeroenjanssens</a>
, and <a href="https://github.com/topepo" target="_blank" rel="noopener">@topepo</a>
.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/orbital-0-4-0/thumbnail-wd.jpg" length="493114" type="image/jpeg" />
    </item>
    <item>
      <title>yaml12: YAML 1.2 for R and Python</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/yaml12-0-1-0/</link>
      <pubDate>Wed, 07 Jan 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/yaml12-0-1-0/</guid>
      <dc:creator>Tomasz Kalinowski</dc:creator><description><![CDATA[<!--
TODO:
* [x] Look over / edit the post's title in the yaml
* [x] Edit (or delete) the description; note this appears in the Twitter card
* [x] Pick category and tags (see existing with [`hugodown::tidy_show_meta()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html))
* [x] Find photo & update yaml metadata
* [x] Create `thumbnail-sq.jpg`; height and width should be equal
* [x] Create `thumbnail-wd.jpg`; width should be >5x height
* [x] [`hugodown::use_tidy_thumbnails()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html)
* [x] Add intro sentence, e.g. the standard tagline for the package
* [ ] [`usethis::use_tidy_thanks()`](https://usethis.r-lib.org/reference/use_tidy_thanks.html) (optional)
-->
<p>Today we&rsquo;re announcing two new packages for parsing and emitting YAML 1.2: <a href="https://posit-dev.github.io/r-yaml12/" target="_blank" rel="noopener"><code>yaml12</code></a>
 for R and <a href="https://posit-dev.github.io/py-yaml12/" target="_blank" rel="noopener"><code>py-yaml12</code></a>
 for Python.</p>
<p>Both packages are implemented in Rust and built on the excellent <a href="https://github.com/saphyr-rs/saphyr" target="_blank" rel="noopener"><code>saphyr</code></a>
 crate. They share the same design goals: predictable YAML 1.2 typing, explicit control over tag interpretation via handlers, and clean round-tripping of unhandled tags.</p>
<p>Before we get into the details, a quick note on how this relates to the existing R <a href="https://github.com/r-lib/yaml" target="_blank" rel="noopener"><code>yaml</code></a>
 package. The R <code>yaml</code> package is now in <a href="https://github.com/r-lib" target="_blank" rel="noopener">r-lib</a>
, and we&rsquo;ve taken over maintenance after years of stewardship by its original author, Jeremy Stephens, and later by Shawn Garbett.</p>
<p>If <code>yaml</code> already works for you, there&rsquo;s no need to switch. <code>yaml12</code> is an experiment providing consistent R and Python bindings to a new Rust library specifically for YAML 1.2, which, as we&rsquo;ll see below, has some particular advantages.</p>
<h2 id="install">Install
</h2>
<p>Install the R package from CRAN:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/utils/install.packages.html'>install.packages</a></span><span class='o'>(</span><span class='s'>"yaml12"</span><span class='o'>)</span></span></code></pre>
</div>
<p>Install the Python package from PyPI:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install py-yaml12
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="quick-start-r">Quick start (R)
</h2>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://posit-dev.github.io/r-yaml12/'>yaml12</a></span><span class='o'>)</span></span>
<span></span>
<span><span class='nv'>yaml</span> <span class='o'>&lt;-</span> <span class='s'>"</span></span>
<span><span class='s'>title: A modern YAML parser and emitter written in Rust</span></span>
<span><span class='s'>properties: [fast, correct, safe, simple]</span></span>
<span><span class='s'>"</span></span>
<span></span>
<span><span class='nv'>doc</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='nv'>yaml</span><span class='o'>)</span></span>
<span><span class='nf'><a href='https://rdrr.io/r/utils/str.html'>str</a></span><span class='o'>(</span><span class='nv'>doc</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; List of 2</span></span>
<span><span class='c'>#&gt;  $ title     : chr "A modern YAML parser and emitter written in Rust"</span></span>
<span><span class='c'>#&gt;  $ properties: chr [1:4] "fast" "correct" "safe" "simple"</span></span>
<span></span></code></pre>
</div>
<p>Round-trip back to YAML:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>obj</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span></span>
<span>  seq <span class='o'>=</span> <span class='m'>1</span><span class='o'>:</span><span class='m'>2</span>,</span>
<span>  map <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span>key <span class='o'>=</span> <span class='s'>"value"</span><span class='o'>)</span>,</span>
<span>  tagged <span class='o'>=</span> <span class='nf'><a href='https://rdrr.io/r/base/structure.html'>structure</a></span><span class='o'>(</span><span class='s'>"1 + 1"</span>, yaml_tag <span class='o'>=</span> <span class='s'>"!expr"</span><span class='o'>)</span></span>
<span><span class='o'>)</span></span>
<span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/format_yaml.html'>write_yaml</a></span><span class='o'>(</span><span class='nv'>obj</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; ---</span></span>
<span><span class='c'>#&gt; seq:</span></span>
<span><span class='c'>#&gt;   - 1</span></span>
<span><span class='c'>#&gt;   - 2</span></span>
<span><span class='c'>#&gt; map:</span></span>
<span><span class='c'>#&gt;   key: value</span></span>
<span><span class='c'>#&gt; tagged: !expr 1 + 1</span></span>
<span><span class='c'>#&gt; ...</span></span>
<span></span><span></span>
<span><span class='nf'><a href='https://rdrr.io/r/base/identical.html'>identical</a></span><span class='o'>(</span><span class='nv'>obj</span>, <span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/format_yaml.html'>format_yaml</a></span><span class='o'>(</span><span class='nv'>obj</span><span class='o'>)</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] TRUE</span></span>
<span></span></code></pre>
</div>
<h2 id="quick-start-python">Quick start (Python)
</h2>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'># Install from PyPI:
#   python -m pip install py-yaml12
from yaml12 import parse_yaml, format_yaml, Yaml

yaml_text = """
title: A modern YAML parser and emitter written in Rust
properties: [fast, correct, safe, simple]
"""

doc = parse_yaml(yaml_text)

assert doc == {
  "title": "A modern YAML parser and emitter written in Rust",
  "properties": ["fast", "correct", "safe", "simple"]
}

assert doc == parse_yaml(format_yaml(doc))

# Tagged values
tagged = parse_yaml("!expr 1 + 1")
assert tagged == Yaml(value="1 + 1", tag="!expr")
</code></pre>
</div>
<h2 id="why-yaml-12">Why YAML 1.2?
</h2>
<p>YAML 1.2 tightened up a number of ambiguous implicit conversions. In particular, plain scalars like <code>on</code>/<code>off</code>/<code>yes</code>/<code>no</code>/<code>y</code>/<code>n</code> are strings in the 1.2 core schema, and YAML 1.2 removed sexagesimal (base-60) parsing, so values like <code>1:2</code> are not treated as numbers.</p>
<p>YAML 1.2 also removed <code>!!timestamp</code>, <code>!!binary</code>, and <code>!!omap</code> from the set of core types, which further reduces implicit coercions (for example, getting a date/time object when you expected a string). If you want to interpret those values, you can do so explicitly via tags and handlers.</p>
<p>That makes YAML a better default for configuration files, front matter, and data interchange. You get fewer surprises and fewer &ldquo;why did this become a boolean?&rdquo; moments (or &ldquo;why did this become a date?&rdquo;).</p>
<h2 id="highlights">Highlights
</h2>
<h3 id="a-consistent-api-in-r-and-python">A consistent API in R and Python
</h3>
<p>The two packages intentionally share the same high-level functions:</p>
<ul>
<li><a href="https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html" target="_blank" rel="noopener"><code>parse_yaml()</code></a>
: Parse YAML from a string</li>
<li><a href="https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html" target="_blank" rel="noopener"><code>read_yaml()</code></a>
: Read YAML from a file</li>
<li><a href="https://posit-dev.github.io/r-yaml12/reference/format_yaml.html" target="_blank" rel="noopener"><code>format_yaml()</code></a>
: Format values as YAML (to a string)</li>
<li><a href="https://posit-dev.github.io/r-yaml12/reference/format_yaml.html" target="_blank" rel="noopener"><code>write_yaml()</code></a>
: Write YAML to a file (or stdout)</li>
</ul>
<h3 id="tags-and-handlers-opt-in-meaning-safe-defaults">Tags and handlers (opt-in, meaning, safe defaults)
</h3>
<p>In YAML, tags are explicit annotations like <code>!expr</code> or <code>!!timestamp</code> that attach type and meaning to a value.</p>
<p>Tags are preserved by default:</p>
<ul>
<li>In R, tags are kept in a <code>yaml_tag</code> attribute.</li>
<li>In Python, tags are kept by wrapping values in a <code>Yaml()</code> object.</li>
</ul>
<p>Handlers let you opt into custom behavior for tags (including tags on mapping keys) while keeping parsing as a data-only operation by default.</p>
<p>If you used R <code>yaml</code>&rsquo;s <code>!expr</code> tag to evaluate expressions, you can recreate that behavior by registering a handler, but it&rsquo;s only recommended when parsing trusted YAML, since evaluating arbitrary code is a security risk. For untrusted input, the default behavior is safer because it keeps <code>!expr</code> as data and does not execute code.</p>
<p>R example:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='c'># by default, tags are kept as data</span></span>
<span><span class='nf'><a href='https://rdrr.io/r/base/dput.html'>dput</a></span><span class='o'>(</span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='s'>"!expr 1 + 1"</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; structure("1 + 1", yaml_tag = "!expr")</span></span>
<span></span><span></span>
<span><span class='c'># Add a handler to process tagged nodes (like the &#123;yaml&#125; package does)</span></span>
<span><span class='nv'>handlers</span> <span class='o'>&lt;-</span> <span class='nf'><a href='https://rdrr.io/r/base/list.html'>list</a></span><span class='o'>(</span><span class='s'>"!expr"</span> <span class='o'>=</span> \<span class='o'>(</span><span class='nv'>x</span><span class='o'>)</span> <span class='nf'><a href='https://rdrr.io/r/base/eval.html'>eval</a></span><span class='o'>(</span><span class='nf'><a href='https://rdrr.io/r/base/parse.html'>str2expression</a></span><span class='o'>(</span><span class='nv'>x</span><span class='o'>)</span>, <span class='nf'><a href='https://rdrr.io/r/base/environment.html'>globalenv</a></span><span class='o'>(</span><span class='o'>)</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='s'>"!expr 1 + 1"</span>, handlers <span class='o'>=</span> <span class='nv'>handlers</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1] 2</span></span>
<span></span></code></pre>
</div>
<p>Python example:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'>from yaml12 import parse_yaml

handlers = {"!expr": eval}  # use with trusted input only
parse_yaml("!expr 1 + 1", handlers=handlers)

#> 2
</code></pre>
</div>
<h3 id="simplification-and-missing-values-r">Simplification and missing values (R)
</h3>
<p>In R, <a href="https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html" target="_blank" rel="noopener"><code>parse_yaml()</code></a>
 can simplify homogeneous sequences to vectors. When it does, YAML <code>null</code> becomes the appropriate <code>NA</code> type:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='s'>"[1, 2, 3, null]"</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; [1]  1  2  3 NA</span></span>
<span></span><span></span>
<span><span class='nf'><a href='https://rdrr.io/r/utils/str.html'>str</a></span><span class='o'>(</span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='s'>"[1, 2, 3, null]"</span>, simplify <span class='o'>=</span> <span class='kc'>FALSE</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; List of 4</span></span>
<span><span class='c'>#&gt;  $ : int 1</span></span>
<span><span class='c'>#&gt;  $ : int 2</span></span>
<span><span class='c'>#&gt;  $ : int 3</span></span>
<span><span class='c'>#&gt;  $ : NULL</span></span>
<span></span></code></pre>
</div>
<h3 id="non-string-mapping-keys">Non-string mapping keys
</h3>
<p>YAML allows mapping keys that aren&rsquo;t plain strings (numbers, booleans, tagged scalars, even sequences and mappings). Both packages preserve these safely:</p>
<ul>
<li>In R, you&rsquo;ll get a regular named list plus a <code>yaml_keys</code> attribute when needed.</li>
<li>In Python, unhashable keys (like lists/dicts) are wrapped in <code>Yaml</code> so they can still be used as <code>dict</code> keys and round-trip correctly.</li>
</ul>
<p>R example:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/base/dput.html'>dput</a></span><span class='o'>(</span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='s'>"&#123;a: b&#125;: c"</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; structure(list("c"), names = "", yaml_keys = list(list(a = "b")))</span></span>
<span></span></code></pre>
</div>
<p>Python example:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'>from yaml12 import parse_yaml, Yaml

doc = parse_yaml("{a: b}: c")
assert doc == {Yaml({'a': 'b'}): 'c'}
</code></pre>
</div>
<h3 id="mapping-order-is-preserved">Mapping order is preserved
</h3>
<p>YAML mappings are ordered. <code>yaml12</code> preserves mapping/dictionary order when parsing and formatting, so the order you see in a YAML file (or emit) round-trips in both R and Python.</p>
<h3 id="document-streams-and-front-matter">Document streams and front matter
</h3>
<p>Both packages support multi-document YAML streams with <code>multi = TRUE</code>. When <code>multi = FALSE</code> (the default), parsing stops after the first document, which is handy for extracting YAML front matter from text that continues with non-YAML content.</p>
<p>Example:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>yaml</span> <span class='o'>&lt;-</span> <span class='s'>"</span></span>
<span><span class='s'>---</span></span>
<span><span class='s'>title: Extracting YAML front matter</span></span>
<span><span class='s'>---</span></span>
<span><span class='s'>This is technically now the second document in a YAML stream</span></span>
<span><span class='s'>"</span></span>
<span><span class='nf'><a href='https://rdrr.io/r/utils/str.html'>str</a></span><span class='o'>(</span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='nv'>yaml</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; List of 1</span></span>
<span><span class='c'>#&gt;  $ title: chr "Extracting YAML front matter"</span></span>
<span></span><span><span class='nf'><a href='https://rdrr.io/r/utils/str.html'>str</a></span><span class='o'>(</span><span class='nf'><a href='https://posit-dev.github.io/r-yaml12/reference/parse_yaml.html'>parse_yaml</a></span><span class='o'>(</span><span class='nv'>yaml</span>, multi <span class='o'>=</span> <span class='kc'>TRUE</span><span class='o'>)</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; List of 2</span></span>
<span><span class='c'>#&gt;  $ :List of 1</span></span>
<span><span class='c'>#&gt;   ..$ title: chr "Extracting YAML front matter"</span></span>
<span><span class='c'>#&gt;  $ : chr "This is technically now the second document in a YAML stream"</span></span>
<span></span></code></pre>
</div>
<h3 id="performance-and-safety-notes">Performance and safety notes
</h3>
<p><code>yaml12</code> is implemented in Rust and written with performance and safety in mind. It avoids unnecessary allocations, copies, and extra traversals where possible. In Python, <code>py-yaml12</code> (imported as <code>yaml12</code>) also releases the GIL for large parses and serializations.</p>
<p>In typical usage, the R package <code>yaml12</code> is ~2× faster than the <code>yaml</code> package, and the Python package <code>py-yaml12</code> is ≥50× faster than default <code>PyYAML</code> in the benchmarks (<a href="https://posit-dev.github.io/r-yaml12/articles/benchmarks.html" target="_blank" rel="noopener">R benchmarks</a>
; <a href="https://posit-dev.github.io/py-yaml12/benchmarks/#read-performance" target="_blank" rel="noopener">Python benchmarks</a>
).</p>
<p>Tags are preserved by default, and interpreting them (including any kind of evaluation) is always an explicit opt-in via handlers. Plain scalars follow the YAML 1.2 core schema rules for predictable typing.</p>
<p>In Python, <code>py-yaml12</code> ships prebuilt wheels for common platforms. If you do need to build from source, you&rsquo;ll need a Rust toolchain. In R, <code>yaml12</code> is available from CRAN (including binaries on common platforms).</p>
<h2 id="wrapping-up">Wrapping up
</h2>
<p>If you work with YAML as a data format for configuration, front matter, or data interchange, we hope <code>yaml12</code> (R) and <code>py-yaml12</code> (Python) help you parse and emit YAML 1.2 predictably. If you run into YAML that doesn&rsquo;t behave as expected, we&rsquo;d love to hear about it in the issue trackers: <a href="https://github.com/posit-dev/r-yaml12/issues" target="_blank" rel="noopener">r-yaml12</a>
 and <a href="https://github.com/posit-dev/py-yaml12/issues" target="_blank" rel="noopener">py-yaml12</a>
.</p>
<h2 id="learn-more">Learn more
</h2>
<ul>
<li>R package docs: <a href="https://posit-dev.github.io/r-yaml12/" target="_blank" rel="noopener">https://posit-dev.github.io/r-yaml12/</a>
</li>
<li>R package on CRAN: <a href="https://cran.r-project.org/package=yaml12" target="_blank" rel="noopener">https://cran.r-project.org/package=yaml12</a>
</li>
<li>Python package docs: <a href="https://posit-dev.github.io/py-yaml12/" target="_blank" rel="noopener">https://posit-dev.github.io/py-yaml12/</a>
</li>
<li>Python package on PyPI: <a href="https://pypi.org/project/py-yaml12/" target="_blank" rel="noopener">https://pypi.org/project/py-yaml12/</a>
</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>Both packages build on the fantastic work in the YAML ecosystem, especially the <code>saphyr</code> Rust crate and the <a href="https://github.com/yaml/yaml-test-suite" target="_blank" rel="noopener">yaml-test-suite</a>
.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/yaml12-0-1-0/thumbnail-wd.jpg" length="477241" type="image/jpeg" />
    </item>
    <item>
      <title>tidymodels &amp; xgboost</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/2025/tidymodels-xgboost/</link>
      <pubDate>Mon, 15 Dec 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/2025/tidymodels-xgboost/</guid>
      <dc:creator>Emil Hvitfeldt</dc:creator><description><![CDATA[<p>The <a href="https://xgboost.readthedocs.io/en/stable/r_docs/R-package/docs/index.html" target="_blank" rel="noopener">xgboost</a>
 library has recently gotten a big CRAN release. Jumping from version 1.7.11.1 to 3.1.2.1. We at the tidymodels team have been following the development and have done our best to ensure that your experience is unaffected by this release.</p>
<p>In addition to all the new features and improvements that are now available for users relying on CRAN versions of packages, there are also a few breaking changes. Specifically between version 1.x and 2.x of the xgboost library. The xgboost team has kindly provided a <a href="https://xgboost.readthedocs.io/en/stable/R-package/migration_guide.html" target="_blank" rel="noopener">migration guide</a>
 for how to update your code if you are upgrading from before version 2.x.</p>
<p>If you are using xgboost purely through tidymodels via functions like <a href="https://parsnip.tidymodels.org/reference/boost_tree.html" target="_blank" rel="noopener"><code>parsnip::boost_tree()</code></a>
 and <a href="https://embed.tidymodels.org/reference/step_discretize_xgb.html" target="_blank" rel="noopener"><code>embed::step_discretize_xgb()</code></a>
, you should not need to change anything, as we have updated our packages to work with both the new and old versions of xgboost. If you are having any issues, please let us know by filing an issue for the affected package.</p>
<p>We look forward to integrating parsnip more deeply into these new changes, such as support for <a href="https://xgboost.readthedocs.io/en/stable/tutorials/categorical.html" target="_blank" rel="noopener">categorical predictors</a>
 and <a href="https://xgboost.readthedocs.io/en/stable/python/examples/quantile_regression.html#quantile-regression" target="_blank" rel="noopener">quantile regression</a>
.</p>
<p>Here are the package that we&rsquo;ve updated or helped the maintainers update</p>
<ul>
<li><a href="https://rstudio.github.io/bundle/dev/news/index.html#bundle-013" target="_blank" rel="noopener">bundle</a>
</li>
<li><a href="https://butcher.tidymodels.org/news/index.html#butcher-040" target="_blank" rel="noopener">butcher</a>
</li>
<li><a href="https://embed.tidymodels.org/news/index.html#embed-121" target="_blank" rel="noopener">embed</a>
</li>
<li><a href="https://github.com/tidymodels/lime/releases/tag/v0.5.4" target="_blank" rel="noopener">lime</a>
</li>
<li><a href="https://business-science.github.io/modeltime/" target="_blank" rel="noopener">modeltime</a>
</li>
<li><a href="https://orbital.tidymodels.org/news/index.html#orbital-041" target="_blank" rel="noopener">orbital</a>
</li>
<li><a href="https://parsnip.tidymodels.org/news/index.html#parsnip-140" target="_blank" rel="noopener">parsnip</a>
</li>
<li><a href="https://tidypredict.tidymodels.org/news/index.html#tidypredict-100" target="_blank" rel="noopener">tidypredict</a>
</li>
<li><a href="https://rstudio.github.io/vetiver-r/dev/news/index.html#vetiver-027" target="_blank" rel="noopener">vetiver</a>
</li>
<li><a href="https://github.com/holub008/xrf/releases/tag/0.3.0" target="_blank" rel="noopener">xf</a>
</li>
</ul>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/2025/tidymodels-xgboost/thumbnail-wd.jpg" length="283800" type="image/jpeg" />
    </item>
    <item>
      <title>OpenTelemetry &#43; Shiny for R v1.12</title>
      <link>https://posit-open-source.netlify.app/blog/shiny/shiny-r-1.12/</link>
      <pubDate>Wed, 10 Dec 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/shiny/shiny-r-1.12/</guid>
      <dc:creator>Barret Schloerke</dc:creator><description><![CDATA[<style>
img { border-radius: 8px; }
</style>
<p>We&rsquo;re thrilled to announce the release of Shiny v1.12! This release brings a powerful new feature that we&rsquo;ve been working on for months: <strong>built-in OpenTelemetry support</strong>. Whether you&rsquo;re building small apps or deploying production applications at scale, this release will help you lift the veil on understanding your app&rsquo;s execution in production.</p>
<details class="callout callout-note" role="note" aria-label="Note">
<summary class="callout-header">
<span class="callout-title">What is Shiny?</span>
</summary>
<div class="callout-body">
<p>If you&rsquo;re new to Shiny, welcome! <a href="https://shiny.posit.co/r/" target="_blank" rel="noopener">Shiny</a>
 is an R package that makes it easy to build interactive web applications directly from R. You don&rsquo;t need to be a web developer&mdash;if you can write R code, you can create beautiful, interactive dashboards, data explorers, and analytical tools. Shiny handles all the web programming complexity behind the scenes, letting you focus on what you do best: working with data and building analyses.</p>
<p>Since its launch in 2012, Shiny has become the go-to framework for creating data-driven web applications in R, powering everything from internal company dashboards to public-facing data visualization tools. With this latest release, we&rsquo;re making it easier than ever to understand what&rsquo;s happening inside your Shiny apps, especially when they&rsquo;re deployed in production environments.</p>
</div>
</details>
<details class="callout callout-note" role="note" aria-label="Note">
<summary class="callout-header">
<span class="callout-title">What about Shiny for Python?</span>
</summary>
<div class="callout-body">
<p>OpenTelemetry support is coming to Shiny for Python! The Shiny team is actively working on bringing the same automatic instrumentation capabilities to Python. This will enable Python developers to gain the same level of observability into their Shiny applications.</p>
<p>Stay tuned for future announcements about OpenTelemetry integration in Shiny for Python. In the meantime, you can follow the development on the <a href="https://github.com/posit-dev/py-shiny" target="_blank" rel="noopener">Shiny for Python GitHub repository</a>
.</p>
</div>
</details>
<h2 id="understanding-opentelemetry">Understanding OpenTelemetry
</h2>
<p>Before we dive into what&rsquo;s new in Shiny, let&rsquo;s talk about OpenTelemetry&mdash;a topic that might sound intimidating but is actually quite straightforward once we cover the basics.</p>
<h3 id="what-is-opentelemetry">What is OpenTelemetry?
</h3>
<p><a href="https://opentelemetry.io/" target="_blank" rel="noopener"><strong>OpenTelemetry</strong></a>
 (aka OTel) describes itself as &ldquo;high-quality, ubiquitous, and portable telemetry to enable effective observability&rdquo;. In simpler terms, OpenTelemetry is a set of tools, APIs, and SDKs that help you collect and export telemetry data (like traces, logs, and metrics) from your applications. This data provides insights into how your applications are performing and behaving in real-world scenarios.</p>
<p>It captures three key types of data:</p>
<ol>
<li>
<p><strong>Traces</strong>: These show the path of a request through your application. In a Shiny app, a trace might show how a user&rsquo;s input triggered a series of reactive calculations, leading to updated outputs.</p>
</li>
<li>
<p><strong>Logs</strong>: These are detailed event records that capture what happened at specific moments.</p>
</li>
<li>
<p><strong>Metrics</strong>: These are numerical measurements over time, like how many users are connected or how long outputs take to render.</p>
</li>
</ol>
<p>These data types were standardized under the OpenTelemetry project, <a href="https://opentelemetry.io/community/marketing-guidelines/#i-opentelemetry-is-a-joint-effort" target="_blank" rel="noopener">which is supported by a large community and many companies</a>
. The goal is to provide a consistent way to collect and export observability data, making it easier to monitor and troubleshoot applications.</p>
<h3 id="the-opentelemetry-ecosystem">The OpenTelemetry ecosystem
</h3>
<p>OpenTelemetry is vendor-neutral, meaning you can send your telemetry data to various local backends like <a href="https://www.jaegertracing.io/" target="_blank" rel="noopener">Jaeger</a>
, <a href="https://zipkin.io/" target="_blank" rel="noopener">Zipkin</a>
, <a href="https://prometheus.io/" target="_blank" rel="noopener">Prometheus</a>
, or cloud-based services like <a href="https://grafana.com/products/cloud/" target="_blank" rel="noopener">Grafana Cloud</a>
, <a href="https://pydantic.dev/logfire" target="_blank" rel="noopener">Logfire</a>
, and <a href="https://langfuse.com/" target="_blank" rel="noopener">Langfuse</a>
. This flexibility means <a href="https://opentelemetry.io/community/marketing-guidelines/#iii-promote-awareness-of-otel-interoperability-and-modularization" target="_blank" rel="noopener">you&rsquo;re not locked into any particular monitoring solution</a>
.</p>
<p>We&rsquo;ve been using <a href="https://pydantic.dev/logfire" target="_blank" rel="noopener">Logfire</a>
 internally at Posit to help develop OTel integration in many R packages and other applications. Throughout this post, you&rsquo;ll see examples of OTel traces visualized in Logfire.</p>
<p>The image below shows an example trace in Logfire (left) from a Shiny app (right) that uses Generative AI to provide weather forecasts. The trace captures the entire user session, including reactive updates, model calls, and a tool invocation. We will explore this example in more detail later in the post.</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/shiny/shiny-r-1.12/demo-app-and-logfire.png" data-fig-alt="OpenTelemetry trace of chat app with weather tool" alt="OpenTelemetry trace of chat app with weather tool" />
<figcaption aria-hidden="true">OpenTelemetry trace of chat app with weather tool</figcaption>
</figure>
<h2 id="opentelemetry-in-shiny">OpenTelemetry in Shiny
</h2>
<p>If you&rsquo;ve ever wondered&hellip;</p>
<ul>
<li>&ldquo;Why is my app slow for some users when hosted? Which reactive expressions are causing the slowdown?&rdquo;</li>
<li>&ldquo;How long does it take for my plot to render? Is it the data or the plotting code that is taking longer to calculate?&rdquo;</li>
<li>&ldquo;What&rsquo;s the sequence of events leading up to when a user clicks <em>that</em> button?&rdquo;</li>
<li>&ldquo;How often are errors occurring in my app, and under what conditions?&rdquo;</li>
</ul>
<p>Normally, we can attempt to answer these questions using <a href="https://rstudio.github.io/reactlog/" target="_blank" rel="noopener"><code>{reactlog}</code></a>
 (a package to replay the recording of a reactive graph) or <a href="https://profvis.r-lib.org/" target="_blank" rel="noopener"><code>{profvis}</code></a>
 (a package to profile R code in the main R process). However, both of these packages are not built for production use. These debugging tools are only to be used locally as they would be considered a memory leak in production.</p>
<p>OpenTelemetry allows us to record information <strong>at scale</strong> with minimal overhead, helping you answer previously impossible questions about your production environment. OpenTelemetry provides visibility into your app&rsquo;s performance and behavior, helping you identify bottlenecks, debug issues, and optimize user experience&mdash;especially crucial when your app is deployed in production with real-world usage.</p>
<h3 id="adding-opentelemetry-integration">Adding OpenTelemetry integration
</h3>
<p>OTel support is automatically enabled in Shiny once <a href="https://otel.r-lib.org/" target="_blank" rel="noopener"><code>{otel}</code></a>
 is able to record traces and logs.</p>
<p>To do this, let&rsquo;s get started by installing the latest version of Shiny, <code>{otel}</code>, and <a href="https://otelsdk.r-lib.org" target="_blank" rel="noopener"><code>{otelsdk}</code></a>
:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">pak</span><span class="o">::</span><span class="nf">pak</span><span class="p">(</span><span class="nf">c</span><span class="p">(</span><span class="s">&#34;shiny&#34;</span><span class="p">,</span> <span class="s">&#34;otel&#34;</span><span class="p">,</span> <span class="s">&#34;otelsdk&#34;</span><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>To enable OpenTelemetry tracing, you need set a few <a href="https://otelsdk.r-lib.org/reference/collecting.html" target="_blank" rel="noopener">specific system environment variables</a>
 to describe where your recordings are being sent. In the example below, we set them in an <code>.Renviron</code> file to point to Logfire.</p>
<p><strong>.Renviron</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Enable OpenTelemetry by setting Collector environment variables</span>
</span></span><span class="line"><span class="cl"><span class="nv">OTEL_TRACES_EXPORTER</span><span class="o">=</span>http
</span></span><span class="line"><span class="cl"><span class="nv">OTEL_LOGS_EXPORTER</span><span class="o">=</span>http
</span></span><span class="line"><span class="cl"><span class="nv">OTEL_LOG_LEVEL</span><span class="o">=</span>debug
</span></span><span class="line"><span class="cl"><span class="nv">OTEL_METRICS_EXPORTER</span><span class="o">=</span>http
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">OTEL_EXPORTER_OTLP_ENDPOINT</span><span class="o">=</span><span class="s2">&#34;https://logfire-us.pydantic.dev&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">OTEL_EXPORTER_OTLP_HEADERS</span><span class="o">=</span><span class="s2">&#34;Authorization=&lt;your-write-token&gt;&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Editing <code>.Renviron</code></span>
</div>
<div class="callout-body">
<p>You can edit your app-specific environment variables by calling <code>usethis::edit_r_environ(scope=&quot;project&quot;)</code> from within your Shiny app project directory.</p>
</div>
</div>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Verifying OTel setup</span>
</div>
<div class="callout-body">
<p>You&rsquo;ll know your setup is enabled if <a href="https://otel.r-lib.org/reference/is_tracing_enabled.html" target="_blank" rel="noopener"><code>otel::is_tracing_enabled()</code></a>
 returns <code>TRUE</code>.</p>
</div>
</div>
<h3 id="opentelemetry-in-action">OpenTelemetry in action
</h3>
<p>Below is an example <code>{shinychat}</code> app with an <code>{ellmer}</code> tool to fetch realtime weather forecasts (via <code>{weathR}</code>, which uses <code>{httr2}</code>) for a given latitude and longitude. This simple (yet non-trivial) app helps us showcase what sort of information <code>{shiny}</code>, <code>{ellmer}</code>, and <code>{httr2}</code> can surface via OTel.</p>
<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">OTel + GenAI</span>
</div>
<div class="callout-body">
<p>Gaining timing insights into applications that leverage <a href="https://aws.amazon.com/what-is/generative-ai/" target="_blank" rel="noopener">Generative AI</a>
 (GenAI) is critical to improving user experience. Without OpenTelemetry, if a user stated an app was slow, we would not be able to accurately determine if the slowness was due to the AI model request time, AI model streaming time, tool execution time, or even followup reactive calculations in Shiny.</p>
</div>
</div>
<p><strong>app.R</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shiny</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create tool that grabs the weather forecast (free) for a given lat/lon</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Inspired from: https://posit-dev.github.io/shinychat/r/articles/tool-ui.html</span>
</span></span><span class="line"><span class="cl"><span class="n">get_weather_forecast_basic</span> <span class="o">&lt;-</span> <span class="n">ellmer</span><span class="o">::</span><span class="nf">tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">weathR</span><span class="o">::</span><span class="nf">point_tomorrow</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">,</span> <span class="n">short</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">name</span> <span class="o">=</span> <span class="s">&#34;get_weather_forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Get the weather forecast for a location.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">arguments</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">lat</span> <span class="o">=</span> <span class="n">ellmer</span><span class="o">::</span><span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Latitude&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">lon</span> <span class="o">=</span> <span class="n">ellmer</span><span class="o">::</span><span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Longitude&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="n">bslib</span><span class="o">::</span><span class="nf">page_fillable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">shinychat</span><span class="o">::</span><span class="nf">chat_mod_ui</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">height</span> <span class="o">=</span> <span class="s">&#34;100%&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Set up client within `server` to not _share_ the client for all sessions</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">&lt;-</span> <span class="n">ellmer</span><span class="o">::</span><span class="nf">chat_claude</span><span class="p">(</span><span class="s">&#34;Be terse.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span><span class="o">$</span><span class="nf">register_tool</span><span class="p">(</span><span class="n">get_weather_forecast_basic</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">chat_server</span> <span class="o">&lt;-</span> <span class="n">shinychat</span><span class="o">::</span><span class="nf">chat_mod_server</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">client</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Set chat placeholder on app init</span>
</span></span><span class="line"><span class="cl">  <span class="nf">observe</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat_server</span><span class="o">$</span><span class="nf">update_user_input</span><span class="p">(</span><span class="s">&#34;What is the weather in Atlanta, GA?&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span> <span class="n">label</span> <span class="o">=</span> <span class="s">&#34;set-default-input&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>You&rsquo;ll notice that the <code>app.R</code> has no OpenTelemetry specific code.</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/shiny/shiny-r-1.12/demo-app.png" data-fig-alt="Chat app with weather tool" height="500" alt="Chat app with weather tool" />
<figcaption aria-hidden="true">Chat app with weather tool</figcaption>
</figure>
<p>Here&rsquo;s an example trace from Logfire showing a user session interacting with the chat app and the weather tool:</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/shiny/shiny-r-1.12/demo-logfire.png" data-fig-alt="OpenTelemetry trace of chat app with weather tool" alt="OpenTelemetry trace of chat app with weather tool" />
<figcaption aria-hidden="true">OpenTelemetry trace of chat app with weather tool</figcaption>
</figure>
<p>The traces above recorded a single user session where the user asked for the weather in Atlanta, GA and then closed the app. The trace shows:</p>
<ul>
<li>The Shiny session lifecycle, including <code>session_start</code> and <code>session_end</code></li>
<li>Many <code>{shinychat}</code> <code>chat</code> module spans for handling user input and messages</li>
<li>Reactive updates triggered by changes in the <code>session</code>&rsquo;s input</li>
<li>An <code>ExtendedTask</code> span for the computation of the AI agent response</li>
<li>2x <code>chat claude</code> spans representing calls to the AI agent model</li>
<li>A single <code>get_weather_forecast</code> tool call being executed, including the HTTP requests made by <code>{httr2}</code> to fetch the weather data</li>
</ul>
<p>The gap between this span&rsquo;s length and its parent&rsquo;s length is how long the results took to stream back to the user or make a decision. For the overall user experience, the total time taken from input to output is represented by the <code>ExtendedTask</code> span, 9.5 seconds in this case. Only a half of a second was spent in the tool call (something <em>we as app authors</em> could possibly optimize). The remaining 9 seconds was spent in the model response generation and streaming.</p>
<p>Notice how the spans are nested, showing the relationship between user actions, required reactive calculations, and external API calls. This level of detail helps you understand exactly how your app is performing in production and where any bottlenecks or issues may arise.</p>
<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Packages used in demo</span>
</div>
<div class="callout-body">
<p>The Shiny app above currently requires the development version of <code>{ellmer}</code> to record OpenTelemetry traces. More R packages (mentioned later in this post) will contain native OpenTelemetry support in their upcoming releases.</p>
</div>
</div>
<h3 id="what-can-shiny-record">What can Shiny record?
</h3>
<p>Shiny automatically creates OpenTelemetry spans for:</p>
<ul>
<li><strong>Session lifecycle</strong>: When sessions start and end, including HTTP request details</li>
<li><strong>Reactive updates</strong>: The entire cascade of reactive calculations triggered by an input change or a new output to be rendered</li>
<li><strong>Reactive expressions</strong>: Individual <code>reactive()</code>, <code>observe()</code>, and <code>output</code> calculations; <code>debounce()</code> and <code>throttle()</code> value updates; <code>reactiveFileReader()</code> and <code>reactivePoll()</code> updates</li>
<li><strong>Extended tasks</strong>: Long-running background computations</li>
</ul>
<p>Additionally, Shiny logs events for:</p>
<ul>
<li>Fatal or unhandled errors (with optional error message sanitization)</li>
<li>When a <code>reactiveVal()</code> is set and</li>
<li>When a <code>reactiveValues()</code> value is set</li>
</ul>
<p>Every span and log entry provided by Shiny includes the session ID, making it easy to filter and analyze data for specific user sessions.</p>
<p>At the time of this post, no metrics (numerical measurements over time) are automatically recorded for Shiny. However, <a href="https://plumber2.posit.co/" target="_blank" rel="noopener"><code>{plumber2}</code></a>
 (via <a href="https://reqres.data-imaginist.com/" target="_blank" rel="noopener"><code>{reqres}</code></a>
) has added OTel metrics in their latest release: a counter of the number of active requests and histograms for request durations and request/response body sizes.</p>
<p>For more detailed information on configuring and using OpenTelemetry within R, check out the <a href="https://otel.r-lib.org/" target="_blank" rel="noopener"><code>{otel}</code> package documentation</a>
 and how to set up record collection with <a href="https://otelsdk.r-lib.org/reference/collecting.html" target="_blank" rel="noopener"><code>{otelsdk}</code></a>
.</p>
<h3 id="fine-grained-control">Fine-grained control
</h3>
<p>Automatic tracing is perfect to get started, but you may want more control over what gets traced. Shiny v1.12 gives you that flexibility through the <code>shiny.otel.collect</code> option (or <code>SHINY_OTEL_COLLECT</code> environment variable). You can set this to control the level of tracing detail:</p>
<ul>
<li><code>&quot;none&quot;</code> - No Shiny OpenTelemetry tracing</li>
<li><code>&quot;session&quot;</code> - Track session start and end</li>
<li><code>&quot;reactive_update&quot;</code> - Track reactive updates (includes <code>&quot;session&quot;</code> tracing)</li>
<li><code>&quot;reactivity&quot;</code> - Trace all reactive expressions (includes <code>&quot;reactive_update&quot;</code> tracing)</li>
<li><code>&quot;all&quot;</code> - Everything (currently equivalent to &ldquo;reactivity&rdquo;)</li>
</ul>
<p>For example:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># Only trace session lifecycle, not every reactive calculation</span>
</span></span><span class="line"><span class="cl"><span class="nf">options</span><span class="p">(</span><span class="n">shiny.otel.collect</span> <span class="o">=</span> <span class="s">&#34;session&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This is especially useful in production environments where you want observability without overwhelming your telemetry backend with data.</p>
<p>You can also temporarily override the <code>shiny.otel.collect</code> option within a specific block of code using the <code>withOtelCollect()</code> or <code>localOtelCollect()</code> functions. This is helpful when you want to exclude certain parts of your app from being traced. For example, to avoid tracing Shiny reactive expressions within a block:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># Do not trace _Shiny_ reactive expressions within this block</span>
</span></span><span class="line"><span class="cl"><span class="c1"># All other otel spans/logs will still be recorded</span>
</span></span><span class="line"><span class="cl"><span class="nf">withOtelCollect</span><span class="p">(</span><span class="s">&#34;none&#34;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">expensive_calculation</span> <span class="o">&lt;-</span> <span class="nf">reactive</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Start a custom span for this calculation</span>
</span></span><span class="line"><span class="cl">    <span class="n">otel</span><span class="o">::</span><span class="nf">start_local_active_span</span><span class="p">(</span><span class="s">&#34;my custom span&#34;</span><span class="p">,</span> <span class="n">tracer</span> <span class="o">=</span> <span class="s">&#34;my_tracer&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">Sys.sleep</span><span class="p">(</span><span class="m">2</span><span class="p">)</span>  <span class="c1"># Simulate a long calculation</span>
</span></span><span class="line"><span class="cl">    <span class="nf">rnorm</span><span class="p">(</span><span class="m">1e6</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">plot</span> <span class="o">&lt;-</span> <span class="nf">renderPlot</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nf">hist</span><span class="p">(</span><span class="nf">expensive_calculation</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="in-summary">In summary
</h2>
<p>Whether you&rsquo;re developing apps locally or managing production deployments, OpenTelemetry support in Shiny now gives you visibility into your applications:</p>
<ul>
<li><strong>Developers</strong> can debug complex reactive flows</li>
<li><strong>Performance engineers</strong> can identify and eliminate bottlenecks</li>
<li><strong>Data scientists</strong> can understand how users interact with their applications</li>
</ul>
<p>We&rsquo;re excited to see how the community uses this powerful new capability. Try it out, and let us know what you think!</p>
<p>While OpenTelemetry support is the star of this release, Shiny v1.12 includes several other nice improvements. For a complete list of changes, check out the NEWS entries for <a href="https://shiny.posit.co/r/reference/shiny/1.12.0/upgrade.html" target="_blank" rel="noopener">Shiny v1.12.0</a>
 and <a href="https://shiny.posit.co/r/reference/shiny/1.12.1/upgrade.html" target="_blank" rel="noopener">Shiny v1.12.1</a>
.</p>
<h2 id="learn-more">Learn more
</h2>
<p>For more details on OpenTelemetry in Shiny, check out our <a href="https://shiny.posit.co/r/articles/improve/opentelemetry/" target="_blank" rel="noopener">dedicated article on OpenTelemetry + Shiny</a>
 article.</p>
<h2 id="whats-next">What&rsquo;s next?
</h2>
<p>We have big plans for OpenTelemetry in Shiny and the broader R ecosystem. Existing and upcoming enhancements include:</p>
<ul>
<li><a href="https://mirai.r-lib.org/" target="_blank" rel="noopener"><code>{mirai}</code></a>
 v2.5.0</li>
<li><a href="https://rstudio.github.io/promises/" target="_blank" rel="noopener"><code>{promises}</code></a>
 v1.5.0</li>
<li><a href="https://plumber2.posit.co/" target="_blank" rel="noopener"><code>{plumber2}</code></a>
 (via <a href="https://fiery.data-imaginist.com/" target="_blank" rel="noopener"><code>{fiery}</code></a>
 v1.4.0, <a href="https://reqres.data-imaginist.com/" target="_blank" rel="noopener"><code>{reqres}</code></a>
 v1.1.0, and <a href="https://routr.data-imaginist.com/" target="_blank" rel="noopener"><code>{routr}</code></a>
 v1.1.0)</li>
<li><a href="https://httr2.r-lib.org/" target="_blank" rel="noopener"><code>{httr2}</code></a>
 v1.2.2</li>
<li><a href="https://yihui.org/knitr/" target="_blank" rel="noopener"><code>{knitr}</code></a>
 (<code>yihui/knitr</code>)</li>
<li><a href="https://ellmer.tidyverse.org/" target="_blank" rel="noopener"><code>{ellmer}</code></a>
 (<a href="https://github.com/tidyverse/ellmer/pull/526" target="_blank" rel="noopener"><code>tidyverse/ellmer#526</code></a>
)</li>
<li><a href="https://dbi.r-dbi.org/" target="_blank" rel="noopener"><code>{DBI}</code></a>
 (<a href="https://github.com/r-dbi/DBI/pull/551" target="_blank" rel="noopener"><code>r-dbi/dbi#551</code></a>
)</li>
<li><a href="https://testthat.r-lib.org/" target="_blank" rel="noopener"><code>{testthat}</code></a>
 (<a href="https://github.com/r-lib/testthat/pull/2282" target="_blank" rel="noopener"><code>r-lib/testthat#2282</code></a>
)</li>
</ul>
<p>In addition, the Shiny Team&rsquo;s focus will be shifting to add OpenTelemetry integration to Shiny for Python. Be sure to be on the lookout for the future announcement!</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thank you to all [the folks/everyone] who [helped make this release happen/contributed to this release]:</p>
<p><a href="https://github.com/andriygm" target="_blank" rel="noopener">@andriygm</a>
, <a href="https://github.com/BajczA475" target="_blank" rel="noopener">@BajczA475</a>
, <a href="https://github.com/bedantaguru" target="_blank" rel="noopener">@bedantaguru</a>
, <a href="https://github.com/billdenney" target="_blank" rel="noopener">@billdenney</a>
, <a href="https://github.com/chendaniely" target="_blank" rel="noopener">@chendaniely</a>
, <a href="https://github.com/Copilot" target="_blank" rel="noopener">@Copilot</a>
, <a href="https://github.com/cpsievert" target="_blank" rel="noopener">@cpsievert</a>
, <a href="https://github.com/csgillespie" target="_blank" rel="noopener">@csgillespie</a>
, <a href="https://github.com/deadbytesus" target="_blank" rel="noopener">@deadbytesus</a>
, <a href="https://github.com/dempsey-CMAR" target="_blank" rel="noopener">@dempsey-CMAR</a>
, <a href="https://github.com/dleopold" target="_blank" rel="noopener">@dleopold</a>
, <a href="https://github.com/federiva" target="_blank" rel="noopener">@federiva</a>
, <a href="https://github.com/federivaFirebird" target="_blank" rel="noopener">@federivaFirebird</a>
, <a href="https://github.com/fzenoni" target="_blank" rel="noopener">@fzenoni</a>
, <a href="https://github.com/gadenbuie" target="_blank" rel="noopener">@gadenbuie</a>
, <a href="https://github.com/gsmolinski" target="_blank" rel="noopener">@gsmolinski</a>
, <a href="https://github.com/ismirsehregal" target="_blank" rel="noopener">@ismirsehregal</a>
, <a href="https://github.com/janxkoci" target="_blank" rel="noopener">@janxkoci</a>
, <a href="https://github.com/JohnADawson" target="_blank" rel="noopener">@JohnADawson</a>
, <a href="https://github.com/karangattu" target="_blank" rel="noopener">@karangattu</a>
, <a href="https://github.com/marcozanotti" target="_blank" rel="noopener">@marcozanotti</a>
, <a href="https://github.com/Mkranj" target="_blank" rel="noopener">@Mkranj</a>
, <a href="https://github.com/mm225022" target="_blank" rel="noopener">@mm225022</a>
, <a href="https://github.com/mmuurr" target="_blank" rel="noopener">@mmuurr</a>
, <a href="https://github.com/nicholasdavies" target="_blank" rel="noopener">@nicholasdavies</a>
, <a href="https://github.com/r2evans" target="_blank" rel="noopener">@r2evans</a>
, <a href="https://github.com/schloerke" target="_blank" rel="noopener">@schloerke</a>
, <a href="https://github.com/shikokuchuo" target="_blank" rel="noopener">@shikokuchuo</a>
, <a href="https://github.com/simon-smart88" target="_blank" rel="noopener">@simon-smart88</a>
, <a href="https://github.com/vedhav" target="_blank" rel="noopener">@vedhav</a>
, and <a href="https://github.com/wch" target="_blank" rel="noopener">@wch</a>
.</p>
<hr>
<p><em>Have questions or feedback? Join the conversation on the <a href="https://community.rstudio.com/c/shiny" target="_blank" rel="noopener">Shiny Community forum</a>
 or <a href="https://github.com/rstudio/shiny" target="_blank" rel="noopener">GitHub</a>
.</em></p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/shiny/shiny-r-1.12/feature.png" length="75668" type="image/png" />
    </item>
    <item>
      <title>tidypredict 1.0.0</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/2025/tidypredict-1-0-0/</link>
      <pubDate>Wed, 10 Dec 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/2025/tidypredict-1-0-0/</guid>
      <dc:creator>Emil Hvitfeldt</dc:creator><description><![CDATA[<!--
TODO:
* [x] Look over / edit the post's title in the yaml
* [x] Edit (or delete) the description; note this appears in the Twitter card
* [x] Pick category and tags (see existing with [`hugodown::tidy_show_meta()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html))
* [x] Find photo & update yaml metadata
* [x] Create `thumbnail-sq.jpg`; height and width should be equal
* [x] Create `thumbnail-wd.jpg`; width should be >5x height
* [x] [`hugodown::use_tidy_thumbnails()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html)
* [x] Add intro sentence, e.g. the standard tagline for the package
* [x] [`usethis::use_tidy_thanks()`](https://usethis.r-lib.org/reference/use_tidy_thanks.html)
-->
<p>We&rsquo;re tickled pink to announce the release of version 1.0.0 of <a href="https://tidypredict.tidymodels.org/" target="_blank" rel="noopener">tidypredict</a>
. The main goal of tidypredict is to enable running predictions inside databases. It reads the model, extracts the components needed to calculate the prediction, and then creates an R formula that can be translated into SQL.</p>
<p>You can install them from CRAN with:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/utils/install.packages.html'>install.packages</a></span><span class='o'>(</span><span class='s'>"tidypredict"</span><span class='o'>)</span></span></code></pre>
</div>
<p>This blog post highlights the most important changes in this release, including faster computations for tree-based models, more efficient tree representations, glmnet model support, and a change in how random forests are handled. You can see a full list of changes in the <a href="https://tidypredict.tidymodels.org/news/index.html#tidypredict-100" target="_blank" rel="noopener">release notes</a>
.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://tidypredict.tidymodels.org'>tidypredict</a></span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="improved-output-for-random-forest-models">Improved output for random forest models
</h2>
<p>The previous version of tidypredict <a href="https://tidypredict.tidymodels.org/reference/tidypredict_fit.html" target="_blank" rel="noopener"><code>tidypredict_fit()</code></a>
 would return a list of expressions, one for each tree, when applied to random forest models. This didn&rsquo;t align with what is returned by other types of models. In version 1.0.0, this has been changed to produce a single, combined expression that reflects how predictions should be made.</p>
<p>This is technically a breaking change, but one we believe is worthwhile, as it provides a more consistent output for <a href="https://tidypredict.tidymodels.org/reference/tidypredict_fit.html" target="_blank" rel="noopener"><code>tidypredict_fit()</code></a>
 and hides the technical details about how to combine trees from different packages.</p>
<h2 id="faster-parsing-of-trees">Faster parsing of trees
</h2>
<p>The parsing of xgboost, partykit, and ranger models should now be substantially faster than before. Examples have been shown to be 10 to 200 times faster. Please note that larger models, more trees, and deeper trees still take some time to parse.</p>
<h2 id="more-efficient-tree-expressions">More efficient tree expressions
</h2>
<p>All trees, whether they are a single tree or part of a collection of trees, such as in boosted trees or random forests, are encoded as <code>case_when()</code> statements by tidypredict. This means that the following tree.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>model</span> <span class='o'>&lt;-</span> <span class='nf'>partykit</span><span class='nf'>::</span><span class='nf'><a href='https://rdrr.io/pkg/partykit/man/ctree.html'>ctree</a></span><span class='o'>(</span><span class='nv'>mpg</span> <span class='o'>~</span> <span class='nv'>am</span> <span class='o'>+</span> <span class='nv'>cyl</span>, data <span class='o'>=</span> <span class='nv'>mtcars</span><span class='o'>)</span></span>
<span><span class='nv'>model</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; Model formula:</span></span>
<span><span class='c'>#&gt; mpg ~ am + cyl</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; Fitted party:</span></span>
<span><span class='c'>#&gt; [1] root</span></span>
<span><span class='c'>#&gt; |   [2] cyl &lt;= 4: 26.664 (n = 11, err = 203.4)</span></span>
<span><span class='c'>#&gt; |   [3] cyl &gt; 4</span></span>
<span><span class='c'>#&gt; |   |   [4] cyl &lt;= 6: 19.743 (n = 7, err = 12.7)</span></span>
<span><span class='c'>#&gt; |   |   [5] cyl &gt; 6: 15.100 (n = 14, err = 85.2)</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; Number of inner nodes:    2</span></span>
<span><span class='c'>#&gt; Number of terminal nodes: 3</span></span>
<span></span></code></pre>
</div>
<p>Would be turned into the following <code>case_when()</code> statement.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">case_when</span><span class="p">(</span>
</span></span><span class="line"><span class="cl"> <span class="n">cyl</span> <span class="o">&lt;=</span> <span class="m">4</span> <span class="o">~</span> <span class="m">26.6636363636364</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="n">cyl</span> <span class="o">&lt;=</span> <span class="m">6</span> <span class="o">&amp;</span> <span class="n">cyl</span> <span class="o">&gt;</span> <span class="m">4</span> <span class="o">~</span> <span class="m">19.7428571428571</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl"> <span class="n">cyl</span> <span class="o">&gt;</span> <span class="m">6</span> <span class="o">&amp;</span> <span class="n">cyl</span> <span class="o">&gt;</span> <span class="m">4</span> <span class="o">~=</span> <span class="m">15.1</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>With this new update, we have taken advantage of the <code>.default</code> argument whenever possible, which should lead to faster predictions, as we no longer need to calculate redundant conditionals.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://tidypredict.tidymodels.org/reference/tidypredict_fit.html'>tidypredict_fit</a></span><span class='o'>(</span><span class='nv'>model</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; case_when(cyl &lt;= 4 ~ 26.6636363636364, cyl &lt;= 6 &amp; cyl &gt; 4 ~ 19.7428571428571, </span></span>
<span><span class='c'>#&gt;     .default = 15.1)</span></span>
<span></span></code></pre>
</div>
<h2 id="glmnet-support">Glmnet support
</h2>
<p>We now support the glmnet package. This package provides generalized linear models with lasso or elasticnet regularization.</p>
<p>The primary restriction when using a glmnet model with <code>tidypredict()</code> is that the model must have been fitted with the <code>lambda</code> argument set to a single value.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>model</span> <span class='o'>&lt;-</span> <span class='nf'>glmnet</span><span class='nf'>::</span><span class='nf'><a href='https://glmnet.stanford.edu/reference/glmnet.html'>glmnet</a></span><span class='o'>(</span><span class='nv'>mtcars</span><span class='o'>[</span>, <span class='o'>-</span><span class='m'>1</span><span class='o'>]</span>, <span class='nv'>mtcars</span><span class='o'>$</span><span class='nv'>mpg</span>, lambda <span class='o'>=</span> <span class='m'>0.01</span><span class='o'>)</span></span>
<span></span>
<span><span class='nf'><a href='https://tidypredict.tidymodels.org/reference/tidypredict_fit.html'>tidypredict_fit</a></span><span class='o'>(</span><span class='nv'>model</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; 13.0081464696679 + (cyl * -0.0773532164346008) + (disp * 0.00969507138358544) + </span></span>
<span><span class='c'>#&gt;     (hp * -0.0192462098902709) + (drat * 0.816753237688302) + </span></span>
<span><span class='c'>#&gt;     (wt * -3.41564341709663) + (qsec * 0.758580151032383) + (vs * </span></span>
<span><span class='c'>#&gt;     0.277874296242861) + (am * 2.47356523820533) + (gear * 0.645144527527598) + </span></span>
<span><span class='c'>#&gt;     (carb * -0.300886812079305)</span></span>
<span></span></code></pre>
</div>
<p><code>glmnet()</code> computes a collection of models using many sets of penalty values. This can be very efficient, but for tidypredict, we need to predict with a single penalty. Note how, as we increase the penalty, the extracted expression correctly removes terms with coefficients of <code>0</code> instead of leaving them as <code>(disp * 0)</code>.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nv'>model</span> <span class='o'>&lt;-</span> <span class='nf'>glmnet</span><span class='nf'>::</span><span class='nf'><a href='https://glmnet.stanford.edu/reference/glmnet.html'>glmnet</a></span><span class='o'>(</span><span class='nv'>mtcars</span><span class='o'>[</span>, <span class='o'>-</span><span class='m'>1</span><span class='o'>]</span>, <span class='nv'>mtcars</span><span class='o'>$</span><span class='nv'>mpg</span>, lambda <span class='o'>=</span> <span class='m'>1</span><span class='o'>)</span></span>
<span></span>
<span><span class='nf'><a href='https://tidypredict.tidymodels.org/reference/tidypredict_fit.html'>tidypredict_fit</a></span><span class='o'>(</span><span class='nv'>model</span><span class='o'>)</span></span>
<span><span class='c'>#&gt; 35.3137765116027 + (cyl * -0.871451193824228) + (hp * -0.0101173960249783) + </span></span>
<span><span class='c'>#&gt;     (wt * -2.59443677687505)</span></span>
<span></span></code></pre>
</div>
<p>tidypredict is used as the primary parser for models employed by the <a href="https://orbital.tidymodels.org/" target="_blank" rel="noopener">orbital</a>
 package. This means that all the changes seen in this post also take effect when using orbital with tidymodels workflows. Such as using <a href="https://parsnip.tidymodels.org/reference/linear_reg.html" target="_blank" rel="noopener"><code>parsnip::linear_reg()</code></a>
 with <code>engine = &quot;glmnet&quot;</code>.</p>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thank you to all the folks who helped make this release happen: <a href="https://github.com/EmilHvitfeldt" target="_blank" rel="noopener">@EmilHvitfeldt</a>
, and <a href="https://github.com/jeroenjanssens" target="_blank" rel="noopener">@jeroenjanssens</a>
.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/2025/tidypredict-1-0-0/thumbnail-wd.jpg" length="315661" type="image/jpeg" />
    </item>
    <item>
      <title>Shiny Talks from posit::conf(2025)</title>
      <link>https://posit-open-source.netlify.app/blog/shiny/conf-2025-shinytalks/</link>
      <pubDate>Tue, 02 Dec 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/shiny/conf-2025-shinytalks/</guid><description><![CDATA[<p>Videos from posit::conf(2025) are now available. To make it easier to explore how people are building with Shiny across R and Python, we&rsquo;ve put together a curated playlist of the Shiny-focused talks from the conference. These sessions highlight practical apps, new tooling, design patterns, AI workflows, and real-world deployments built with Shiny.</p>
<h2 id="shiny-talks-playlist">Shiny Talks Playlist:
</h2>
<iframe width="560" height="315" src="https://www.youtube.com/embed/Kh8xDym8sTg?si=trOED7T3iArnY7Kj" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen>
</iframe>
<p>Talks included in the playlist, broken up into a few categories for easier browsing, are as follows:</p>
<h2 id="shiny--ai--intelligent-automation">Shiny + AI &amp; Intelligent Automation
</h2>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Speakers</th>
          <th style="text-align: center">Title</th>
          <th style="text-align: left">Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right"><strong>Winston Chang (Shiny Team)</strong></td>
          <td style="text-align: center">Web applications with Shiny and React (and AI)</td>
          <td style="text-align: left"><a href="https://youtu.be/Kh8xDym8sTg" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/Kh8xDym8sTg/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Karan Gathani (Shiny Team)</strong></td>
          <td style="text-align: center">Old Apps, New Tricks: How AI can write Automated Tests for Shiny</td>
          <td style="text-align: left"><a href="https://youtu.be/gxcAadq0Bmk" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/gxcAadq0Bmk/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Regis A. James</strong></td>
          <td style="text-align: center">AskRADS: An AI Recommendation Agent for Maximizing Shiny Development</td>
          <td style="text-align: left"><a href="https://youtu.be/v3CCoq7j9Tk" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/v3CCoq7j9Tk/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h2 id="shiny-architecture-performance--data">Shiny Architecture, Performance &amp; Data
</h2>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Speakers</th>
          <th style="text-align: center">Title</th>
          <th style="text-align: left">Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right"><strong>Alex Chisholm</strong></td>
          <td style="text-align: center">Keeping Data Alive: Persistent Storage Options for Shiny</td>
          <td style="text-align: left"><a href="https://youtu.be/Pe-XTCKUzV8" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/Pe-XTCKUzV8/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Melissa Albino Hegeman</strong></td>
          <td style="text-align: center">Get your ducks in a row&hellip; faster Shiny apps with DuckDB</td>
          <td style="text-align: left"><a href="https://www.youtube.com/watch?v=2sLJSosz1OY" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://i.ytimg.com/vi/2sLJSosz1OY/hqdefault.jpg?sqp=-oaymwEjCNACELwBSFryq4qpAxUIARUAAAAAGAElAADIQj0AgKJDeAE=&amp;rs=AOn4CLBZKR5FG40MaA-0o4SMJg3cKepLdQ"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Barret Schloerke (Shiny Team)</strong></td>
          <td style="text-align: center">Observability at scale: Monitoring Shiny Applications</td>
          <td style="text-align: left"><a href="https://youtu.be/zwxhFKRlQLs" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/zwxhFKRlQLs/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h2 id="shiny-ux-ui-design--accessibility">Shiny UX, UI Design &amp; Accessibility
</h2>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Speakers</th>
          <th style="text-align: center">Title</th>
          <th style="text-align: left">Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right"><strong>Cameron Race; Sarah Wong-Brown</strong></td>
          <td style="text-align: center">shinyGovStyle &ndash; accessible government design in Shiny</td>
          <td style="text-align: left"><a href="https://youtu.be/33doZkPSUqY" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/33doZkPSUqY/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Casey Aguilar-Gervase; Maya Gans</strong></td>
          <td style="text-align: center">Design of Everyday Shiny Apps</td>
          <td style="text-align: left"><a href="https://youtu.be/RQod46DSkiA" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/RQod46DSkiA/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Jeremy Winget, PhD</strong></td>
          <td style="text-align: center">Death by Dropdown? Engineer Insightful Shiny Apps</td>
          <td style="text-align: left"><a href="https://youtu.be/QvMnUZLQ-oo" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/QvMnUZLQ-oo/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Kim Schouten</strong></td>
          <td style="text-align: center">Modular, layout-as-code approach for customizable Shiny dashboards</td>
          <td style="text-align: left"><a href="https://youtu.be/HJs2HVltpcs" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/HJs2HVltpcs/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h2 id="shiny-extensions-packages--developer-tooling">Shiny Extensions, Packages &amp; Developer Tooling
</h2>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Speakers</th>
          <th style="text-align: center">Title</th>
          <th style="text-align: left">Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right"><strong>Eric Nantz</strong></td>
          <td style="text-align: center">shinystate: Launching collaboration and session state</td>
          <td style="text-align: left"><a href="https://youtu.be/69bnkXD3e6w" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/69bnkXD3e6w/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>James Wade</strong></td>
          <td style="text-align: center">shinyEventLogger &mdash; logging events in Shiny apps</td>
          <td style="text-align: left"><a href="https://youtu.be/smnrmTtoiOM" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/smnrmTtoiOM/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h2 id="applied-shiny-case-studies">Applied Shiny Case Studies
</h2>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Speakers</th>
          <th style="text-align: center">Title</th>
          <th style="text-align: left">Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right"><strong>Marcus Beck</strong></td>
          <td style="text-align: center">Shiny for ecological data workflows</td>
          <td style="text-align: left"><a href="https://youtu.be/Cnk3770AzuA" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/Cnk3770AzuA/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Hugo Fitipaldi</strong></td>
          <td style="text-align: center">Building a Real-Time COVID-19 Surveillance System with Shiny</td>
          <td style="text-align: left"><a href="https://youtu.be/3-UsetFXFlk" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/3-UsetFXFlk/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h2 id="shiny-workshops-from-positconf2025">Shiny Workshops from posit::conf(2025)
</h2>
<p>Full workshop materials are now available, including slides, exercises, and code for all Shiny workshops.</p>
<h3 class="me-2">
LLMs + Shiny Workshop
</h3>
/
<p class="ms-2">
Joe Cheng & Garrick Aden-Buie
</p>
<p><a href="https://posit-conf-2025.github.io/llm/" class="me-3">LLM Course Materials</a><a href="https://github.com/posit-conf-2025/llm"><img src="https://posit-open-source.netlify.app/images/github.svg" style="width:16px;height:auto;display:inline;"></a></p>
<h3 class="me-2">
Shiny for Python Workshop
</h3>
/
<p class="ms-2">
Daniel Chen
</p>
<p><a href="https://posit-conf-2025.github.io/shiny-py/" class="me-3">Shiny for R Materials</a><a href="https://github.com/posit-conf-2025/shiny-py"><img src="https://posit-open-source.netlify.app/images/github.svg" style="width:16px;height:auto;display:inline;"></a></p>
<h3 class="me-2">
Shiny for R Workshop
</h3>
/
<p class="ms-2">
Colin Rundel
</p>
<p><a href="https://posit-conf-2025.github.io/shiny-r/" class="me-3">Shiny for Python Materials</a><a href="https://github.com/posit-conf-2025/shiny-r/"><img src="https://posit-open-source.netlify.app/images/github.svg" style="width:16px;height:auto;display:inline;"></a></p>
<h2 id="closing">Closing
</h2>
<p>We hope you enjoy this roundup of Shiny talks from posit::conf(2025). The Shiny community continues to explore new patterns&mdash;AI-assisted development, real-time analytics, performance tooling, design systems, and more.
We hope to see you next year at posit::conf&mdash;and maybe even see <em>your</em> Shiny work up on stage.</p>
<style>
  td, th {text-align: left !important;}
  /*tr:last-of-type td {border-bottom: 0px !important;}
  table {border-bottom: 1px solid transparent !important;}*/
</style>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/shiny/conf-2025-shinytalks/conf-recordings-banner.png" length="243314" type="image/png" />
    </item>
    <item>
      <title>posit::conf(2025) Quarto talks</title>
      <link>https://posit-open-source.netlify.app/blog/quarto/2025-11-24-conf-talk-videos/</link>
      <pubDate>Mon, 24 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/quarto/2025-11-24-conf-talk-videos/</guid>
      <dc:creator>Andrew Holz</dc:creator><description><![CDATA[<p>The posit::conf(2025) session videos are now live! We&rsquo;ve created a curated playlist highlighting all the talks that showcase Quarto&mdash;how it is evolving, how people are using it, and how they&rsquo;re building on top of it.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/videoseries?si=RnKN-0bM7C0bwXfQ&amp;list=PLitrm9UndxcvQgAigiiOofTEPAWpROiiK" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen>
</iframe>
<p>Talks included in the playlist, broken up into a few categories for easier browsing, are as follows:</p>
<h1 id="quarto-extensions--advanced-features">Quarto Extensions &amp; Advanced Features
</h1>
<table>
  <thead>
      <tr>
          <th>Speakers</th>
          <th>Title</th>
          <th>Thumbnail</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Carlos Scheidegger (Quarto Team)</strong></td>
          <td>What we&rsquo;re doing to make Quarto fast(er)</td>
          <td><a href="https://youtu.be/OBHppBRztO4" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/OBHppBRztO4/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Christophe Dervieux (Quarto Team)</strong></td>
          <td>Beyond the Basics: Expanding Quarto&rsquo;s Capabilities</td>
          <td><a href="https://youtu.be/u9ev3mvC-p0" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/u9ev3mvC-p0/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Garrick Aden-Buie</strong></td>
          <td>Theming Made Easy: Introducing brand.yml</td>
          <td><a href="https://youtu.be/DPaoNM8Ux04" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/DPaoNM8Ux04/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Gordon Woodhull (Quarto Team)</strong></td>
          <td>Brand YML and Dark Mode in Quarto</td>
          <td><a href="https://youtu.be/WNwsgS-klMA" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/WNwsgS-klMA/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>JooYoung Seo</strong></td>
          <td>maidr: Empowering Accessible, Multimodal Data Science with Quarto</td>
          <td><a href="https://youtu.be/QR7mdgM8Hf0" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/QR7mdgM8Hf0/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h1 id="workflow-automation--reporting">Workflow Automation &amp; Reporting
</h1>
<table>
  <thead>
      <tr>
          <th>Speakers</th>
          <th>Title</th>
          <th>Thumbnail</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Becca Krouse</strong></td>
          <td>Instant Impact: Developing {docorator} to Simplify Document Production</td>
          <td><a href="https://youtu.be/SWt-lcnYlNM" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/SWt-lcnYlNM/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>John Paul Helveston</strong></td>
          <td>surveydown: A Markdown-Based Platform for Interactive Surveys</td>
          <td><a href="https://youtu.be/VwoeFKNvN5k" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/VwoeFKNvN5k/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Keaton Wilson</strong></td>
          <td>Using Quarto to Improve Formatting and Automation</td>
          <td><a href="https://youtu.be/vHrI17AeYGs" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/vHrI17AeYGs/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h1 id="teaching--education">Teaching &amp; Education
</h1>
<table>
  <thead>
      <tr>
          <th>Speakers</th>
          <th>Title</th>
          <th>Thumbnail</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Claus Wilke</strong></td>
          <td>Teaching data visualization with R entirely in Quarto</td>
          <td><a href="https://youtu.be/Q7y0YqCuvHc" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/Q7y0YqCuvHc/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Mine Çetinkaya-Rundel (Quarto Team)</strong></td>
          <td>Leveraging LLMs for student feedback in introductory data science courses</td>
          <td><a href="https://youtu.be/5gS7AUGwZPs" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/5gS7AUGwZPs/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Ted Laderas</strong></td>
          <td>Empowering Learners with WebR, Pyodide, and Quarto</td>
          <td><a href="https://youtu.be/EQ9_MP2PYL8" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/EQ9_MP2PYL8/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h1 id="business-collaboration--publishing">Business, Collaboration &amp; Publishing
</h1>
<table>
  <thead>
      <tr>
          <th>Speakers</th>
          <th>Title</th>
          <th>Thumbnail</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Andrew Heiss</strong> and <strong>Gabe Osterhout</strong></td>
          <td>Election Night Reporting Using R &amp; Quarto</td>
          <td><a href="https://youtu.be/UCloM4GcfVY" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/UCloM4GcfVY/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Bill Pikounis</strong></td>
          <td>Quarto for Business Collaboration and Technical Documentation in Word docx format</td>
          <td><a href="https://youtu.be/4-dQ2Q985A0" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/4-dQ2Q985A0/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Timothy Keyes</strong></td>
          <td>Trust, but Verify: Lessons from Deploying LLMs</td>
          <td><a href="https://youtu.be/HYQaZTLb2Co" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/HYQaZTLb2Co/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Tip</span>
</div>
<div class="callout-body">
<p><strong>Workshop materials now available:</strong><br>
The Quarto team has published full materials from the two workshops at posit::conf 2025: &ldquo;Branded Websites, Presentations, Dashboards, and PDFs with Quarto&rdquo; and &ldquo;Extending Quarto&rdquo;.<br>
You can access the workshop websites, exercise source code, and full slide decks under a CC BY-SA 4.0 license from the <a href="https://quarto.org/docs/blog/posts/2025-10-27-conf-workshops-materials/" target="_blank" rel="noopener">Quarto blog</a>
.</p>
</div>
</div>
<p>We hope you enjoyed this look back at the Quarto sessions from posit::conf(2025). Every year the community brings new ideas, new tools, and new ways of working &mdash; and we&rsquo;d love to see <strong>your</strong> voice added to the mix. We hope to see you next year, and maybe even see <em>you</em> up on stage sharing your own work at posit::conf!</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/quarto/2025-11-24-conf-talk-videos/thumbnail.png" length="221625" type="image/png" />
    </item>
    <item>
      <title>Two New tidymodels Packages</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/2025/two-new-tidymodels-packages/</link>
      <pubDate>Sat, 22 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/2025/two-new-tidymodels-packages/</guid>
      <dc:creator>Frances Lin</dc:creator>
      <dc:creator>Max Kuhn</dc:creator><description><![CDATA[<!--
TODO:
* [ ] Look over / edit the post's title in the yaml
* [ ] Edit (or delete) the description; note this appears in the Twitter card
* [ ] Pick category and tags (see existing with `hugodown::tidy_show_meta()`)
* [ ] Find photo & update yaml metadata
* [ ] Create `thumbnail-sq.jpg`; height and width should be equal
* [ ] Create `thumbnail-wd.jpg`; width should be >5x height
* [ ] `hugodown::use_tidy_thumbnails()`
* [ ] Add intro sentence, e.g. the standard tagline for the package
* [ ] `usethis::use_tidy_thanks()`
-->
<p>We&rsquo;re very chuffed to announce the release of <em>two</em> new modeling packages: filtro and important.</p>
<p>You can install them from CRAN with:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="nf">c</span><span class="p">(</span><span class="s">&#34;filtro&#34;</span><span class="p">,</span> <span class="s">&#34;important&#34;</span><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This blog post will introduce both.</p>
<h2 id="filtro">filtro
</h2>
<p>Feature selection is an important step in building machine learning models that are robust and reliable. By keeping only the most relevant predictors, we can reduce overfitting, improve model performance, and speed up computation.</p>
<p><a href="https://filtro.tidymodels.org/" target="_blank" rel="noopener">filtro</a>
 is a low-level tidy tools designed for filter-based supervised feature selection. filtro makes it easy to score, rank, and select features using a wide range of statistical and model-based metrics. The scoring metrics include: p-values, correlation, random forest feature importance, information gain, and more.</p>
<p>With filtro, we can quickly rank the variables and select either the top proportion or the top number of features that best contribute to our model. It also supports <a href="https://scholar.google.com/scholar?hl=en&amp;as_sdt=0%2C7&amp;q=%22multi-parameter&#43;optimization%22&amp;btnG=" target="_blank" rel="noopener">multi-parameter optimization</a>
 via <a href="https://scholar.google.com/scholar?hl=en&amp;as_sdt=0%2C7&amp;q=%22desirability&#43;functions%22&amp;btnG=" target="_blank" rel="noopener">desirability functions</a>
. filtro is a standalone tool, but it integrates with other packages, allowing it to be used within the tidymodels workflows.</p>
<p>Currently, filtro implements a total of six filters. Like other elements of the framework, also filtro is extensible if you want to use a score we haven&rsquo;t implemented yet. You can read more on how to do this on <a href="https://www.tidymodels.org/learn/develop/filtro/" target="_blank" rel="noopener">tidymodels.org</a>
.</p>
<p>The available score class objects are:</p>
<pre tabindex="0"><code>##  [1] &#34;score_aov_fstat&#34;          &#34;score_aov_pval&#34;          
##  [3] &#34;score_cor_pearson&#34;        &#34;score_cor_spearman&#34;      
##  [5] &#34;score_gain_ratio&#34;         &#34;score_imp_rf&#34;            
##  [7] &#34;score_imp_rf_conditional&#34; &#34;score_imp_rf_oblique&#34;    
##  [9] &#34;score_info_gain&#34;          &#34;score_roc_auc&#34;           
## [11] &#34;score_sym_uncert&#34;         &#34;score_xtab_pval_chisq&#34;   
## [13] &#34;score_xtab_pval_fisher&#34;
</code></pre><p>Let&rsquo;s look at an example. <a href="https://www.google.com/search?q=Kuhn&#43;and&#43;Johnson&#43;Applied&#43;Predictive&#43;Modeling&#43;2013" target="_blank" rel="noopener">Kuhn and Johnson (2013)</a>
 described a data set where 176 samples were collected from a chemical manufacturing process. The goal is to predict process yield. Predictors are continuous, count, and categorical; some are correlated, and some contain missing values.</p>
<p>Let’s create an initial split of the data (which are in the modeldata package):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">tidymodels</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">filtro</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">set.seed</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">yield_split</span> <span class="o">&lt;-</span> <span class="nf">initial_split</span><span class="p">(</span><span class="n">modeldata</span><span class="o">::</span><span class="n">chem_proc_yield</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">yield_split</span>
</span></span></code></pre></td></tr></table>
</div>
</div><pre tabindex="0"><code>## &lt;Training/Testing/Total&gt;
## &lt;132/44/176&gt;
</code></pre><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">yield_train</span> <span class="o">&lt;-</span> <span class="nf">training</span><span class="p">(</span><span class="n">yield_split</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">yield_test</span> <span class="o">&lt;-</span> <span class="nf">testing</span><span class="p">(</span><span class="n">yield_split</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>We’d like to estimate the strength of the relationship between these 57 predictors and the process yield. We’ll quantify that in two ways. First is the old-fashioned <a href="https://en.wikipedia.org/wiki/Spearman%27s_rank_correlation_coefficient" target="_blank" rel="noopener">Spearman rank correlation</a>
 statistic. We can estimate these values and rank them by the absolute value of the correlations. We can also measure their value using a random forest variable importance. One quality of the predictors is that their values are correlated, so there may be some value in using an <em>oblique</em> random forest model. This creates a collection of tree-based models with splits that are linear combinations of the selected predictors.</p>
<p>To estimate the scores, we use the score objects contained in the package along with the <code>fit()</code> method:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">yield_rank_res</span> <span class="o">&lt;-</span>
</span></span><span class="line"><span class="cl">  <span class="n">score_cor_spearman</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">yield</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">yield_train</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># The object contains the statistics:</span>
</span></span><span class="line"><span class="cl"><span class="n">yield_rank_res</span><span class="o">@</span><span class="n">results</span> <span class="o">|&gt;</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">arrange</span><span class="p">(</span><span class="nf">desc</span><span class="p">(</span><span class="nf">abs</span><span class="p">(</span><span class="n">score</span><span class="p">)))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><pre tabindex="0"><code>## # A tibble: 57 × 4
##    name          score outcome predictor      
##    &lt;chr&gt;         &lt;dbl&gt; &lt;chr&gt;   &lt;chr&gt;          
##  1 cor_spearman  0.655 yield   man_proc_32    
##  2 cor_spearman -0.537 yield   man_proc_36    
##  3 cor_spearman  0.519 yield   bio_material_03
##  4 cor_spearman  0.502 yield   bio_material_06
##  5 cor_spearman  0.491 yield   man_proc_09    
##  6 cor_spearman  0.478 yield   bio_material_02
##  7 cor_spearman  0.446 yield   man_proc_33    
##  8 cor_spearman  0.421 yield   bio_material_12
##  9 cor_spearman -0.420 yield   man_proc_13    
## 10 cor_spearman  0.412 yield   bio_material_04
## # ℹ 47 more rows
</code></pre><p>To score via a random forest model, we only need to switch out the score object:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">yield_rf_res</span> <span class="o">&lt;-</span>
</span></span><span class="line"><span class="cl">  <span class="n">score_imp_rf_oblique</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">yield</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">yield_train</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">yield_rf_res</span><span class="o">@</span><span class="n">results</span> <span class="o">|&gt;</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">arrange</span><span class="p">(</span><span class="nf">desc</span><span class="p">(</span><span class="nf">abs</span><span class="p">(</span><span class="n">score</span><span class="p">)))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><pre tabindex="0"><code>## # A tibble: 57 × 4
##    name            score outcome predictor      
##    &lt;chr&gt;           &lt;dbl&gt; &lt;chr&gt;   &lt;chr&gt;          
##  1 imp_rf_oblique 0.128  yield   man_proc_32    
##  2 imp_rf_oblique 0.0697 yield   man_proc_36    
##  3 imp_rf_oblique 0.0670 yield   man_proc_17    
##  4 imp_rf_oblique 0.0644 yield   man_proc_09    
##  5 imp_rf_oblique 0.0612 yield   man_proc_13    
##  6 imp_rf_oblique 0.0446 yield   bio_material_03
##  7 imp_rf_oblique 0.0315 yield   man_proc_33    
##  8 imp_rf_oblique 0.0263 yield   man_proc_11    
##  9 imp_rf_oblique 0.0263 yield   bio_material_04
## 10 imp_rf_oblique 0.0262 yield   bio_material_06
## # ℹ 47 more rows
</code></pre><p>We should probably combine the scores and do a joint ranking. To combine the two sets of statistics:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">class_score_list</span> <span class="o">&lt;-</span> <span class="nf">list</span><span class="p">(</span><span class="n">yield_rank_res</span><span class="p">,</span> <span class="n">yield_rf_res</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">bind_scores</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">class_score_list</span>
</span></span></code></pre></td></tr></table>
</div>
</div><pre tabindex="0"><code>## # A tibble: 57 × 4
##    outcome predictor       cor_spearman imp_rf_oblique
##    &lt;chr&gt;   &lt;chr&gt;                  &lt;dbl&gt;          &lt;dbl&gt;
##  1 yield   bio_material_01        0.404        0.0178 
##  2 yield   bio_material_02        0.478        0.0190 
##  3 yield   bio_material_03        0.519        0.0446 
##  4 yield   bio_material_04        0.412        0.0263 
##  5 yield   bio_material_05        0.116        0.00639
##  6 yield   bio_material_06        0.502        0.0262 
##  7 yield   bio_material_07       -0.101        0.00151
##  8 yield   bio_material_08        0.369        0.00714
##  9 yield   bio_material_09        0.109        0.0122 
## 10 yield   bio_material_10        0.214        0.00998
## # ℹ 47 more rows
</code></pre><p>We can accomplish a joint ranking via desirability functions. Here, we set goals for each score (i.e., maximize, minimize, etc.). The algorithm rescales their values and uses a geometric mean for an overall ranking. The desirability2 package has some nice tools for this. Here&rsquo;s how we do it:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">desirability2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">class_score_list</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">show_best_desirability_prop</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">maximize</span><span class="p">(</span><span class="n">cor_spearman</span><span class="p">,</span> <span class="n">low</span> <span class="o">=</span> <span class="m">0.25</span><span class="p">,</span> <span class="n">high</span> <span class="o">=</span> <span class="m">1</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="nf">maximize</span><span class="p">(</span><span class="n">imp_rf_oblique</span><span class="p">,</span> <span class="n">scale</span> <span class="o">=</span> <span class="m">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="o">|&gt;</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">arrange</span><span class="p">(</span><span class="nf">desc</span><span class="p">(</span><span class="n">.d_overall</span><span class="p">))</span> <span class="o">|&gt;</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">select</span><span class="p">(</span><span class="o">-</span><span class="nf">starts_with</span><span class="p">(</span><span class="s">&#34;.d_max_&#34;</span><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><pre tabindex="0"><code>## # A tibble: 57 × 5
##    outcome predictor       cor_spearman imp_rf_oblique .d_overall
##    &lt;chr&gt;   &lt;chr&gt;                  &lt;dbl&gt;          &lt;dbl&gt;      &lt;dbl&gt;
##  1 yield   man_proc_32            0.655         0.128      0.735 
##  2 yield   man_proc_09            0.491         0.0644     0.291 
##  3 yield   bio_material_03        0.519         0.0446     0.217 
##  4 yield   man_proc_33            0.446         0.0315     0.134 
##  5 yield   bio_material_06        0.502         0.0262     0.129 
##  6 yield   bio_material_04        0.412         0.0263     0.104 
##  7 yield   bio_material_02        0.478         0.0190     0.0926
##  8 yield   bio_material_01        0.404         0.0178     0.0719
##  9 yield   bio_material_11        0.381         0.0194     0.0714
## 10 yield   man_proc_12            0.391         0.0183     0.0705
## # ℹ 47 more rows
</code></pre><p>Using the <code>scale = 2</code> option puts more weight on the random forest results.</p>
<p>It is unlikely that users will work with filtro directly; it is much better to incorporate these feature selection tools inside a model workflow (as we will see below).</p>
<p>Now that we&rsquo;ve looked at filtro, next up is the important package (yes, this is what we named it).</p>
<h2 id="important">important
</h2>
<p>The <a href="https://important.tidymodels.org/" target="_blank" rel="noopener">important</a>
 package does two things. First, it provides yet another tool for calculating random forest-like permutation importance scores. We highly value other packages that perform these same calculations (such as <a href="https://modeloriented.github.io/DALEX/" target="_blank" rel="noopener">DALEX</a>
 and <a href="https://github.com/koalaverse/vip/" target="_blank" rel="noopener">vip</a>
). Our rationale for creating another package for this is that we&rsquo;ve developed interfaces for censored regression, including dynamic metrics such as Brier scores or ROC curves that evaluate models at a specific time point. These dynamic methods aren&rsquo;t available in other packages, and the peculiarities of these metrics make them difficult to incorporate into existing frameworks.</p>
<p>Other niceties about importance scores are that any metric from the yardstick package can be used, and we have optimized parallel processing for the underlying computations. For the latter feature, we support the future and mirai packages for parallel processing.</p>
<p>important also has three recipe steps for supervised feature selection (similar to what Steven Pawley did with his <a href="https://stevenpawley.github.io/colino/" target="_blank" rel="noopener">colino package</a>
). The steps are:</p>
<ul>
<li><a href="https://important.tidymodels.org/reference/step_predictor_best.html" target="_blank" rel="noopener"><code>step_predictors_best()</code></a>
</li>
<li><a href="https://important.tidymodels.org/reference/step_predictor_retain.html" target="_blank" rel="noopener"><code>step_predictors_retain()</code></a>
</li>
<li><a href="https://important.tidymodels.org/reference/step_predictor_desirability.html" target="_blank" rel="noopener"><code>step_predictors_desirability()</code></a>
</li>
</ul>
<p>Let&rsquo;s look at the last one, which mirrors our analysis above.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">important</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">goals</span> <span class="o">&lt;-</span>
</span></span><span class="line"><span class="cl">  <span class="nf">desirability</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">maximize</span><span class="p">(</span><span class="n">cor_spearman</span><span class="p">,</span> <span class="n">low</span> <span class="o">=</span> <span class="m">0.25</span><span class="p">,</span> <span class="n">high</span> <span class="o">=</span> <span class="m">1</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="nf">maximize</span><span class="p">(</span><span class="n">imp_rf_oblique</span><span class="p">,</span> <span class="n">scale</span> <span class="o">=</span> <span class="m">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">yield_rec</span> <span class="o">&lt;-</span>
</span></span><span class="line"><span class="cl">  <span class="nf">recipe</span><span class="p">(</span><span class="n">yield</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">yield_train</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_impute_knn</span><span class="p">(</span><span class="nf">all_predictors</span><span class="p">(),</span> <span class="n">neighbors</span> <span class="o">=</span> <span class="m">10</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">step_predictor_desirability</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">all_predictors</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="n">score</span> <span class="o">=</span> <span class="n">goals</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">prop_terms</span> <span class="o">=</span> <span class="m">1</span> <span class="o">/</span> <span class="m">10</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">yield_rec</span>
</span></span></code></pre></td></tr></table>
</div>
</div><pre tabindex="0"><code>## 
</code></pre><pre tabindex="0"><code>## ── Recipe ───────────────────────────────────────────────────────
</code></pre><pre tabindex="0"><code>## 
</code></pre><pre tabindex="0"><code>## ── Inputs
</code></pre><pre tabindex="0"><code>## Number of variables by role
</code></pre><pre tabindex="0"><code>## outcome:    1
## predictor: 57
</code></pre><pre tabindex="0"><code>## 
</code></pre><pre tabindex="0"><code>## ── Operations
</code></pre><pre tabindex="0"><code>## • K-nearest neighbor imputation for: all_predictors()
</code></pre><pre tabindex="0"><code>## • Feature selection via desirability functions (`cor_spearman`
##   and `imp_rf_oblique`) on: all_predictors()
</code></pre><p>When combined with a specific model, we can tune the number of neighbors as well as the proportion of predictors retained (10% above).</p>
<p><code>prep()</code> will do the appropriate estimation steps:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">trained_rec</span> <span class="o">&lt;-</span> <span class="nf">prep</span><span class="p">(</span><span class="n">yield_rec</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Which 10% of the predictors were retained? The <code>tidy()</code> method can list the scores and their rankings:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">scores</span> <span class="o">&lt;-</span> <span class="nf">tidy</span><span class="p">(</span><span class="n">trained_rec</span><span class="p">,</span> <span class="n">number</span> <span class="o">=</span> <span class="m">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">scores</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">arrange</span><span class="p">(</span><span class="nf">desc</span><span class="p">(</span><span class="n">.d_overall</span><span class="p">))</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">select</span><span class="p">(</span><span class="o">-</span><span class="nf">starts_with</span><span class="p">(</span><span class="s">&#34;.d_max_&#34;</span><span class="p">),</span> <span class="o">-</span><span class="n">id</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><pre tabindex="0"><code>## # A tibble: 57 × 5
##    terms           removed cor_spearman imp_rf_oblique .d_overall
##    &lt;chr&gt;           &lt;lgl&gt;          &lt;dbl&gt;          &lt;dbl&gt;      &lt;dbl&gt;
##  1 man_proc_32     FALSE          0.655         0.128       0.735
##  2 man_proc_36     FALSE         -0.530         0.0668      0.325
##  3 man_proc_09     FALSE          0.491         0.0673      0.304
##  4 man_proc_13     FALSE         -0.420         0.0725      0.275
##  5 bio_material_03 FALSE          0.519         0.0517      0.249
##  6 bio_material_06 TRUE           0.502         0.0445      0.210
##  7 man_proc_17     TRUE          -0.303         0.0749      0.158
##  8 man_proc_33     TRUE           0.443         0.0374      0.156
##  9 bio_material_02 TRUE           0.478         0.0330      0.151
## 10 bio_material_04 TRUE           0.412         0.0347      0.133
## # ℹ 47 more rows
</code></pre><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># What percentage was removed?</span>
</span></span><span class="line"><span class="cl"><span class="nf">mean</span><span class="p">(</span><span class="n">scores</span><span class="o">$</span><span class="n">removed</span> <span class="o">*</span> <span class="m">100</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><pre tabindex="0"><code>## [1] 91.22807
</code></pre><h2 id="summary">Summary
</h2>
<p>Both filtro and important satisfy a feature for tidymodels that has been highly ranked in our user surveys: supervised feature selection. filtro contains the underlying framework and important provides recipe steps that can be used in a workflow.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/2025/two-new-tidymodels-packages/thumbnail-wd.jpg" length="97105" type="image/jpeg" />
    </item>
    <item>
      <title>Tool Calling UI in shinychat</title>
      <link>https://posit-open-source.netlify.app/blog/shiny/shinychat-tool-ui/</link>
      <pubDate>Thu, 20 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/shiny/shinychat-tool-ui/</guid>
      <dc:creator>Garrick Aden-Buie</dc:creator>
      <dc:creator>Carson Sievert</dc:creator>
      <dc:creator>Barret Schloerke</dc:creator><description><![CDATA[<link href="index_files/libs/shinychat-0.3.0/chat/chat.css" rel="stylesheet" />
<link href="index_files/libs/shinychat-0.3.0/markdown-stream/markdown-stream.css" rel="stylesheet" />
<script src="https://posit-open-source.netlify.app/blog/shiny/shinychat-tool-ui/index_files/libs/shinychat-0.3.0/chat/chat.js" type="module"></script>
<script src="https://posit-open-source.netlify.app/blog/shiny/shinychat-tool-ui/index_files/libs/shinychat-0.3.0/markdown-stream/markdown-stream.js" type="module"></script>
<style>
.highlight-line {
  font-weight: bold;
}
body:not(.modal-open) div.sourceCode pre code.has-line-highlights> span:not(.highlight-line) {
  opacity: 0.6;
}
body:not(.modal-open) div.sourceCode:hover pre code.has-line-highlights> span:not(.highlight-line) {
  opacity: 0.8;
}
.card-header {
  --bs-card-cap-bg: transparent;
}
.shiny-tool-card {
  margin-bottom: 1rem !important;
}
.code-copy-button> .bi::after {
  display: none;
}
</style>
<p>We&rsquo;re jazzed to announce that <a href="https://posit-dev.github.io/shinychat" target="_blank" rel="noopener">shinychat</a>
 now includes rich UI for tool calls!
shinychat makes it easy to build LLM-powered chat interfaces in Shiny apps, and with tool calling UI, your users can see which tools are being executed and their outcomes.
This feature is available in <a href="https://posit-dev.github.io/shinychat/r" target="_blank" rel="noopener">shinychat for R</a>
 (v0.3.0) and <a href="https://posit-dev.github.io/shinychat/py/" target="_blank" rel="noopener">shinychat for Python</a>
 (v0.2.0 or later).</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-1" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-1-1">R</a></li>
<li><a href="#tabset-1-2">Python</a></li>
</ul>
<div id="tabset-1-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;shinychat&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-1-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install shinychat
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>This release brings tool call displays that work with <a href="https://ellmer.tidyverse.org" target="_blank" rel="noopener">ellmer</a>
 (R) and <a href="https://github.com/posit-dev/chatlas" target="_blank" rel="noopener">chatlas</a>
 (Python).
When the LLM calls a tool, shinychat automatically displays the request and result in a collapsible card interface.</p>
<p>In this post we&rsquo;ll cover the new <a href="#tool-calling-ui">Tool calling UI</a>
 features, how to set them up in your apps, and ways to customize the display.
We&rsquo;ll also highlight some <a href="#bookmarking-support">chat bookmarking support</a>
 and <a href="#other-improvements-in-r-v0.3.0">other improvements in shinychat for R v0.3.0</a>
.
As always, you can find the full list of changes in the <a href="https://posit-dev.github.io/shinychat/r/news/index.html#shinychat-030" target="_blank" rel="noopener">R release notes</a>
 and <a href="https://github.com/posit-dev/shinychat/blob/main/pkg-py/CHANGELOG.md" target="_blank" rel="noopener">Python release notes</a>
.</p>
<h2 id="tool-calling-ui">Tool calling UI
</h2>
<p>Tool calling lets you extend an LLM&rsquo;s capabilities by giving it access to functions you define.
When you provide a tool to the LLM, you&rsquo;re telling it &ldquo;here&rsquo;s a function you can call if you need it.&rdquo;
The key thing to understand is that the tool runs on <em>your machine</em> (or wherever your Shiny app is running) &mdash; the LLM doesn&rsquo;t directly run the tool itself.
Instead, it asks <em>you</em> to run the function and return the result.</p>
<p>Both ellmer and chatlas make it easy to define tools and register them with your chat client<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, and they also handle the back-and-forth of tool calls by receiving requests from the LLM, executing the tool, and sending the results back.
This means you can focus on what you do best: writing code to solve problems.</p>
<p>Any problem you can solve with a function can become a tool for an LLM!
You can give the LLM access to live data, APIs, databases, or any other resources your app can reach.</p>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">btw: A complete toolkit for R</span>
</div>
<div class="callout-body">
<p>If you&rsquo;re working in R, <a href="https://posit-dev.github.io/btw" target="_blank" rel="noopener">btw</a>
 is a complete toolkit to help LLMs work better with R.
Whether you&rsquo;re copy-pasting to ChatGPT, chatting with an AI assistant in your IDE, or building LLM-powered apps with shinychat, btw makes it easy to give LLMs the context they need.</p>
<p>And, most importantly, btw provides a full suite of tools for gathering context from R sessions, including tools to: read help pages and vignettes, describe data frames, search for packages on CRAN, read web pages, and more.</p>
<p>Learn more at <a href="https://posit-dev.github.io/btw" target="_blank" rel="noopener">posit-dev.github.io/btw</a>
!</p>
</div>
</div>
<p>When the LLM decides to call a tool, shinychat displays the request and result in the chat interface.
Users can see which tools are being invoked, what arguments are being passed, and what data is being returned.
The tool display is designed to be customizable, so shinychat developers can customize the appearance and display of tool calls to best serve their users.</p>
<h3 id="basic-tool-display">Basic tool display
</h3>
<p>Let&rsquo;s start by creating a simple weather forecasting tool that fetches a weather data (in the United States) for a given latitude and longitude.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-2" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-2-1">R</a></li>
<li><a href="#tabset-2-2">Python</a></li>
</ul>
<div id="tabset-2-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shinychat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ellmer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">weathR</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">get_weather_forecast</span> <span class="o">&lt;-</span> <span class="nf">tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">point_tomorrow</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">,</span> <span class="n">short</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">name</span> <span class="o">=</span> <span class="s">&#34;get_weather_forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Get the weather forecast for a location.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">arguments</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">lat</span> <span class="o">=</span> <span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Latitude&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">lon</span> <span class="o">=</span> <span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Longitude&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Register the tool with your chat client</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="n">ellmer</span><span class="o">::</span><span class="nf">chat</span><span class="p">(</span><span class="s">&#34;openai/gpt-4.1-nano&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">$</span><span class="nf">register_tool</span><span class="p">(</span><span class="n">get_weather_forecast</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-2-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatOpenAI</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">requests</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_weather_forecast</span><span class="p">(</span><span class="n">lat</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">lon</span><span class="p">:</span> <span class="nb">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Get the weather forecast for a location.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">lat_lng</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;latitude=</span><span class="si">{</span><span class="n">lat</span><span class="si">}</span><span class="s2">&amp;longitude=</span><span class="si">{</span><span class="n">lon</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;https://api.open-meteo.com/v1/forecast?</span><span class="si">{</span><span class="n">lat_lng</span><span class="si">}</span><span class="s2">&amp;current=temperature_2m,wind_speed_10m&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()[</span><span class="s2">&#34;current&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Register the tool with your chat client</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">ChatOpenAI</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s2">&#34;gpt-4.1-nano&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">register_tool</span><span class="p">(</span><span class="n">get_weather_forecast</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>With this tool registered, when you ask a weather-related question, the LLM might decide to call the <code>get_weather_forecast()</code> tool to get the latest weather.</p>
<p>In a chat conversation in your R console with ellmer, this might look like the following.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">chat</span><span class="o">$</span><span class="nf">chat</span><span class="p">(</span><span class="s">&#34;Will I need an umbrella for my walk to the T?&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ◯ [tool call] get_weather_forecast(lat = 42.3515, lon = -71.0552)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ● #&gt; [{&#34;time&#34;:&#34;2025-11-20 16:00:00 EST&#34;,&#34;temp&#34;:42,&#34;dewpoint&#34;:0,&#34;humidity&#34;:67,&#34;p_rain&#34;:1,&#34;wi…</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Based on the weather forecast, there is a chance of rain around 4 to 5 PM,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; with mostly cloudy to partly sunny skies. It seems there might be some rain</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; during this time, so carrying an umbrella could be a good idea if you plan</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; to go out around that time. Otherwise, the weather looks relatively clear</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; in the evening.</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Notice that I didn&rsquo;t provide many context clues, but the model correctly guessed that I&rsquo;m walking to the MBTA in Boston, MA and picked <a href="https://www.openstreetmap.org/?mlat=42.35150&amp;mlon=-71.05520#map=16/42.35150/-71.05520&amp;layers=P" target="_blank" rel="noopener">the latitude and longitude for Boston&rsquo;s South Station</a>
.</p>
<p>In shinychat, when the LLM calls the tool, shinychat automatically displays the tool request in a collapsed card:</p>
<pre><code>OpenTelemetry error: there is no package called 'otelsdk'
</code></pre>
<p><shiny-tool-request request-id="tool_call_001" tool-name="get_weather_forecast" arguments="{&quot;lat&quot;:42.3515,&quot;lon&quot;:-71.0552}"></shiny-tool-request></p>
<p>Expanding the card shows the arguments passed to the tool.
When the tool completes, shinychat replaces the request with a card containing the result:</p>
<p><shiny-tool-result request-id="tool_call_002" tool-name="get_weather_forecast" request-call="get_weather_forecast(lat = 42.3515, lon = -71.0552)" status="success" show-request value="[&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 18:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 13,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 19:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 12,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 20:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 10,&#10;    &quot;wind_speed&quot;: 10,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 21:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 10,&#10;    &quot;wind_speed&quot;: 9,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 22:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 0.5556,&#10;    &quot;humidity&quot;: 82,&#10;    &quot;p_rain&quot;: 11,&#10;    &quot;wind_speed&quot;: 8,&#10;    &quot;wind_dir&quot;: &quot;E&quot;,&#10;    &quot;skies&quot;: &quot;Cloudy&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 23:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 0.5556,&#10;    &quot;humidity&quot;: 82,&#10;    &quot;p_rain&quot;: 11,&#10;    &quot;wind_speed&quot;: 7,&#10;    &quot;wind_dir&quot;: &quot;E&quot;,&#10;    &quot;skies&quot;: &quot;Cloudy&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  }&#10;]" value-type="code"></shiny-tool-result></p>
<p>If the tool throws an error, the error is captured and the error message is shown to the LLM.
Typically this happens when the model makes a mistake in calling the tool and often the error message is instructive.</p>
<p>shinychat updates the card to show the error message:</p>
<p><shiny-tool-result request-id="tool_call_001c" tool-name="get_weather_forecast" request-call="get_weather_forecast(lat = 42.3515, lon = -71.0552)" status="error" show-request value="object of type &#39;closure&#39; is not subsettable" value-type="code"></shiny-tool-result></p>
<h3 id="setting-up-streaming">Setting up streaming
</h3>
<p>To enable tool UI in your apps, you need to ensure that tool requests and results are streamed to shinychat:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-3" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-3-1">R</a></li>
<li><a href="#tabset-3-2">Python</a></li>
</ul>
<div id="tabset-3-1">
<p>You don&rsquo;t need to do anything if you&rsquo;re using <code>chat_app()</code> or the chat module via <code>chat_mod_ui()</code> and <code>chat_mod_server()</code>; tool UI is enabled automatically.</p>
<p>If you&rsquo;re using <code>chat_ui()</code> with <code>chat_append()</code>, set <code>stream = &quot;content&quot;</code> when calling <code>$stream_async()</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">&lt;-</span> <span class="n">ellmer</span><span class="o">::</span><span class="nf">chat</span><span class="p">(</span><span class="s">&#34;openai/gpt-4.1-nano&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span><span class="o">$</span><span class="nf">register_tool</span><span class="p">(</span><span class="n">get_weather_forecast</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">chat_user_input</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">stream</span> <span class="o">&lt;-</span> <span class="n">client</span><span class="o">$</span><span class="nf">stream_async</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">chat_user_input</span><span class="p">,</span> <span class="n">stream</span> <span class="o">=</span> <span class="s">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">chat_append</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">stream</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-3-2">
<p>In Python with Shiny Express, use <code>content=&quot;all&quot;</code> when calling <code>stream_async()</code>:</p>
<p><strong>app.py</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatOpenAI</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.express</span> <span class="kn">import</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shinychat.express</span> <span class="kn">import</span> <span class="n">Chat</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">ChatOpenAI</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s2">&#34;gpt-4.1-nano&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">client</span><span class="o">.</span><span class="n">register_tool</span><span class="p">(</span><span class="n">get_weather_forecast</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">Chat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">ui</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@chat.on_user_submit</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">handle_user_input</span><span class="p">(</span><span class="n">user_input</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="k">await</span> <span class="n">client</span><span class="o">.</span><span class="n">stream_async</span><span class="p">(</span><span class="n">user_input</span><span class="p">,</span> <span class="n">content</span><span class="o">=</span><span class="s2">&#34;all&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">chat</span><span class="o">.</span><span class="n">append_message_stream</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>For Shiny Core mode:</p>
<p><strong>app.py</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatOpenAI</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny</span> <span class="kn">import</span> <span class="n">App</span><span class="p">,</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shinychat</span> <span class="kn">import</span> <span class="n">Chat</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">ChatOpenAI</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s2">&#34;gpt-4.1-nano&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">client</span><span class="o">.</span><span class="n">register_tool</span><span class="p">(</span><span class="n">get_weather_forecast</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app_ui</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">page_fluid</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">Chat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">ui</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">server</span><span class="p">(</span><span class="nb">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat</span> <span class="o">=</span> <span class="n">Chat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@chat.on_user_submit</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">handle_user_input</span><span class="p">(</span><span class="n">user_input</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">response</span> <span class="o">=</span> <span class="k">await</span> <span class="n">client</span><span class="o">.</span><span class="n">stream_async</span><span class="p">(</span><span class="n">user_input</span><span class="p">,</span> <span class="n">content</span><span class="o">=</span><span class="s2">&#34;all&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="n">chat</span><span class="o">.</span><span class="n">append_message_stream</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<h3 id="customizing-tool-title-and-icon">Customizing tool title and icon
</h3>
<p>You can enhance the visual presentation of tool requests and results by adding custom titles and icons to your tools.
This helps users quickly identify which tools are being called.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-4" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-4-1">R</a></li>
<li><a href="#tabset-4-2">Python</a></li>
</ul>
<div id="tabset-4-1">
<p>Use <code>tool_annotations()</code> to add a title and icon:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">get_weather_forecast</span> <span class="o">&lt;-</span> <span class="nf">tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">point_tomorrow</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">,</span> <span class="n">short</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">name</span> <span class="o">=</span> <span class="s">&#34;get_weather_forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Get the weather forecast for a location.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">arguments</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">lat</span> <span class="o">=</span> <span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Latitude&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">lon</span> <span class="o">=</span> <span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Longitude&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">annotations</span> <span class="o">=</span> <span class="nf">tool_annotations</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;Weather Forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">icon</span> <span class="o">=</span> <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;cloud-sun&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-4-2">
<p>With chatlas, you can customize the tool display in two ways:</p>
<ol>
<li>
<p>Use the <code>._display</code> attribute to customize the tool display:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">faicons</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_weather_forecast</span><span class="p">(</span><span class="n">lat</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">lon</span><span class="p">:</span> <span class="nb">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Get the weather forecast for a location.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ... implementation ...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">get_weather_forecast</span><span class="o">.</span><span class="n">_display</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;title&#34;</span><span class="p">:</span> <span class="s2">&#34;Weather Forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;icon&#34;</span><span class="p">:</span> <span class="n">faicons</span><span class="o">.</span><span class="n">icon_svg</span><span class="p">(</span><span class="s2">&#34;cloud-sun&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This approach sets the title and icon for all calls to this tool, so it&rsquo;s ideal for predefined tools or tools that are bundled in a Python module or package.</p>
</li>
<li>
<p>Set the tool annotations at registration time:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">register_tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">get_weather_forecast</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">annotations</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;title&#34;</span><span class="p">:</span> <span class="s2">&#34;Weather Forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;icon&#34;</span><span class="p">:</span> <span class="n">faicons</span><span class="o">.</span><span class="n">icon_svg</span><span class="p">(</span><span class="s2">&#34;cloud-sun&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This approach allows you to customize the display for a specific chat client or application without modifying the tool function itself.</p>
</li>
</ol>
</div>
</div>
<p>Now the tool card shows your custom title and icon:</p>
<p><shiny-tool-result request-id="tool_call_004" tool-name="get_weather_forecast" request-call="get_weather_forecast(lat = 42.3515, lon = -71.0552)" status="success" tool-title="Weather Forecast" icon="&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 16 16&quot; class=&quot;bi bi-cloud-sun &quot; style=&quot;height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot; &gt;&lt;path d=&quot;M7 8a3.5 3.5 0 0 1 3.5 3.555.5.5 0 0 0 .624.492A1.503 1.503 0 0 1 13 13.5a1.5 1.5 0 0 1-1.5 1.5H3a2 2 0 1 1 .1-3.998.5.5 0 0 0 .51-.375A3.502 3.502 0 0 1 7 8zm4.473 3a4.5 4.5 0 0 0-8.72-.99A3 3 0 0 0 3 16h8.5a2.5 2.5 0 0 0 0-5h-.027z&quot;&gt;&lt;/path&gt;&#10;&lt;path d=&quot;M10.5 1.5a.5.5 0 0 0-1 0v1a.5.5 0 0 0 1 0v-1zm3.743 1.964a.5.5 0 1 0-.707-.707l-.708.707a.5.5 0 0 0 .708.708l.707-.708zm-7.779-.707a.5.5 0 0 0-.707.707l.707.708a.5.5 0 1 0 .708-.708l-.708-.707zm1.734 3.374a2 2 0 1 1 3.296 2.198c.199.281.372.582.516.898a3 3 0 1 0-4.84-3.225c.352.011.696.055 1.028.129zm4.484 4.074c.6.215 1.125.59 1.522 1.072a.5.5 0 0 0 .039-.742l-.707-.707a.5.5 0 0 0-.854.377zM14.5 6.5a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1h-1z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;" show-request value="[&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 18:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 13,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 19:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 12,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 20:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 10,&#10;    &quot;wind_speed&quot;: 10,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 21:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 10,&#10;    &quot;wind_speed&quot;: 9,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 22:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 0.5556,&#10;    &quot;humidity&quot;: 82,&#10;    &quot;p_rain&quot;: 11,&#10;    &quot;wind_speed&quot;: 8,&#10;    &quot;wind_dir&quot;: &quot;E&quot;,&#10;    &quot;skies&quot;: &quot;Cloudy&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 23:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 0.5556,&#10;    &quot;humidity&quot;: 82,&#10;    &quot;p_rain&quot;: 11,&#10;    &quot;wind_speed&quot;: 7,&#10;    &quot;wind_dir&quot;: &quot;E&quot;,&#10;    &quot;skies&quot;: &quot;Cloudy&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  }&#10;]" value-type="code"></shiny-tool-result></p>
<h3 id="custom-display-content">Custom display content
</h3>
<p>By default, shinychat shows the raw tool result value as a code block.
But often you&rsquo;ll want to present data to users in a more polished format&mdash;like a formatted table or a summary.</p>
<p>You can customize the display by returning alternative content:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-5" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-5-1">R</a></li>
<li><a href="#tabset-5-2">Python</a></li>
</ul>
<div id="tabset-5-1">
<p>Return a <code>ContentToolResult</code> with <code>extra$display</code> containing alternative content:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">get_weather_forecast</span> <span class="o">&lt;-</span> <span class="nf">tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">,</span> <span class="n">location_name</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">forecast_data</span> <span class="o">&lt;-</span> <span class="nf">point_tomorrow</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">,</span> <span class="n">short</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">forecast_table</span> <span class="o">&lt;-</span> <span class="n">gt</span><span class="o">::</span><span class="nf">as_raw_html</span><span class="p">(</span><span class="n">gt</span><span class="o">::</span><span class="nf">gt</span><span class="p">(</span><span class="n">forecast_data</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">ContentToolResult</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">forecast_data</span><span class="p">,</span>  <span class="c1"># This is what the LLM sees</span>
</span></span><span class="line"><span class="cl">      <span class="n">extra</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">display</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">html</span> <span class="o">=</span> <span class="n">forecast_table</span><span class="p">,</span>  <span class="c1"># This is what users see</span>
</span></span><span class="line"><span class="cl">          <span class="n">title</span> <span class="o">=</span> <span class="nf">paste</span><span class="p">(</span><span class="s">&#34;Weather Forecast for&#34;</span><span class="p">,</span> <span class="n">location_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">name</span> <span class="o">=</span> <span class="s">&#34;get_weather_forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Get the weather forecast for a location.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">arguments</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">lat</span> <span class="o">=</span> <span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Latitude&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">lon</span> <span class="o">=</span> <span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Longitude&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">location_name</span> <span class="o">=</span> <span class="nf">type_string</span><span class="p">(</span><span class="s">&#34;Name of the location&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">annotations</span> <span class="o">=</span> <span class="nf">tool_annotations</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;Weather Forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">icon</span> <span class="o">=</span> <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;cloud-sun&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-5-2">
<p>Return a <code>ToolResult</code> with display options:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ToolResult</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="nn">pd</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_weather_forecast</span><span class="p">(</span><span class="n">lat</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">lon</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">location_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Get the weather forecast for a location.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Get forecast data</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">=</span> <span class="n">fetch_weather_data</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Create a DataFrame for the LLM</span>
</span></span><span class="line"><span class="cl">    <span class="n">forecast_df</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Create HTML table for users</span>
</span></span><span class="line"><span class="cl">    <span class="n">forecast_table</span> <span class="o">=</span> <span class="n">forecast_df</span><span class="o">.</span><span class="n">to_html</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">ToolResult</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">value</span><span class="o">=</span><span class="n">forecast_df</span><span class="o">.</span><span class="n">to_dict</span><span class="p">(),</span>  <span class="c1"># LLM sees this</span>
</span></span><span class="line"><span class="cl">        <span class="n">display</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;html&#34;</span><span class="p">:</span> <span class="n">forecast_table</span><span class="p">,</span>  <span class="c1"># Users see this</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;title&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;Weather Forecast for </span><span class="si">{</span><span class="n">location_name</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>The <code>display</code> options support three content types (in order of preference):</p>
<ol>
<li><strong><code>html</code></strong>: HTML content from packages like <code>{gt}</code>, <code>{reactable}</code>, or <code>{htmlwidgets}</code> (R), or Pandas/HTML strings (Python)</li>
<li><strong><code>markdown</code></strong>: Markdown text that&rsquo;s automatically rendered</li>
<li><strong><code>text</code></strong>: Plain text without code formatting</li>
</ol>
<p>Here&rsquo;s what a formatted table looks like in the tool result:</p>
<p><shiny-tool-result request-id="tool_call_007" tool-name="get_weather_forecast" request-call="get_weather_forecast(lat = 42.3515, lon = -71.0552, location_name = &quot;South Station in Boston, MA&quot;)" status="success" tool-title="Weather Forecast for South Station in Boston, MA" icon="&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 16 16&quot; class=&quot;bi bi-cloud-sun &quot; style=&quot;height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot; &gt;&lt;path d=&quot;M7 8a3.5 3.5 0 0 1 3.5 3.555.5.5 0 0 0 .624.492A1.503 1.503 0 0 1 13 13.5a1.5 1.5 0 0 1-1.5 1.5H3a2 2 0 1 1 .1-3.998.5.5 0 0 0 .51-.375A3.502 3.502 0 0 1 7 8zm4.473 3a4.5 4.5 0 0 0-8.72-.99A3 3 0 0 0 3 16h8.5a2.5 2.5 0 0 0 0-5h-.027z&quot;&gt;&lt;/path&gt;&#10;&lt;path d=&quot;M10.5 1.5a.5.5 0 0 0-1 0v1a.5.5 0 0 0 1 0v-1zm3.743 1.964a.5.5 0 1 0-.707-.707l-.708.707a.5.5 0 0 0 .708.708l.707-.708zm-7.779-.707a.5.5 0 0 0-.707.707l.707.708a.5.5 0 1 0 .708-.708l-.708-.707zm1.734 3.374a2 2 0 1 1 3.296 2.198c.199.281.372.582.516.898a3 3 0 1 0-4.84-3.225c.352.011.696.055 1.028.129zm4.484 4.074c.6.215 1.125.59 1.522 1.072a.5.5 0 0 0 .039-.742l-.707-.707a.5.5 0 0 0-.854.377zM14.5 6.5a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1h-1z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;" show-request value="&lt;div id=&quot;sxmpupkcrl&quot; style=&quot;padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;&quot;&gt;&#10;  &#10;  &lt;table class=&quot;gt_table&quot; data-quarto-disable-processing=&quot;false&quot; data-quarto-bootstrap=&quot;false&quot; style=&quot;-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-family: system-ui, &#39;Segoe UI&#39;, Roboto, Helvetica, Arial, sans-serif, &#39;Apple Color Emoji&#39;, &#39;Segoe UI Emoji&#39;, &#39;Segoe UI Symbol&#39;, &#39;Noto Color Emoji&#39;; display: table; border-collapse: collapse; line-height: normal; margin-left: auto; margin-right: auto; color: #333333; font-size: 16px; font-weight: normal; font-style: normal; background-color: #FFFFFF; width: auto; border-top-style: solid; border-top-width: 2px; border-top-color: #A8A8A8; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #A8A8A8; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3;&quot; bgcolor=&quot;#FFFFFF&quot;&gt;&#10;  &lt;thead style=&quot;border-style: none;&quot;&gt;&#10;    &lt;tr class=&quot;gt_col_headings&quot; style=&quot;border-style: none; border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3;&quot;&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_left&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;time&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: left;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;left&quot;&gt;time&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_right&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;temp&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;right&quot;&gt;temp&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_right&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;dewpoint&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;right&quot;&gt;dewpoint&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_right&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;humidity&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;right&quot;&gt;humidity&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_right&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;p_rain&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;right&quot;&gt;p_rain&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_right&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;wind_speed&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;right&quot;&gt;wind_speed&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_left&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;wind_dir&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: left;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;left&quot;&gt;wind_dir&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_left&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;skies&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: left;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;left&quot;&gt;skies&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_center&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;geometry&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: center;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;center&quot;&gt;geometry&lt;/th&gt;&#10;    &lt;/tr&gt;&#10;  &lt;/thead&gt;&#10;  &lt;tbody class=&quot;gt_table_body&quot; style=&quot;border-style: none; border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3;&quot;&gt;&#10;    &lt;tr style=&quot;border-style: none;&quot;&gt;&lt;td headers=&quot;time&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;2026-04-02 18:00:00 EDT&lt;/td&gt;&#10;&lt;td headers=&quot;temp&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;38&lt;/td&gt;&#10;&lt;td headers=&quot;dewpoint&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;1.666667&lt;/td&gt;&#10;&lt;td headers=&quot;humidity&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;89&lt;/td&gt;&#10;&lt;td headers=&quot;p_rain&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;13&lt;/td&gt;&#10;&lt;td headers=&quot;wind_speed&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;14&lt;/td&gt;&#10;&lt;td headers=&quot;wind_dir&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;NE&lt;/td&gt;&#10;&lt;td headers=&quot;skies&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;Patchy Fog&lt;/td&gt;&#10;&lt;td headers=&quot;geometry&quot; class=&quot;gt_row gt_center&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: center;&quot; valign=&quot;middle&quot; align=&quot;center&quot;&gt;c(-71.0552, 42.3515)&lt;/td&gt;&lt;/tr&gt;&#10;    &lt;tr style=&quot;border-style: none;&quot;&gt;&lt;td headers=&quot;time&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;2026-04-02 19:00:00 EDT&lt;/td&gt;&#10;&lt;td headers=&quot;temp&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;38&lt;/td&gt;&#10;&lt;td headers=&quot;dewpoint&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;1.666667&lt;/td&gt;&#10;&lt;td headers=&quot;humidity&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;89&lt;/td&gt;&#10;&lt;td headers=&quot;p_rain&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;12&lt;/td&gt;&#10;&lt;td headers=&quot;wind_speed&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;13&lt;/td&gt;&#10;&lt;td headers=&quot;wind_dir&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;NE&lt;/td&gt;&#10;&lt;td headers=&quot;skies&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;Patchy Fog&lt;/td&gt;&#10;&lt;td headers=&quot;geometry&quot; class=&quot;gt_row gt_center&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: center;&quot; valign=&quot;middle&quot; align=&quot;center&quot;&gt;c(-71.0552, 42.3515)&lt;/td&gt;&lt;/tr&gt;&#10;    &lt;tr style=&quot;border-style: none;&quot;&gt;&lt;td headers=&quot;time&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;2026-04-02 20:00:00 EDT&lt;/td&gt;&#10;&lt;td headers=&quot;temp&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;38&lt;/td&gt;&#10;&lt;td headers=&quot;dewpoint&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;1.666667&lt;/td&gt;&#10;&lt;td headers=&quot;humidity&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;89&lt;/td&gt;&#10;&lt;td headers=&quot;p_rain&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;10&lt;/td&gt;&#10;&lt;td headers=&quot;wind_speed&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;13&lt;/td&gt;&#10;&lt;td headers=&quot;wind_dir&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;NE&lt;/td&gt;&#10;&lt;td headers=&quot;skies&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;Patchy Fog&lt;/td&gt;&#10;&lt;td headers=&quot;geometry&quot; class=&quot;gt_row gt_center&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: center;&quot; valign=&quot;middle&quot; align=&quot;center&quot;&gt;c(-71.0552, 42.3515)&lt;/td&gt;&lt;/tr&gt;&#10;    &lt;tr style=&quot;border-style: none;&quot;&gt;&lt;td headers=&quot;time&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;2026-04-02 21:00:00 EDT&lt;/td&gt;&#10;&lt;td headers=&quot;temp&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;38&lt;/td&gt;&#10;&lt;td headers=&quot;dewpoint&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;1.666667&lt;/td&gt;&#10;&lt;td headers=&quot;humidity&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;89&lt;/td&gt;&#10;&lt;td headers=&quot;p_rain&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;10&lt;/td&gt;&#10;&lt;td headers=&quot;wind_speed&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;13&lt;/td&gt;&#10;&lt;td headers=&quot;wind_dir&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;E&lt;/td&gt;&#10;&lt;td headers=&quot;skies&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;Patchy Fog&lt;/td&gt;&#10;&lt;td headers=&quot;geometry&quot; class=&quot;gt_row gt_center&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: center;&quot; valign=&quot;middle&quot; align=&quot;center&quot;&gt;c(-71.0552, 42.3515)&lt;/td&gt;&lt;/tr&gt;&#10;    &lt;tr style=&quot;border-style: none;&quot;&gt;&lt;td headers=&quot;time&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;2026-04-02 22:00:00 EDT&lt;/td&gt;&#10;&lt;td headers=&quot;temp&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;38&lt;/td&gt;&#10;&lt;td headers=&quot;dewpoint&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;1.111111&lt;/td&gt;&#10;&lt;td headers=&quot;humidity&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;85&lt;/td&gt;&#10;&lt;td headers=&quot;p_rain&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;11&lt;/td&gt;&#10;&lt;td headers=&quot;wind_speed&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;12&lt;/td&gt;&#10;&lt;td headers=&quot;wind_dir&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;E&lt;/td&gt;&#10;&lt;td headers=&quot;skies&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;Patchy Fog&lt;/td&gt;&#10;&lt;td headers=&quot;geometry&quot; class=&quot;gt_row gt_center&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: center;&quot; valign=&quot;middle&quot; align=&quot;center&quot;&gt;c(-71.0552, 42.3515)&lt;/td&gt;&lt;/tr&gt;&#10;    &lt;tr style=&quot;border-style: none;&quot;&gt;&lt;td headers=&quot;time&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;2026-04-02 23:00:00 EDT&lt;/td&gt;&#10;&lt;td headers=&quot;temp&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;38&lt;/td&gt;&#10;&lt;td headers=&quot;dewpoint&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;1.111111&lt;/td&gt;&#10;&lt;td headers=&quot;humidity&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;85&lt;/td&gt;&#10;&lt;td headers=&quot;p_rain&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;11&lt;/td&gt;&#10;&lt;td headers=&quot;wind_speed&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;10&lt;/td&gt;&#10;&lt;td headers=&quot;wind_dir&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;E&lt;/td&gt;&#10;&lt;td headers=&quot;skies&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;Patchy Fog&lt;/td&gt;&#10;&lt;td headers=&quot;geometry&quot; class=&quot;gt_row gt_center&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: center;&quot; valign=&quot;middle&quot; align=&quot;center&quot;&gt;c(-71.0552, 42.3515)&lt;/td&gt;&lt;/tr&gt;&#10;  &lt;/tbody&gt;&#10;  &#10;&lt;/table&gt;&#10;&lt;/div&gt;" value-type="html"></shiny-tool-result></p>
<h3 id="additional-display-options">Additional display options
</h3>
<p>You can control how tool results are presented using additional display options:</p>
<ul>
<li><code>show_request = FALSE</code>: Hide the tool call details when they&rsquo;re obvious from the display</li>
<li><code>open = TRUE</code>: Expand the result panel by default (useful for rich content like maps or charts)</li>
<li><code>title</code> and <code>icon</code>: Override the tool&rsquo;s default title and icon for this specific result</li>
</ul>
<p>Another helpful feature is to include an <code>_intent</code> argument in your tool definition.
When present in the tool arguments, shinychat shows the <code>_intent</code> value in the tool card header, helping users understand why the LLM is calling the tool.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-6" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-6-1">R</a></li>
<li><a href="#tabset-6-2">Python</a></li>
</ul>
<div id="tabset-6-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">tool_with_intent</span> <span class="o">&lt;-</span> <span class="nf">tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">`_intent`</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">runif</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">name</span> <span class="o">=</span> <span class="s">&#34;random_number&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Generate a random number.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">arguments</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">`_intent`</span> <span class="o">=</span> <span class="nf">type_string</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;Explain why you&#39;re generating this number&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-6-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">random_number</span><span class="p">(</span><span class="n">_intent</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Generate a random number.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    Args:
</span></span></span><span class="line"><span class="cl"><span class="s2">        _intent: Explain why you&#39;re generating this number
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">import</span> <span class="nn">random</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>Notice that the tool function itself doesn&rsquo;t actually use the <code>_intent</code> argument, but its presence allows shinychat to give the user additional context about the tool call.</p>
<h2 id="bookmarking-support">Bookmarking support
</h2>
<p>When a Shiny app reloads, the app returns to its initial state, unless the URL includes <a href="https://shiny.posit.co/r/articles/build/bookmarking-state/" target="_blank" rel="noopener">bookmarked state</a>
.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>
Automatically updating the URL to include a bookmark of the chat state is a great way to help users return to their work if they accidentally refresh the page or unexpectedly lose their connection.</p>
<p>Both shinychat for R and Python provide helper functions that make it easy to restore conversations with bookmarks.
This means users can refresh the page or share a URL and pick up right where they left off.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-7" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-7-1">R</a></li>
<li><a href="#tabset-7-2">Python</a></li>
</ul>
<div id="tabset-7-1">
<p>In R, the <code>chat_restore()</code> function restores the message history from the bookmark when the app starts up <em>and</em> ensures that the chat client state is automatically bookmarked on user input and assistant responses.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shiny</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shinychat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nf">page_fillable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">chat_ui</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat_client</span> <span class="o">&lt;-</span> <span class="n">ellmer</span><span class="o">::</span><span class="nf">chat_openai</span><span class="p">(</span><span class="n">model</span> <span class="o">=</span> <span class="s">&#34;gpt-4o-mini&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Automatically save chat state on user input and responses</span>
</span></span><span class="line"><span class="cl">  <span class="nf">chat_restore</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">chat_client</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">chat_user_input</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">stream</span> <span class="o">&lt;-</span> <span class="n">chat_client</span><span class="o">$</span><span class="nf">stream_async</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">chat_user_input</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">chat_append</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">stream</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enable URL-based bookmarking</span>
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">,</span> <span class="n">enableBookmarking</span> <span class="o">=</span> <span class="s">&#34;url&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>enableBookmarking = &quot;url&quot;</code> stores the chat state in encoded data in the query string of the app&rsquo;s URL.
Because browsers have native limitations on the size of a URL, you should use <code>enableBookmarking = &quot;server&quot;</code> to store state server-side without URL size limitations for chatbots expected to have large conversation histories.</p>
<p>And if you&rsquo;re using <code>chat_app()</code> for quick prototypes, bookmarking is already enabled automatically.</p>
</div>
<div id="tabset-7-2">
<p>In Python, the <code>.enable_bookmarking()</code> method handles the where, when, and how of bookmarking chat state.</p>
<h3 id="express-mode">Express mode
</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatOllama</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.express</span> <span class="kn">import</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">chat_client</span> <span class="o">=</span> <span class="n">ChatOllama</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s2">&#34;llama3.2&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">Chat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">ui</span><span class="p">(</span><span class="n">messages</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;Welcome!&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">enable_bookmarking</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat_client</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">bookmark_store</span><span class="o">=</span><span class="s2">&#34;url&#34;</span><span class="p">,</span> <span class="c1"># or &#34;server&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">bookmark_on</span><span class="o">=</span><span class="s2">&#34;response&#34;</span><span class="p">,</span> <span class="c1"># or None</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="core-mode">Core mode
</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatOllama</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny</span> <span class="kn">import</span> <span class="n">ui</span><span class="p">,</span> <span class="n">App</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app_ui</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">page_fixed</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">chat_ui</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">,</span> <span class="n">messages</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;Welcome!&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">server</span><span class="p">(</span><span class="nb">input</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat_client</span> <span class="o">=</span> <span class="n">ChatOllama</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s2">&#34;llama3.2&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">Chat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">chat</span><span class="o">.</span><span class="n">enable_bookmarking</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat_client</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">bookmark_on</span><span class="o">=</span><span class="s2">&#34;response&#34;</span><span class="p">,</span> <span class="c1"># or None</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">,</span> <span class="n">bookmark_store</span><span class="o">=</span><span class="s2">&#34;url&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="configuration-options">Configuration options
</h3>
<p>The <code>.enable_bookmarking()</code> method handles three aspects of bookmarking:</p>
<ol>
<li><strong>Where</strong> (<code>bookmark_store</code>)
<ul>
<li><code>&quot;url&quot;</code>: Store the state in the URL.</li>
<li><code>&quot;server&quot;</code>: Store the state on the server. Consider this over <code>&quot;url&quot;</code> if you want to support a large amount of state, or have other bookmark state that can&rsquo;t be serialized to JSON.</li>
</ul>
</li>
<li><strong>When</strong> (<code>bookmark_on</code>)
<ul>
<li><code>&quot;response&quot;</code>: Triggers a bookmark when an <code>&quot;assistant&quot;</code> response is appended.</li>
<li><code>None</code>: Don&rsquo;t trigger a bookmark automatically. This assumes you&rsquo;ll be triggering bookmarks through other means (e.g., a button).</li>
</ul>
</li>
<li><strong>How</strong> is handled automatically by registering the relevant <code>on_bookmark</code> and <code>on_restore</code> callbacks.</li>
</ol>
<p>When <code>.enable_bookmarking()</code> triggers a bookmark for you, it&rsquo;ll also update the URL query string to include the bookmark state.
This way, when the user unexpectedly loses connection, they can load the current URL to restore the chat state, or go back to the original URL to start over.</p>
</div>
</div>
<h2 id="other-improvements-in-shinychat-for-r">Other improvements in shinychat for R
</h2>
<p>Beyond tool calling UI and bookmarking support, shinychat for R v0.3.0 includes several other enhancements.</p>
<h3 id="better-programmatic-control">Better programmatic control
</h3>
<p><code>chat_mod_server()</code> now returns a set of reactive values and functions for controlling the chat interface:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_mod_server</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">ellmer</span><span class="o">::</span><span class="nf">chat_openai</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># React to user input</span>
</span></span><span class="line"><span class="cl">  <span class="nf">observe</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nf">req</span><span class="p">(</span><span class="n">chat</span><span class="o">$</span><span class="nf">last_input</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="nf">print</span><span class="p">(</span><span class="nf">paste</span><span class="p">(</span><span class="s">&#34;User said:&#34;</span><span class="p">,</span> <span class="n">chat</span><span class="o">$</span><span class="nf">last_input</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># React to assistant responses</span>
</span></span><span class="line"><span class="cl">  <span class="nf">observe</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nf">req</span><span class="p">(</span><span class="n">chat</span><span class="o">$</span><span class="nf">last_turn</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="nf">print</span><span class="p">(</span><span class="s">&#34;Assistant completed response&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Programmatically control the chat</span>
</span></span><span class="line"><span class="cl">  <span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">suggest_question</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat</span><span class="o">$</span><span class="nf">update_user_input</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">value</span> <span class="o">=</span> <span class="s">&#34;What&#39;s the weather like today?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">submit</span> <span class="o">=</span> <span class="kc">TRUE</span>  <span class="c1"># Automatically submit</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">reset</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat</span><span class="o">$</span><span class="nf">clear</span><span class="p">()</span>  <span class="c1"># Clear history and UI</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The returned list includes:</p>
<ul>
<li><strong><code>last_input</code></strong> and <strong><code>last_turn</code></strong> reactives for monitoring chat state</li>
<li><strong><code>update_user_input()</code></strong> for programmatically setting or submitting user input&mdash;great for suggested prompts or guided conversations</li>
<li><strong><code>append()</code></strong> for adding messages to the chat UI</li>
<li><strong><code>clear()</code></strong> for resetting the chat, with options to control how the client history is handled</li>
<li><strong><code>client</code></strong> for direct access to the ellmer chat client</li>
</ul>
<p>There&rsquo;s also a standalone <code>update_chat_user_input()</code> function if you&rsquo;re using <code>chat_ui()</code> directly, which supports updating the placeholder text and moving focus to the input.</p>
<h3 id="custom-assistant-icons">Custom assistant icons
</h3>
<p>You can now customize the icon shown next to assistant messages to better match your application&rsquo;s branding or to distinguish between different assistants:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">bsicons</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Set a custom icon for a specific response</span>
</span></span><span class="line"><span class="cl"><span class="nf">chat_append</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;chat&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Here&#39;s some helpful information!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">icon</span> <span class="o">=</span> <span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;lightbulb&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Or set a default icon for all assistant messages</span>
</span></span><span class="line"><span class="cl"><span class="nf">chat_ui</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">icon_assistant</span> <span class="o">=</span> <span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;robot&#34;</span><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This is especially useful when building multi-agent applications where different assistants might have different personalities or roles.</p>
<h3 id="safer-external-links">Safer external links
</h3>
<p>External links in chat messages now open in a new tab with a confirmation dialog.
This prevents users from accidentally navigating away from the chat session and losing their conversation.
This is particularly helpful when LLMs include links in their responses, for example when shinychat is used in combination with Retrieval Augmented Generation via <a href="https://ragnar.tidyverse.org" target="_blank" rel="noopener">ragnar</a>
.</p>
<h2 id="learn-more">Learn more
</h2>
<p>The tool calling UI opens up exciting possibilities for building transparent, user-friendly AI applications.
Whether you&rsquo;re fetching data, running calculations, or integrating with external services, users can now see exactly what&rsquo;s happening.</p>
<p>To dive deeper:</p>
<ul>
<li>Read the <a href="https://posit-dev.github.io/shinychat/r/articles/tool-ui.html" target="_blank" rel="noopener">tool calling UI article</a>
 for comprehensive examples in R</li>
<li>Explore tool calling with <a href="https://ellmer.tidyverse.org/articles/tool-calling.html" target="_blank" rel="noopener">ellmer</a>
 (R) or <a href="https://posit-dev.github.io/chatlas/tool-calling/displays.html" target="_blank" rel="noopener">chatlas</a>
 (Python)</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A huge thank you to everyone who contributed to this release with bug reports, feature requests, and code contributions:</p>
<p><a href="https://github.com/bianchenhao" target="_blank" rel="noopener">@bianchenhao</a>
, <a href="https://github.com/cboettig" target="_blank" rel="noopener">@cboettig</a>
, <a href="https://github.com/chendaniely" target="_blank" rel="noopener">@chendaniely</a>
, <a href="https://github.com/cpsievert" target="_blank" rel="noopener">@cpsievert</a>
, <a href="https://github.com/DavZim" target="_blank" rel="noopener">@DavZim</a>
, <a href="https://github.com/DeepanshKhurana" target="_blank" rel="noopener">@DeepanshKhurana</a>
, <a href="https://github.com/DivadNojnarg" target="_blank" rel="noopener">@DivadNojnarg</a>
, <a href="https://github.com/gadenbuie" target="_blank" rel="noopener">@gadenbuie</a>
, <a href="https://github.com/iainwallacebms" target="_blank" rel="noopener">@iainwallacebms</a>
, <a href="https://github.com/janlimbeck" target="_blank" rel="noopener">@janlimbeck</a>
, <a href="https://github.com/jcheng5" target="_blank" rel="noopener">@jcheng5</a>
, <a href="https://github.com/jimrothstein" target="_blank" rel="noopener">@jimrothstein</a>
, <a href="https://github.com/karangattu" target="_blank" rel="noopener">@karangattu</a>
, <a href="https://github.com/ManuelSpinola" target="_blank" rel="noopener">@ManuelSpinola</a>
, <a href="https://github.com/MohoWu" target="_blank" rel="noopener">@MohoWu</a>
, <a href="https://github.com/nissinbo" target="_blank" rel="noopener">@nissinbo</a>
, <a href="https://github.com/noamanemobidata" target="_blank" rel="noopener">@noamanemobidata</a>
, <a href="https://github.com/parmsam" target="_blank" rel="noopener">@parmsam</a>
, <a href="https://github.com/PaulC91" target="_blank" rel="noopener">@PaulC91</a>
, <a href="https://github.com/rkennedy01" target="_blank" rel="noopener">@rkennedy01</a>
, <a href="https://github.com/schloerke" target="_blank" rel="noopener">@schloerke</a>
, <a href="https://github.com/selesnow" target="_blank" rel="noopener">@selesnow</a>
, <a href="https://github.com/simonpcouch" target="_blank" rel="noopener">@simonpcouch</a>
, <a href="https://github.com/skaltman" target="_blank" rel="noopener">@skaltman</a>
, <a href="https://github.com/stefanlinner" target="_blank" rel="noopener">@stefanlinner</a>
, <a href="https://github.com/t-kalinowski" target="_blank" rel="noopener">@t-kalinowski</a>
, <a href="https://github.com/thendrix-trlm" target="_blank" rel="noopener">@thendrix-trlm</a>
, <a href="https://github.com/wch" target="_blank" rel="noopener">@wch</a>
, <a href="https://github.com/wlandau" target="_blank" rel="noopener">@wlandau</a>
, and <a href="https://github.com/Yousuf28" target="_blank" rel="noopener">@Yousuf28</a>
.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>See the <a href="https://ellmer.tidyverse.org/articles/tool-calling.html" target="_blank" rel="noopener">ellmer tool calling documentation</a>
 for R and the <a href="https://posit-dev.github.io/chatlas/tool-calling/how-it-works.html" target="_blank" rel="noopener">chatlas tool calling documentation</a>
 for Python for more details on defining and registering tools.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>This can be especially frustrating behavior since hosted apps, by default, will close an idle session after a certain (<a href="https://docs.posit.co/shinyapps.io/guide/applications/#advanced-settings" target="_blank" rel="noopener">configurable</a>
) amount of time.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/shiny/shinychat-tool-ui/feature.png" length="147726" type="image/png" />
    </item>
    <item>
      <title>ellmer 0.4.0</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/2025/ellmer-0-4-0/</link>
      <pubDate>Tue, 18 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/2025/ellmer-0-4-0/</guid>
      <dc:creator>Hadley Wickham</dc:creator><description><![CDATA[<!--
TODO:
* [x] Look over / edit the post's title in the yaml
* [x] Edit (or delete) the description; note this appears in the Twitter card
* [x] Pick category and tags (see existing with [`hugodown::tidy_show_meta()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html))
* [x] Find photo & update yaml metadata
* [x] Create `thumbnail-sq.jpg`; height and width should be equal
* [x] Create `thumbnail-wd.jpg`; width should be >5x height
* [x] [`hugodown::use_tidy_thumbnails()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html)
* [x] Add intro sentence, e.g. the standard tagline for the package
* [ ] [`usethis::use_tidy_thanks()`](https://usethis.r-lib.org/reference/use_tidy_thanks.html)
-->
<p>We&rsquo;re very happy to announce the release of <a href="https://ellmer.tidyverse.org" target="_blank" rel="noopener">ellmer</a>
 0.4.0. ellmer makes it easy to chat with a large language model directly from R. It supports a wide variety of providers (including OpenAI, Anthropic, Azure, Google, Snowflake, Databricks and many more), makes it easy to <a href="https://ellmer.tidyverse.org/articles/structured-data.html" target="_blank" rel="noopener">extract structured data</a>
, and to give the LLM the ability to call R functions via <a href="https://ellmer.tidyverse.org/articles/tool-calling.html" target="_blank" rel="noopener">tool calling</a>
.</p>
<p>You can install it from CRAN with:</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='nf'><a href='https://rdrr.io/r/utils/install.packages.html'>install.packages</a></span><span class='o'>(</span><span class='s'>"ellmer"</span><span class='o'>)</span></span></code></pre>
</div>
<p>This blog post will cover the major changes in this release, including important lifecycle updates, new features for Claude (caching, file uploads, and web tools), improvements to OpenAI support (responses API and built-in tools), and a variety of enhancements to error handling, pricing tracking, and security.</p>
<p>You can see a full list of changes in the <a href="https://github.com/tidyverse/ellmer/releases/tag/v0.4.0" target="_blank" rel="noopener">release notes</a>
.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://ellmer.tidyverse.org'>ellmer</a></span><span class='o'>)</span></span></code></pre>
</div>
<h2 id="lifecycle">Lifecycle
</h2>
<p><a href="https://ellmer.tidyverse.org/reference/parallel_chat.html" target="_blank" rel="noopener"><code>parallel_chat()</code></a>
 and <a href="https://ellmer.tidyverse.org/reference/batch_chat.html" target="_blank" rel="noopener"><code>batch_chat()</code></a>
 are no longer experimental. Based on user feedback, both <a href="https://ellmer.tidyverse.org/reference/parallel_chat.html" target="_blank" rel="noopener"><code>parallel_chat()</code></a>
 and <a href="https://ellmer.tidyverse.org/reference/batch_chat.html" target="_blank" rel="noopener"><code>batch_chat()</code></a>
 do a much better job of handling errors, and I&rsquo;m confident that they&rsquo;re around to stay.</p>
<p>Reflecting Anthropic&rsquo;s recent rebranding of developer tools under the Claude name, <a href="https://ellmer.tidyverse.org/reference/chat_anthropic.html" target="_blank" rel="noopener"><code>chat_claude()</code></a>
 is no longer deprecated and is an alias for <a href="https://ellmer.tidyverse.org/reference/chat_anthropic.html" target="_blank" rel="noopener"><code>chat_anthropic()</code></a>
. New <a href="https://ellmer.tidyverse.org/reference/chat_anthropic.html" target="_blank" rel="noopener"><code>models_claude()</code></a>
 is now an alias for <a href="https://ellmer.tidyverse.org/reference/chat_anthropic.html" target="_blank" rel="noopener"><code>models_anthropic()</code></a>
.</p>
<p>The following deprecated functions/arguments/methods have been removed:</p>
<ul>
<li><code>Chat$extract_data()</code> -&gt; <code>chat$chat_structured()</code> (0.2.0)</li>
<li><code>Chat$extract_data_async()</code> -&gt; <code>chat$chat_structured_async()</code> (0.2.0)</li>
<li><code>chat_anthropic(max_tokens)</code> -&gt; <code>chat_anthropic(params)</code> (0.2.0)</li>
<li><code>chat_azure()</code> -&gt; <a href="https://ellmer.tidyverse.org/reference/chat_azure_openai.html" target="_blank" rel="noopener"><code>chat_azure_openai()</code></a>
 (0.2.0)</li>
<li><code>chat_azure_openai(token)</code> (0.1.1)</li>
<li><code>chat_bedrock()</code> -&gt; <a href="https://ellmer.tidyverse.org/reference/chat_aws_bedrock.html" target="_blank" rel="noopener"><code>chat_aws_bedrock()</code></a>
 (0.2.0)</li>
<li><a href="https://ellmer.tidyverse.org/reference/chat_anthropic.html" target="_blank" rel="noopener"><code>chat_claude()</code></a>
 -&gt; <a href="https://ellmer.tidyverse.org/reference/chat_anthropic.html" target="_blank" rel="noopener"><code>chat_anthropic()</code></a>
 (0.2.0)</li>
<li><code>chat_cortex()</code> -&gt; <a href="https://ellmer.tidyverse.org/reference/chat_snowflake.html" target="_blank" rel="noopener"><code>chat_snowflake()</code></a>
 (0.2.0)</li>
<li><code>chat_gemini()</code> -&gt; <a href="https://ellmer.tidyverse.org/reference/chat_google_gemini.html" target="_blank" rel="noopener"><code>chat_google_gemini()</code></a>
 (0.2.0)</li>
<li><code>chat_openai(seed)</code> -&gt; <code>chat_openai(params)</code> (0.2.0)</li>
<li><code>create_tool_def(model)</code> -&gt; <code>create_tool_def(chat)</code> (0.2.0)</li>
</ul>
<h2 id="chat_claude"><code>chat_claude()</code>
</h2>
<p><a href="https://ellmer.tidyverse.org/reference/chat_anthropic.html" target="_blank" rel="noopener"><code>chat_claude()</code></a>
 gains a new <code>cache</code> parameter to control caching. By default it is set to &ldquo;5m&rdquo;. Claude&rsquo;s caching model is rather difficult to understand, but I&rsquo;m reasonably confident that this will reduce your costs overall. <a href="https://ellmer.tidyverse.org/reference/chat_anthropic.html" target="_blank" rel="noopener"><code>?chat_claude</code></a>
 goes into the details of why I think this will save you money.</p>
<p>With help from @dcomputing, ellmer has gained a suite of file management helpers such as <a href="https://ellmer.tidyverse.org/reference/claude_file_upload.html" target="_blank" rel="noopener"><code>claude_file_upload()</code></a>
, <a href="https://ellmer.tidyverse.org/reference/claude_file_upload.html" target="_blank" rel="noopener"><code>claude_file_list()</code></a>
, <a href="https://ellmer.tidyverse.org/reference/claude_file_upload.html" target="_blank" rel="noopener"><code>claude_file_delete()</code></a>
, and so on. These allow you to upload <a href="https://docs.claude.com/en/docs/build-with-claude/files#file-types-and-content-blocks" target="_blank" rel="noopener">a variety of file types</a>
 for investigation.</p>
<p>You can now take advantage of Claude&rsquo;s built-in <a href="https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool" target="_blank" rel="noopener">web search</a>
 and <a href="https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-fetch-tool" target="_blank" rel="noopener">web fetch</a>
 with <a href="https://ellmer.tidyverse.org/reference/claude_tool_web_search.html" target="_blank" rel="noopener"><code>claude_tool_web_search()</code></a>
 and <a href="https://ellmer.tidyverse.org/reference/claude_tool_web_fetch.html" target="_blank" rel="noopener"><code>claude_tool_web_fetch()</code></a>
. These empower Claude to perform web searches and read web pages on your behalf.</p>
<h2 id="chat_openai-and-chat_openai_compatible"><code>chat_openai()</code> and <code>chat_openai_compatible()</code>
</h2>
<p><a href="https://ellmer.tidyverse.org/reference/chat_openai.html" target="_blank" rel="noopener"><code>chat_openai()</code></a>
 now uses OpenAI&rsquo;s more modern &ldquo;responses API&rdquo;. This is their now-recommended API, and unlocks the ability to use the built-in tools, such as web search with <a href="https://ellmer.tidyverse.org/reference/openai_tool_web_search.html" target="_blank" rel="noopener"><code>openai_tool_web_search()</code></a>
. It also gains a <code>service_tier</code> argument which allows you to request slower/cheaper or faster/more expensive results.</p>
<p>If you want to talk to a model provider that is OpenAI API compatible (i.e. uses the older &ldquo;chat completions&rdquo; API), you&rsquo;ll need to use <a href="https://ellmer.tidyverse.org/reference/chat_openai_compatible.html" target="_blank" rel="noopener"><code>chat_openai_compatible()</code></a>
.</p>
<h2 id="new-features">New features
</h2>
<ul>
<li>
<p><a href="https://ellmer.tidyverse.org/reference/parallel_chat.html" target="_blank" rel="noopener"><code>parallel_chat()</code></a>
 and <a href="https://ellmer.tidyverse.org/reference/batch_chat.html" target="_blank" rel="noopener"><code>batch_chat()</code></a>
 are much better at dealing with errors, and should now (by and large) succeed even if not all prompts succeeded or return badly formatted output. This does make the output from <a href="https://ellmer.tidyverse.org/reference/parallel_chat.html" target="_blank" rel="noopener"><code>parallel_chat()</code></a>
 a bit more complex, since it can now be a mix of <code>Chat</code> objects, error objects, and <code>NULL</code>, but we think the trade-off is worth it.</p>
</li>
<li>
<p><a href="https://ellmer.tidyverse.org/reference/batch_chat.html" target="_blank" rel="noopener"><code>batch_chat()</code></a>
 and friends have a revised hashing mechanism which is used to ensure that you don&rsquo;t accidentally use saved results with the wrong inputs. The mechanism now only hashes the provider <code>name</code>, <code>model</code>, and <code>base_url</code>. This should provide some protection from accidentally reusing the same <code>.json</code> file with different providers, while still allowing you to use the same batch file across ellmer versions. There&rsquo;s also a new <code>ignore_hash</code> argument that allows you to opt out of the check if you&rsquo;re confident the difference only arises because ellmer itself has changed.</p>
</li>
<li>
<p>There were a bunch of smaller improvements to pricing: the package now uses the latest pricing data, <a href="https://ellmer.tidyverse.org/reference/batch_chat.html" target="_blank" rel="noopener"><code>batch_chat()</code></a>
 only records costs on retrieval, <code>Chat$get_tokens()</code> includes cost information, and the print method does a better job of matching underlying data.</p>
</li>
<li>
<p><a href="https://ellmer.tidyverse.org/reference/params.html" target="_blank" rel="noopener"><code>params()</code></a>
 gains new <code>reasoning_effort</code> and <code>reasoning_tokens</code> so you can control the amount of effort a reasoning model spends on thinking. Initial support is provided for <a href="https://ellmer.tidyverse.org/reference/chat_anthropic.html" target="_blank" rel="noopener"><code>chat_claude()</code></a>
, <a href="https://ellmer.tidyverse.org/reference/chat_google_gemini.html" target="_blank" rel="noopener"><code>chat_google_gemini()</code></a>
, and <a href="https://ellmer.tidyverse.org/reference/chat_openai.html" target="_blank" rel="noopener"><code>chat_openai()</code></a>
.</p>
</li>
<li>
<p><code>chat_*()</code> functions now use a <code>credentials</code> function instead of an <code>api_key</code> value. This means that API keys are never stored in the chat object (which might be saved to disk), but are instead retrieved on demand as needed. You generally shouldn&rsquo;t need to use the <code>credentials</code> argument directly yourself, but when you do, you should use it to dynamically retrieve the API key from some other source (i.e. never inline a secret directly into a function call).</p>
</li>
<li>
<p><a href="https://ellmer.tidyverse.org/reference/tool.html" target="_blank" rel="noopener"><code>tool()</code></a>
s can now return image or PDF content types, with <a href="https://ellmer.tidyverse.org/reference/content_image_url.html" target="_blank" rel="noopener"><code>content_image_file()</code></a>
 or <code>content_pdf()</code>.</p>
</li>
<li>
<p>You can use the new <code>schema_df()</code> to describe the schema of a data frame to an LLM. It&rsquo;s designed to give a high-quality summary without spending too many tokens.</p>
</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A big thanks to everyone who contributed to this release! <a href="https://github.com/abiyug" target="_blank" rel="noopener">@abiyug</a>
, <a href="https://github.com/AdaemmerP" target="_blank" rel="noopener">@AdaemmerP</a>
, <a href="https://github.com/AlmogAngel" target="_blank" rel="noopener">@AlmogAngel</a>
, <a href="https://github.com/app2let" target="_blank" rel="noopener">@app2let</a>
, <a href="https://github.com/benhmin" target="_blank" rel="noopener">@benhmin</a>
, <a href="https://github.com/bensoltoff" target="_blank" rel="noopener">@bensoltoff</a>
, <a href="https://github.com/benzipperer" target="_blank" rel="noopener">@benzipperer</a>
, <a href="https://github.com/bianchenhao" target="_blank" rel="noopener">@bianchenhao</a>
, <a href="https://github.com/bshor" target="_blank" rel="noopener">@bshor</a>
, <a href="https://github.com/CChen89" target="_blank" rel="noopener">@CChen89</a>
, <a href="https://github.com/cherylisabella" target="_blank" rel="noopener">@cherylisabella</a>
, <a href="https://github.com/cpsievert" target="_blank" rel="noopener">@cpsievert</a>
, <a href="https://github.com/dcomputing" target="_blank" rel="noopener">@dcomputing</a>
, <a href="https://github.com/durraniu" target="_blank" rel="noopener">@durraniu</a>
, <a href="https://github.com/fh-slangerman" target="_blank" rel="noopener">@fh-slangerman</a>
, <a href="https://github.com/flaviaerius" target="_blank" rel="noopener">@flaviaerius</a>
, <a href="https://github.com/foton263" target="_blank" rel="noopener">@foton263</a>
, <a href="https://github.com/gadenbuie" target="_blank" rel="noopener">@gadenbuie</a>
, <a href="https://github.com/gary-mu" target="_blank" rel="noopener">@gary-mu</a>
, <a href="https://github.com/Green-State-Data" target="_blank" rel="noopener">@Green-State-Data</a>
, <a href="https://github.com/hadley" target="_blank" rel="noopener">@hadley</a>
, <a href="https://github.com/howardbaik" target="_blank" rel="noopener">@howardbaik</a>
, <a href="https://github.com/jeroenjanssens" target="_blank" rel="noopener">@jeroenjanssens</a>
, <a href="https://github.com/jharvey-records" target="_blank" rel="noopener">@jharvey-records</a>
, <a href="https://github.com/joranE" target="_blank" rel="noopener">@joranE</a>
, <a href="https://github.com/kbenoit" target="_blank" rel="noopener">@kbenoit</a>
, <a href="https://github.com/LukasWallrich" target="_blank" rel="noopener">@LukasWallrich</a>
, <a href="https://github.com/m20m22" target="_blank" rel="noopener">@m20m22</a>
, <a href="https://github.com/maciekbanas" target="_blank" rel="noopener">@maciekbanas</a>
, <a href="https://github.com/mattwarkentin" target="_blank" rel="noopener">@mattwarkentin</a>
, <a href="https://github.com/parmsam" target="_blank" rel="noopener">@parmsam</a>
, <a href="https://github.com/parmsam-pfizer" target="_blank" rel="noopener">@parmsam-pfizer</a>
, <a href="https://github.com/promothesh" target="_blank" rel="noopener">@promothesh</a>
, <a href="https://github.com/rempsyc" target="_blank" rel="noopener">@rempsyc</a>
, <a href="https://github.com/roldanalex" target="_blank" rel="noopener">@roldanalex</a>
, <a href="https://github.com/rplsmn" target="_blank" rel="noopener">@rplsmn</a>
, <a href="https://github.com/schloerke" target="_blank" rel="noopener">@schloerke</a>
, <a href="https://github.com/simonpcouch" target="_blank" rel="noopener">@simonpcouch</a>
, <a href="https://github.com/t-kalinowski" target="_blank" rel="noopener">@t-kalinowski</a>
, <a href="https://github.com/wklimowicz" target="_blank" rel="noopener">@wklimowicz</a>
, <a href="https://github.com/wlandau" target="_blank" rel="noopener">@wlandau</a>
, and <a href="https://github.com/xx02al" target="_blank" rel="noopener">@xx02al</a>
.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/2025/ellmer-0-4-0/thumbnail-wd.jpg" length="190301" type="image/jpeg" />
    </item>
    <item>
      <title>Q3 2025 tidymodels digest</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/2025/tidymodels-2025-q3/</link>
      <pubDate>Tue, 18 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/2025/tidymodels-2025-q3/</guid>
      <dc:creator>Emil Hvitfeldt</dc:creator><description><![CDATA[<!--
TODO:
* [x] Look over / edit the post's title in the yaml
* [x] Edit (or delete) the description; note this appears in the Twitter card
* [x] Pick category and tags (see existing with [`hugodown::tidy_show_meta()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html))
* [x] Find photo & update yaml metadata
* [x] Create `thumbnail-sq.jpg`; height and width should be equal
* [x] Create `thumbnail-wd.jpg`; width should be >5x height
* [x] [`hugodown::use_tidy_thumbnails()`](https://rdrr.io/pkg/hugodown/man/use_tidy_post.html)
* [x] Add intro sentence, e.g. the standard tagline for the package
* [x] [`usethis::use_tidy_thanks()`](https://usethis.r-lib.org/reference/use_tidy_thanks.html)
-->
<p>The tidymodels framework is a collection of R packages for modeling and machine learning using tidyverse principles.</p>
<p>Since the beginning of 2021, we have been publishing quarterly updates here on the tidyverse blog summarizing what&rsquo;s new in the tidymodels ecosystem. The purpose of these regular posts is to share useful new features and any updates you may have missed. You can check out the tidymodels tag to find all tidymodels blog posts here, including our roundup posts as well as those that are more focused.</p>
<p>Since our last update we have had some larger releases that you can read about in these posts.</p>
<ul>
<li><a href="https://tidyverse.org/blog/2025/11/tune-2/" target="_blank" rel="noopener">tune 2.0.0</a>
</li>
<li><a href="https://tidyverse.org/blog/2025/04/recipes-1-3-0/" target="_blank" rel="noopener">recipes 1.3.0</a>
</li>
<li><a href="https://tidyverse.org/blog/2025/04/rsample-1-3-0/" target="_blank" rel="noopener">rsample 1.3.0</a>
</li>
<li><a href="https://tidyverse.org/blog/2025/03/tidymodels-sparsity/" target="_blank" rel="noopener">improved sparsity support in tidymodels</a>
</li>
</ul>
<p>The post will update, you on which packages have changed and the improvements you should know about that haven&rsquo;t been covered in the above posts.</p>
<p>Here&rsquo;s a list of the packages and their News sections:</p>
<ul>
<li><a href="https://dials.tidymodels.org/news/index.html" target="_blank" rel="noopener">dials</a>
</li>
<li><a href="https://parsnip.tidymodels.org/news/index.html" target="_blank" rel="noopener">parsnip</a>
</li>
<li><a href="https://rsample.tidymodels.org/news/index.html" target="_blank" rel="noopener">rsample</a>
</li>
<li><a href="https://recipes.tidymodels.org/news/index.html" target="_blank" rel="noopener">recipes</a>
</li>
<li><a href="https://probably.tidymodels.org/news/index.html" target="_blank" rel="noopener">probably</a>
</li>
<li><a href="https://brulee.tidymodels.org/news/index.html" target="_blank" rel="noopener">brulee</a>
</li>
</ul>
<p>Let&rsquo;s look at a few specific updates.</p>
<h2 id="quiet-linear-svm-models">Quiet linear svm models
</h2>
<p>When you used to fit a linear SVM model, you would get a message that you were not able to avoid.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">parsnip</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">modeldata</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">res</span> <span class="o">&lt;-</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">svm_linear</span><span class="p">(</span><span class="n">mode</span> <span class="o">=</span> <span class="s">&#34;classification&#34;</span><span class="p">,</span> <span class="n">engine</span> <span class="o">=</span> <span class="s">&#34;kernlab&#34;</span><span class="p">)</span> <span class="o">|&gt;</span> 
</span></span><span class="line"><span class="cl">  <span class="nf">fit</span><span class="p">(</span><span class="n">Class</span> <span class="o">~</span> <span class="n">.,</span> <span class="n">data</span> <span class="o">=</span> <span class="n">two_class_dat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;  Setting default kernel parameters</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This message by itself was not that useful and was unable to turn off in a reasonable way. We have silenced this message to hopefully alleviate some of the noise that came from using this method.</p>
<div class="highlight">
<pre class='chroma'><code class='language-r' data-lang='r'><span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://github.com/tidymodels/parsnip'>parsnip</a></span><span class='o'>)</span></span>
<span><span class='kr'><a href='https://rdrr.io/r/base/library.html'>library</a></span><span class='o'>(</span><span class='nv'><a href='https://modeldata.tidymodels.org'>modeldata</a></span><span class='o'>)</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; Attaching package: 'modeldata'</span></span>
<span></span><span><span class='c'>#&gt; The following object is masked from 'package:datasets':</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt;     penguins</span></span>
<span></span><span></span>
<span><span class='nv'>res</span> <span class='o'>&lt;-</span> </span>
<span>  <span class='nf'><a href='https://parsnip.tidymodels.org/reference/svm_linear.html'>svm_linear</a></span><span class='o'>(</span>mode <span class='o'>=</span> <span class='s'>"classification"</span>, engine <span class='o'>=</span> <span class='s'>"kernlab"</span><span class='o'>)</span> <span class='o'>|&gt;</span> </span>
<span>  <span class='nf'><a href='https://generics.r-lib.org/reference/fit.html'>fit</a></span><span class='o'>(</span><span class='nv'>Class</span> <span class='o'>~</span> <span class='nv'>.</span>, data <span class='o'>=</span> <span class='nv'>two_class_dat</span><span class='o'>)</span></span>
<span><span class='nv'>res</span></span>
<span><span class='c'>#&gt; parsnip model object</span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; Support Vector Machine object of class "ksvm" </span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; SV type: C-svc  (classification) </span></span>
<span><span class='c'>#&gt;  parameter : cost C = 1 </span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; Linear (vanilla) kernel function. </span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; Number of Support Vectors : 361 </span></span>
<span><span class='c'>#&gt; </span></span>
<span><span class='c'>#&gt; Objective Function Value : -357.1487 </span></span>
<span><span class='c'>#&gt; Training error : 0.178255 </span></span>
<span><span class='c'>#&gt; Probability model included.</span></span>
<span></span></code></pre>
</div>
<h2 id="fewer-numeric-overflow-issues-in-brulee">Fewer numeric overflow issues in brulee
</h2>
<p>The brulee package has been improved to try to help avoid numeric overflow in the loss functions. The following things have been done to help deal with this type of issue.</p>
<ul>
<li>
<p>Starting values were transitioned to using Gaussian distribution (instead of uniform) with a smaller standard deviation.</p>
</li>
<li>
<p>The results always contain the initial results to use as a fallback if there is overflow during the first epoch.</p>
</li>
<li>
<p><code>brulee_mlp()</code> has two additional parameters, <code>grad_value_clip</code> and <code>grad_value_clip</code>, that prevent issues.</p>
</li>
<li>
<p>The warning was changed to &ldquo;Early stopping occurred at epoch {X} due to numerical overflow of the loss function.&rdquo;</p>
</li>
</ul>
<h2 id="additional-torch-optimizers-in-brulee">Additional torch optimizers in brulee
</h2>
<p>Several additional optimizers have been added: <code>&quot;ADAMw&quot;</code>, <code>&quot;Adadelta&quot;</code>, <code>&quot;Adagrad&quot;</code>, and <code>&quot;RMSprop&quot;</code>. Previously, the options were <code>&quot;SGD&quot;</code> and <code>LBFGS&quot;</code>. ## Acknowledgements</p>
<p>We want to sincerely thank everyone who contributed to these packages since their previous versions:</p>
<ul>
<li>dials: <a href="https://github.com/brendad8" target="_blank" rel="noopener">@brendad8</a>
, <a href="https://github.com/hfrick" target="_blank" rel="noopener">@hfrick</a>
, <a href="https://github.com/topepo" target="_blank" rel="noopener">@topepo</a>
, and <a href="https://github.com/Wander03" target="_blank" rel="noopener">@Wander03</a>
.</li>
<li>parsnip: <a href="https://github.com/chillerb" target="_blank" rel="noopener">@chillerb</a>
, <a href="https://github.com/EmilHvitfeldt" target="_blank" rel="noopener">@EmilHvitfeldt</a>
, <a href="https://github.com/jmgirard" target="_blank" rel="noopener">@jmgirard</a>
, <a href="https://github.com/topepo" target="_blank" rel="noopener">@topepo</a>
, and <a href="https://github.com/ZWael" target="_blank" rel="noopener">@ZWael</a>
.</li>
<li>rsample: <a href="https://github.com/abichat" target="_blank" rel="noopener">@abichat</a>
, <a href="https://github.com/hfrick" target="_blank" rel="noopener">@hfrick</a>
, <a href="https://github.com/mkiang" target="_blank" rel="noopener">@mkiang</a>
, and <a href="https://github.com/vincentarelbundock" target="_blank" rel="noopener">@vincentarelbundock</a>
.</li>
<li>recipes: <a href="https://github.com/EmilHvitfeldt" target="_blank" rel="noopener">@EmilHvitfeldt</a>
, <a href="https://github.com/SimonDedman" target="_blank" rel="noopener">@SimonDedman</a>
, and <a href="https://github.com/topepo" target="_blank" rel="noopener">@topepo</a>
.</li>
<li>probably: <a href="https://github.com/abichat" target="_blank" rel="noopener">@abichat</a>
, <a href="https://github.com/ayueme" target="_blank" rel="noopener">@ayueme</a>
, <a href="https://github.com/dchiu911" target="_blank" rel="noopener">@dchiu911</a>
, <a href="https://github.com/EmilHvitfeldt" target="_blank" rel="noopener">@EmilHvitfeldt</a>
, <a href="https://github.com/frankiethull" target="_blank" rel="noopener">@frankiethull</a>
, <a href="https://github.com/gaborcsardi" target="_blank" rel="noopener">@gaborcsardi</a>
, <a href="https://github.com/hfrick" target="_blank" rel="noopener">@hfrick</a>
, <a href="https://github.com/Jeffrothschild" target="_blank" rel="noopener">@Jeffrothschild</a>
, <a href="https://github.com/jgaeb" target="_blank" rel="noopener">@jgaeb</a>
, <a href="https://github.com/jrwinget" target="_blank" rel="noopener">@jrwinget</a>
, <a href="https://github.com/mark-burdon" target="_blank" rel="noopener">@mark-burdon</a>
, <a href="https://github.com/martinhulin" target="_blank" rel="noopener">@martinhulin</a>
, <a href="https://github.com/simonpcouch" target="_blank" rel="noopener">@simonpcouch</a>
, <a href="https://github.com/teunbrand" target="_blank" rel="noopener">@teunbrand</a>
, <a href="https://github.com/topepo" target="_blank" rel="noopener">@topepo</a>
, <a href="https://github.com/wjakethompson" target="_blank" rel="noopener">@wjakethompson</a>
, and <a href="https://github.com/yellowbridge" target="_blank" rel="noopener">@yellowbridge</a>
.</li>
<li>brulee: <a href="https://github.com/genec1" target="_blank" rel="noopener">@genec1</a>
, <a href="https://github.com/talegari" target="_blank" rel="noopener">@talegari</a>
, and <a href="https://github.com/topepo" target="_blank" rel="noopener">@topepo</a>
.</li>
</ul>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/tidyverse/2025/tidymodels-2025-q3/thumbnail-wd.jpg" length="838271" type="image/jpeg" />
    </item>
  </channel>
</rss>
