<?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>Python on Posit Open Source</title>
    <link>https://opensource.posit.co/languages/python/</link>
    <description>Recent content in Python on Posit Open Source</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 07 Apr 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://opensource.posit.co/languages/python/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>April Release Highlights</title>
      <link>https://opensource.posit.co/blog/2026-04-07_april-newsletter/</link>
      <pubDate>Tue, 07 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-04-07_april-newsletter/</guid>
      <dc:creator>Cindy Tong</dc:creator><description><![CDATA[<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>






<a href="https://posit.co/positron-updates-signup/" target="_blank" rel="noopener">Subscribe</a>
 to get this newsletter directly in your email inbox.</p>
</div>
</div>
<p>Welcome to the first edition of our Positron newsletter! Here, we will share highlights from our latest release, tips on how to be more productive with Positron, and useful resources.</p>
<p>We just returned from an in-person onsite in beautiful Monterey, California. During the trip, we got a chance to meet (some of us for the first time), touch grass and sand, and brainstorm ways we can improve to build better products for you.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/monterey.png" alt="A view from Point Lobos State Natural Reserve" />
<figcaption aria-hidden="true">A view from Point Lobos State Natural Reserve</figcaption>
</figure>
<p>Let&rsquo;s get into the updates.</p>
<h2 id="key-product-updates">Key Product Updates
</h2>
<p>The April 2026 release of Positron brings significant improvements across:</p>
<ul>
<li>


  
  
  





<a href="#positron-server-for-academic-use-via-jupyterhub">Positron Server for Academic Use</a>
 via JupyterHub</li>
<li>


  
  
  





<a href="#ai-next-steps-in-the-native-jupyter-notebook-editor">AI enhancements</a>
: Next Steps in Jupyter Notebooks, Agent Skills, and Azure AI Foundry Support</li>
<li>


  
  
  





<a href="#telemetry-update-anonymous-session-identifiers">Telemetry updates</a>
</li>
<li>


  
  
  





<a href="#rstudio-addins-support">R improvements</a>
: Addins, Debugging, and more</li>
<li>


  
  
  





<a href="#data-explorer-faster-with-multiple-dataframes">Data Explorer Performance Improvement</a>
</li>
<li>


  
  
  





<a href="#windows-arm-is-generally-available">Windows ARM in GA</a>
</li>
<li>


  
  
  





<a href="#whats-coming-next">What&rsquo;s Coming Next</a>
: Inline Outputs, Packages Pane, and Posit Assistant</li>
</ul>
<p>Here&rsquo;s a look at the key features that shipped with the April 2026 release.</p>
<h3 id="positron-server-for-academic-use-via-jupyterhub">Positron Server for Academic Use via JupyterHub
</h3>
<p><strong>What we built:</strong> Academic institutions can now offer Positron Server to their students at no cost through JupyterHub (





  


  
  

<a href="https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub">blog post</a>
). If your institution already runs JupyterHub, you can add Positron as a launcher option alongside JupyterLab, with no additional infrastructure required. Students simply log in and select Positron from the launcher, getting the full Positron experience including rich Python and R support, the extension marketplace, and (optionally) Positron Assistant.</p>
<p><strong>Why this matters:</strong> This removes the barrier for students and educators who want to use Positron in a classroom setting. No local installs, no configuration headaches &mdash; just a familiar JupyterHub login with Positron ready to go.</p>
<p><strong>Get started:</strong> 






<a href="https://github.com/posit-dev/positron/blob/main/LICENSE.txt" target="_blank" rel="noopener">Review the eligibility criteria</a>
 and send an email to 






<a href="mailto:academic-licenses@posit.co">academic-licenses@posit.co</a>
 to request a free teaching license.</p>
<h3 id="ai-next-steps-in-the-native-jupyter-notebook-editor">AI Next Steps in the Native Jupyter Notebook Editor
</h3>























  
  
    <div class="w-full aspect-video">
      <video
        src="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/notebook-next-step-suggestions.mov"
        class="w-full h-full object-contain"
        
        controls></video>
    </div>
  




<p><strong>What we built:</strong> AI Next Steps uses the Positron Assistant to analyze your current cell output and suggest a logical next step in a &ldquo;ghost cell&rdquo; at the bottom of your notebook. If you just loaded a CSV, it might suggest data cleaning steps or a visualization, without you needing to open a chat pane or write a prompt. Suggestions stay aligned with the notebook&rsquo;s live kernel state, updating as your code and outputs change.</p>
<p><strong>Why this matters</strong>: The design came out of interviews with data scientists who kept telling us the same thing: switching to a chat pane mid-analysis breaks their concentration. AI Next Steps sits at the bottom of your notebook and updates as your outputs change. You just run a cell, and if there&rsquo;s a logical next step, it surfaces, with no prompt required.</p>
<p><strong>Get started:</strong> Enable the feature by setting 






<a href="positron://settings/positron.assistant.notebook.ghostCellSuggestions.enabled"><code>positron.assistant.notebook.ghostCellSuggestions.enabled</code></a>
 to <code>true</code> in your settings. When you run a cell, look for the ghost cell suggestion at the bottom of the notebook, accept, reject, or hide it.</p>
<h3 id="agent-skills-in-positron-assistant">Agent Skills in Positron Assistant
</h3>
<p><strong>What we built:</strong> Agent skills &mdash; reusable, structured capabilities that extend what agents can do in agent.md files &mdash; are now integrated into Positron (






<a href="https://github.com/posit-dev/positron/issues/11753" target="_blank" rel="noopener">#11753</a>
). Skills let agents execute multi-step workflows like &ldquo;profile this dataset and suggest cleaning steps&rdquo; or &ldquo;run this test suite and summarize failures,&rdquo; so you define a task once and reuse it across sessions and projects.</p>
<p><strong>Why this matters:</strong> Skills make agents composable building blocks rather than one-off chat interactions. Instead of re-explaining a complex workflow every time, you codify it as a skill that any team member can use.</p>
<p><strong>Get started:</strong> Open the chat gear icon and select <strong>Skills</strong>, or run <em>Chat: Configure Skills</em> from the Command Palette.</p>
<h3 id="positron-assistant-now-supports-microsoft-foundry-as-a-provider">Positron Assistant Now Supports Microsoft Foundry as a Provider
</h3>
<p><strong>What we built:</strong> Positron Assistant now supports Microsoft Foundry as a model provider (






<a href="https://github.com/posit-dev/positron/issues/8583" target="_blank" rel="noopener">#8583</a>
) with API key-based access via a custom base URL.</p>
<p><strong>Why this matters:</strong> If your team runs on Azure and uses LLMs through Foundry, you can now use Positron Assistant with them.</p>
<p><strong>Get Started:</strong> In Positron Assistant&rsquo;s provider settings, set 






<a href="positron://settings/positron.assistant.provider.msFoundry.enable"><code>positron.assistant.provider.msFoundry.enable</code></a>
 to <code>true</code> to select Microsoft Foundry as a provider. You can authenticate with an API key and your Foundry endpoint URL.</p>
<img src="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/microsoft-foundry.png" data-fig-alt="Configuring Microsoft Foundry in Positron Assistant" />
<h3 id="telemetry-update-anonymous-session-identifiers">Telemetry Update: Anonymous Session Identifiers
</h3>
<p><strong>What we changed:</strong> Positron now generates an anonymous, random session identifier to help us understand usage patterns like session frequency and retention across releases. This identifier contains no personal information, account data, or workspace content; it&rsquo;s a cryptographically random UUID that cannot be linked to any other identifiers, including the identifier that VS Code uses for telemetry.</p>
<p><strong>Why we&rsquo;re doing this:</strong> As a free, source available project, we don&rsquo;t have traditional product analytics. Understanding whether people come back, how often they use Positron, and whether releases improve or regress the experience helps us prioritize the right work to build a better experience for you.</p>
<p>You can opt out by updating your settings outlined 






<a href="https://positron.posit.co/privacy.html" target="_blank" rel="noopener">here</a>
, or you can reset the anonymous identifier with the command <em>Preferences: Reset Anonymous Telemetry ID</em>. If you&rsquo;ve opted out of product updates, no session identifier is generated or sent.</p>
<h3 id="rstudio-addins-support">RStudio Addins Support
</h3>
<p><strong>What we built:</strong> Positron now supports running RStudio addins from R packages. If a package registers an addin (like styler, reprex, clipr, or shinyuieditor), you can run it directly from Positron (






<a href="https://github.com/posit-dev/positron/issues/1313" target="_blank" rel="noopener">#1313</a>
).</p>
<p><strong>Why this matters:</strong> This was one of our most upvoted issues this release (25 👍). Many R users rely on addins as part of their daily workflow for code formatting, generating reproducible examples, or launching Shiny tools.</p>
<p><strong>Get started:</strong> Open the Command Palette (<code>Ctrl-Shift-P</code> (windows), <code>Ctrl-Shift-P</code> (linux), <code>Command-Shift-P</code> (mac)) and search for <em>Run RStudio Addin</em>. You&rsquo;ll see a quick pick with all available addins from your installed packages.</p>
<img src="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/addins-support.png" data-fig-alt="RStudio Addins running in Positron" />
<h3 id="r-debugger--workflow-improvements">R Debugger &amp; Workflow Improvements
</h3>
<p><strong>What we built:</strong> The R debugger received a suite of improvements this release. In addition to conditional breakpoints, hit count breakpoints, and log breakpoints (






<a href="https://github.com/posit-dev/positron/issues/12360" target="_blank" rel="noopener">#12360</a>
), the debugger now supports error and warning breakpoints (






<a href="https://github.com/posit-dev/positron/issues/11797" target="_blank" rel="noopener">#11797</a>
), the ability to pause R at any time (






<a href="https://github.com/posit-dev/positron/issues/11799" target="_blank" rel="noopener">#11799</a>
), Watch Pane support (






<a href="https://github.com/posit-dev/positron/issues/1765" target="_blank" rel="noopener">#1765</a>
), and synchronization between the Console and Variables pane with the selected call stack frame (






<a href="https://github.com/posit-dev/positron/issues/3078" target="_blank" rel="noopener">#3078</a>
 and 






<a href="https://github.com/posit-dev/positron/issues/12131" target="_blank" rel="noopener">#12131</a>
).</p>
<p><strong>Why this matters:</strong> Advanced debugging in R has traditionally meant scattering <code>if (...) browser()</code> calls through your code or setting <code>options(error = recover)</code> by hand. These new features put Positron&rsquo;s R debugger on par with what you&rsquo;d expect from any modern language:</p>
<ul>
<li><strong>Conditional, hit count, and log breakpoints</strong> let you control exactly when breakpoints fire and print diagnostic info, all without touching your source code.</li>
<li><strong>Error and warning breakpoints</strong> drop you into the debugger the moment an error or warning is emitted, so you can inspect the state that caused it.</li>
<li><strong>Pause R at any time.</strong> If R is stuck in a long computation or an infinite loop, you can drop into the debugger mid-execution, look around, and resume by clicking <strong>Continue</strong>.</li>
<li><strong>Watch Pane</strong> lets you track expressions across debug steps. Prefix an expression with <code>/print</code> to see R&rsquo;s printed output (hover to get full output) instead of a structured variable.</li>
<li><strong>Synchronization with the call stack.</strong> Click any frame in the <strong>Call Stack</strong> view and the Console, completions, and Variables pane all switch to that frame&rsquo;s environment. The Console synchronization is like <code>recover()</code>, but built into the IDE.</li>
</ul>
<p><strong>Get started:</strong> Set a breakpoint in any R file, then right-click it and choose <strong>Edit Breakpoint</strong>. Select &ldquo;Expression&rdquo; to add a condition (e.g., <code>i &gt; 100</code>), &ldquo;Hit Count&rdquo; to break after N hits, or &ldquo;Log Message&rdquo; to print a message without pausing. For error and warning breakpoints, open the <strong>Breakpoints</strong> pane and enable them there. To pause R while code is running, use the command <em>Debug: Pause</em> or check the <strong>Interrupt</strong> breakpoint option in the <strong>Breakpoints</strong> pane. While debugging, add expressions in the <strong>Watch</strong> section of the debug sidebar and click on frames in the <strong>Call Stack</strong> to navigate environments.</p>























  
  
    <div class="w-full aspect-video">
      <video
        src="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/conditional-breakpoints.mov"
        class="w-full h-full object-contain"
        
        controls></video>
    </div>
  




<h3 id="data-explorer-faster-with-multiple-dataframes">Data Explorer: Faster with Multiple DataFrames
</h3>
<p><strong>What we built:</strong> We fixed two long-standing performance issues in the Data Explorer. Background Data Explorer tabs no longer trigger backend recomputation, and the summary panel no longer recalculates summary statistics for large DataFrames on every cell execution (






<a href="https://github.com/posit-dev/positron/issues/4279" target="_blank" rel="noopener">#4279</a>
 and 






<a href="https://github.com/posit-dev/positron/issues/2795" target="_blank" rel="noopener">#2795</a>
).</p>
<p><strong>Why this matters:</strong> If you work with multiple DataFrames open, you may have noticed lag as Positron recomputed statistics for tabs you weren&rsquo;t even looking at. That&rsquo;s gone now.</p>
<p><strong>Get started:</strong> Nothing to configure. When you open multiple DataFrames in the Data Explorer and switch between them, you should notice snappier performance, especially with large datasets.</p>
<h3 id="windows-arm-is-generally-available">Windows ARM Is Generally Available
</h3>
<p><strong>What we built:</strong> We started creating experimental builds for Windows ARM several months ago, and our early users have had good experiences with them. This release, we promoted the Windows ARM builds from experimental to stable and they are now available through all standard installation channels (






<a href="https://github.com/posit-dev/positron/issues/12207" target="_blank" rel="noopener">#12207</a>
).</p>
<p><strong>Why this matters:</strong> ARM-based devices are increasingly common for Windows users, whether you&rsquo;re a student or a professional. GA support means these users get the same Positron experience, including Quarto with R and Python support, without needing workarounds or experimental builds. Do be aware that the Windows ARM build bundles the non-ARM version of Quarto, which runs under emulation.</p>
<p><strong>Get started:</strong> Install Positron on your ARM-based Windows device through 






<a href="https://positron.posit.co/download.html" target="_blank" rel="noopener">standard installation channels</a>
.</p>
<p>View all issues in the 






<a href="https://github.com/posit-dev/positron/milestone/36" target="_blank" rel="noopener">2026.04.0 Release milestone</a>
.</p>
<h2 id="whats-coming-next">What&rsquo;s Coming Next
</h2>
<p>We are currently building the following features and we&rsquo;d love your feedback. Please share on 






<a href="https://github.com/posit-dev/positron/discussions" target="_blank" rel="noopener">GitHub</a>
. These early alpha features with some rough edges are available for testing by enabling their respective settings.</p>
<h3 id="inline-outputs-for-quarto-and-r-markdown-files">Inline Outputs for Quarto and R Markdown Files
</h3>
<p>This was the second most upvoted issue we have ever, ever had! We just completed an initial run to allow displaying inline outputs within Quarto and R Markdown files (






<a href="https://github.com/posit-dev/positron/issues/5640" target="_blank" rel="noopener">#5640</a>
), and it is available for early testing. Note that this experimental version, while it does get the basics into Positron, does not have support for many popular RStudio features. You can opt in to the experimental feature using the 






<a href="positron://settings/positron.quarto.inlineOutput.enabled"><code>positron.quarto.inlineOutput.enabled</code></a>
 setting.</p>
<img src="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/inline-output.png" data-fig-alt="Inline outputs rendered in a Quarto file" />
<h3 id="packages-pane-for-managing-environments">Packages Pane for Managing Environments
</h3>
<p>We are currently building out a new Packages pane that will allow you to install, update, and uninstall packages without leaving your workspace or needing to use the terminal (






<a href="https://github.com/posit-dev/positron/issues/11214" target="_blank" rel="noopener">#11214</a>
). We&rsquo;d love to hear your feedback on this 






<a href="https://github.com/posit-dev/positron/discussions/12863" target="_blank" rel="noopener">discussion thread</a>
.</p>
<h2 id="events-and-resources">Events and Resources
</h2>
<h3 id="explore-positrons-video-walkthroughs-on-youtube">Explore Positron&rsquo;s Video Walkthroughs on YouTube
</h3>
<p>We hosted a walkthrough of exploring GitHub data in a Jupyter Notebook and converting this into an interactive Shiny app with AI. 






<a href="https://www.youtube.com/watch?v=qrVkG89ndi8" target="_blank" rel="noopener">Catch up on the recording</a>
 or 






<a href="https://www.youtube.com/@PositPBC" target="_blank" rel="noopener">explore more Positron videos</a>
.</p>
<h3 id="registration-for-positconf2026-is-now-open">Registration for posit::conf(2026) Is Now Open!
</h3>
<p>Registration is officially open for posit::conf(2026)! Join the global data community in Houston or tune in online from September 14&ndash;16. 






<a href="https://posit.co/conference/" target="_blank" rel="noopener">Register today!</a>
</p>
<h3 id="how-we-chose-a-python-type-checker">How We Chose a Python Type Checker
</h3>
<p>Ever wondered about the decision making process behind how we chose which Python type checker to bundle in Positron? Check out Austin Dickey&rsquo;s 





  


  
  

<a href="https://opensource.posit.co/blog/2026-03-31_python-type-checkers">blog post</a>
 walking through his research and decision making process.</p>
<h2 id="community-affirmations">Community Affirmations
</h2>
<p>Thank you all for your support, ideas and engagement. We&rsquo;re building Positron in the open because the best ideas come from the people using it. If there&rsquo;s a feature you&rsquo;d love to see, 






<a href="https://github.com/posit-dev/positron/issues" target="_blank" rel="noopener">open an issue</a>
 or upvote an existing one, it genuinely shapes what we work on next.</p>
<p>Have a great April!</p>
<p>Positron Team</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-04-07_april-newsletter/images/addins-support.png" length="374293" type="image/png" />
    </item>
    <item>
      <title>Positron Server available for academic use via JupyterHub</title>
      <link>https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub/</link>
      <pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub/</guid>
      <dc:creator>Isabel Zimmerman</dc:creator><description><![CDATA[<p>Academic institutions can now offer Positron directly within their existing JupyterHub environments, giving students a robust data science IDE without needing a local install or new infrastructure.
With a free teaching license, institutions can provide Positron Server to currently enrolled students for use in coursework.
This makes it easy to deliver a consistent, fully featured data science environment to students without requiring local installation or setup.</p>
<p>Students can launch Positron the same way they would open JupyterLab or a notebook.
Just select it from the JupyterHub launcher and start working.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub/images/jupyter-positron.gif"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>Once launched, Positron provides the full IDE experience, including:</p>
<ul>
<li>Rich Python and R support</li>
<li>Access to the OpenVSX extension marketplace</li>
<li>Built in data viewer and variables explorer</li>
<li>Integrated help pane, debugger, version control and 






<a href="https://positron.posit.co/features.html" target="_blank" rel="noopener">other features</a>
 to help students level up when they&rsquo;re ready</li>
</ul>
<h2 id="how-it-works">How it works
</h2>
<p>Positron Server is designed to integrate directly with existing JupyterHub deployments.
It&rsquo;s compatible with JupyterHub environments running JupyterLab 4 and Python 3.9+.</p>
<p>It&rsquo;s installed via the 






<a href="https://github.com/posit-dev/jupyter-positron-server" target="_blank" rel="noopener"><code>jupyter-positron-server</code> Python package</a>
, built on Jupyter Server Proxy.
If you&rsquo;ve configured similar services before, setup will feel familiar.
This is not a standalone desktop install.
Rather, it lets you bring Positron into an existing JupyterHub setup.</p>
<h2 id="who-can-use-it">Who can use it?
</h2>
<p>This offering is available to academic institutions using Positron for teaching.
Under a free license, institutions can provide access to enrolled students, course participants, or staff involved in the delivery or receipt of educational programming.</p>
<p>Full eligibility details are available in the 






<a href="https://github.com/posit-dev/positron/blob/main/LICENSE.txt" target="_blank" rel="noopener">Positron Education License Rider</a>
.</p>
<h2 id="getting-started">Getting started
</h2>
<p>Hosting Positron for teaching purposes requires a free license key.
To get set up:</p>
<ol>
<li>Review the eligibility criteria in the 






<a href="https://github.com/posit-dev/positron/blob/main/LICENSE.txt" target="_blank" rel="noopener">Positron Education License Rider</a>
.</li>
<li>Email 






<a href="mailto:academic-licenses@posit.co">academic-licenses@posit.co</a>
 to request a teaching license.</li>
<li>Once your license is confirmed, follow the 






<a href="https://posit-dev.github.io/jupyter-positron-server/" target="_blank" rel="noopener"><code>jupyter-positron-server</code> documentation</a>
 to complete setup in your JupyterHub environment.</li>
</ol>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Get in touch</span>
</div>
<div class="callout-body">
<p>Have questions or want to learn more?</p>
<p>Reach out to 






<a href="mailto:academic-licenses@posit.co">academic-licenses@posit.co</a>
 and let us know you&rsquo;re interested in Positron.
We&rsquo;ll help you navigate next steps!</p>
</div>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-04-06_positron-server-jupyterhub/images/social.png" length="129271" type="image/png" />
    </item>
    <item>
      <title>What&#39;s next: Quarto 2</title>
      <link>https://opensource.posit.co/blog/2026-04-06_whats-next-quarto-2/</link>
      <pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-04-06_whats-next-quarto-2/</guid>
      <dc:creator>Carlos Scheidegger</dc:creator><description><![CDATA[<p>We&rsquo;re excited to share an early look at Quarto 2. You might be aware that we 





  


  
  
    
  

<a href="https://opensource.posit.co/blog/2026-03-24_1.9-release/">recently released Quarto 1.9</a>
, with support for long-standing requests such as PDF accessibility. Quarto is an excellent choice for authors of scientific and technical documents, and the amount and quality of the work you create with it is genuinely humbling for us. Before anything else, we want to thank you for using Quarto; you&rsquo;re all quite literally the reason we build it.</p>
<p><strong>Quarto 2 is a full rewrite of the Quarto CLI, written from the ground up in Rust</strong> to better support your existing use cases, and enable a number of new, exciting use cases. Most importantly, Quarto 2 will include a built-in collaborative editor, and we plan on adding support for collaborative writing in Posit&rsquo;s commercial products such as Posit Cloud, Connect, and Workbench. With that said, the design of those integrations is still taking shape.</p>
<p>It is also very early in the project. If you interact with the Quarto project solely as a user of the tool, nothing in your workflow will change, and you should proceed as if you didn&rsquo;t know about our plans for Quarto 2. <strong>We don&rsquo;t expect to have a public release of Quarto 2 for at least 6 months. In addition, we will continue to develop and maintain parallel versions until Quarto 2 is a suitable replacement for users of Quarto 1</strong>.</p>
<p>Just like Quarto 1, Quarto 2 is open source and MIT licensed. The GitHub repository for Quarto 2 is currently 






<a href="https://github.com/quarto-dev/q2" target="_blank" rel="noopener"><code>quarto-dev/q2</code></a>
.</p>
<h2 id="why-quarto-2">Why Quarto 2?
</h2>
<p>There are some fundamental pain points in Quarto 1 that can&rsquo;t be solved incrementally. The goal of Quarto 2 is not to change how you currently work with Quarto; instead, we&rsquo;ve arrived at a point where incremental improvements do not provide the value you deserve given our team size and constraints. These are some of the things we want to do in Quarto 2:</p>
<ul>
<li>
<p><strong>A new Markdown parser enables tighter integration with editors for the entire rendering pipeline</strong>.
We know that good error messages, autocompletion, and YAML validation are some of your favorite features in Quarto 1. Quarto has about 1,000 different YAML configuration options, and we know how important it is to be able to provide good error messages. We want to extend this same idea to <em>everything</em> in your Quarto project: Markdown syntax errors, Lua filter errors, broken links, etc. Whenever possible, these should be flagged in your editor of choice.</p>
</li>
<li>
<p><strong>A fundamental solution for long-standing performance problems</strong>.
Quarto 1 is built by integrating a number of tools that work very well in isolation, but aren&rsquo;t designed to be performant when used together. A full rewrite of the Quarto core functionality in a single programming language will enable us to provide much better performance than before.</p>
</li>
<li>
<p><strong>A collaborative editor</strong>.
Quarto 2 will ship with a collaborative editor designed to work directly on the web as well as on the command-line. Keeping in the tradition and ethos of the Quarto project, this will include a robust open-source foundation based on 






<a href="https://automerge.org" target="_blank" rel="noopener">automerge</a>
, as well as a commercial solution for hosted project management. This follows the relationship between Quarto 1 and its integration with other Posit commercial offerings.</p>
</li>
<li>
<p><strong>A visual editor that works well alongside a source editor</strong>.
The visual editor we ship in RStudio, VS Code, and Positron works well if everyone working on the document is using the visual editor. On the other hand, if you choose the visual editor, but your colleague chooses the source editor, then you&rsquo;ll find that the experience is full of sharp edges. Quarto 2 is built from the ground up to support <em>bidirectional</em> editing workflows. A small change in your document using the visual editor shouldn&rsquo;t cause a large change in the <code>.qmd</code> file that is disruptive for your colleagues using a source editor.</p>
</li>
<li>
<p><strong>Support for Quarto 1 projects</strong>.
We aim for Quarto 2 to be backwards compatible with Quarto 1. Concretely, we&rsquo;re aiming to incorporate our Quarto 1 test suite directly into Quarto 2&rsquo;s project, including support for Pandoc and its output formats that our community depends on. Your existing extensions and projects should just work in Quarto 2. Early on, there will be gaps, and Quarto 2 will initially be a better fit for new projects.</p>
</li>
</ul>
<h2 id="what-happens-to-quarto-1-development">What happens to Quarto 1 development?
</h2>
<p>It&rsquo;s not going anywhere, and will be in active development for at least the next year. We&rsquo;ll still provide bugfixes, and accept pull requests.</p>
<h2 id="current-status">Current status
</h2>
<p>The development is happening in a 






<a href="https://github.com/quarto-dev/q2" target="_blank" rel="noopener">separate GitHub repository</a>
. Feel free to look around! However, this code base isn&rsquo;t ready for public consumption, and is very much in flux: that means we&rsquo;re not going to spend a lot of time answering architectural questions about it until things have settled, and all discussion of Quarto should remain in our current discussion forum and issue tracker.</p>
<p>There are big, interesting changes in the Quarto 2 architecture, and they deserve a longer exposition. We are working on those documents right now, and will share them with you in the next few weeks. Stay tuned!</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-04-06_whats-next-quarto-2/thumbnail.png" length="98981" type="image/png" />
    </item>
    <item>
      <title>Shiny for Python 1.6 brings toolbars and OpenTelemetry</title>
      <link>https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/</link>
      <pubDate>Thu, 02 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/</guid>
      <dc:creator>Liz Nelson</dc:creator>
      <dc:creator>Barret Schloerke</dc:creator><description><![CDATA[<style>
  .panel-tabset .tab-content, .nav {
    border: none;
  }
  .panel-tabset.nav-centered .nav {
    justify-content: center;
  }
</style>
<p>We&rsquo;re pleased to announce that Shiny for Python <code>v1.6</code> is now 






<a href="https://pypi.org/project/shiny/" target="_blank" rel="noopener">available on PyPI</a>
!</p>
<p>Install it now with <code>pip install -U shiny</code>.</p>
<p>This release has two big additions: 


  
  
  





<a href="#toolbars">toolbar components</a>
 for building compact, modern UIs, and 


  
  
  





<a href="#opentelemetry">OpenTelemetry support</a>
 for understanding how your apps behave in production. A full list of changes is available in the 






<a href="https://github.com/posit-dev/py-shiny/blob/main/CHANGELOG.md" target="_blank" rel="noopener">CHANGELOG</a>
.</p>
<h2 id="toolbars">Toolbars
</h2>
<p>Toolbars are a new set of compact components designed to fit controls into tight spaces &mdash; card headers and footers, input labels, and text areas. They&rsquo;re perfect for dashboards that are running out of room, or for AI chat interfaces where you want to add controls without cluttering the layout.</p>
<p>The core components are:</p>
<table>
  <thead>
      <tr>
          <th>Component</th>
          <th>Description</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ui.toolbar()</code></td>
          <td>Container for toolbar inputs</td>
      </tr>
      <tr>
          <td><code>ui.toolbar_input_button()</code></td>
          <td>A small action button</td>
      </tr>
      <tr>
          <td><code>ui.toolbar_input_select()</code></td>
          <td>A compact dropdown select</td>
      </tr>
      <tr>
          <td><code>ui.toolbar_divider()</code></td>
          <td>A visual separator</td>
      </tr>
      <tr>
          <td><code>ui.toolbar_spacer()</code></td>
          <td>Pushes items to opposite sides</td>
      </tr>
  </tbody>
</table>
<p>Each input also has a corresponding <code>ui.update_toolbar_input_*()</code> function for updating it dynamically.</p>
<h3 id="toolbars-in-card-headers-and-footers">Toolbars in card headers and footers
</h3>
<p>The most common use case is placing a toolbar in a card header to attach controls directly to a card&rsquo;s content:</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/toolbar_ex1.png" alt="Example of a card with a toolbar input button and toolbar input select" />
<figcaption aria-hidden="true">Example of a card with a toolbar input button and toolbar input select</figcaption>
</figure>
<div class="code-block"><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-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">faicons</span> <span class="kn">import</span> <span class="n">icon_svg</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="nb">input</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></span><span class="line"><span class="cl"><span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">card</span><span class="p">(</span><span class="n">full_screen</span><span class="o">=</span><span class="kc">True</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">card_header</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Header&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">toolbar</span><span class="p">(</span><span class="n">align</span><span class="o">=</span><span class="s2">&#34;right&#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">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="nb">id</span><span class="o">=</span><span class="s2">&#34;action1&#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="s2">&#34;Refresh&#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">icon_svg</span><span class="p">(</span><span class="s2">&#34;arrows-rotate&#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">toolbar_divider</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">toolbar_input_select</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="nb">id</span><span class="o">=</span><span class="s2">&#34;options&#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="s2">&#34;Filter&#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 class="s2">&#34;ABC&#34;</span><span class="p">,</span> <span class="s2">&#34;CDE&#34;</span><span class="p">,</span> <span class="s2">&#34;EFG&#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="nd">@render.text</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">toolbar_status</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;Button clicks: </span><span class="si">{</span><span class="nb">input</span><span class="o">.</span><span class="n">action1</span><span class="p">()</span><span class="si">}</span><span class="s2">, Selected: </span><span class="si">{</span><span class="nb">input</span><span class="o">.</span><span class="n">options</span><span class="p">()</span><span class="si">}</span><span class="s2">&#34;</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h3 id="toolbars-in-input-labels">Toolbars in input labels
</h3>
<p>You can also pass a toolbar as an input&rsquo;s <code>label</code> to add an info button for additional information or provide quick actions, like resetting an input value.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/toolbar_info_ex.png" alt="Example of an info toolbar button using a tooltip in an input label" />
<figcaption aria-hidden="true">Example of an info toolbar button using a tooltip in an input label</figcaption>
</figure>
<div class="code-block"><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></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">faicons</span> <span class="kn">import</span> <span class="n">icon_svg</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="k">with</span> <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;Data Settings&#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;threshold&#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="n">ui</span><span class="o">.</span><span class="n">toolbar</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">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;threshold_info&#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="s2">&#34;About this setting&#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">icon_svg</span><span class="p">(</span><span class="s2">&#34;circle-info&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">tooltip</span><span class="o">=</span><span class="s2">&#34;Standard deviations from the mean before a value is flagged as an outlier.&#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="s2">&#34;Outlier threshold&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">align</span><span class="o">=</span><span class="s2">&#34;left&#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="nb">min</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nb">max</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">value</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">step</span><span class="o">=</span><span class="mf">0.5</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_numeric</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;sample_size&#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="n">ui</span><span class="o">.</span><span class="n">toolbar</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">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;sample_info&#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="s2">&#34;About this setting&#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">icon_svg</span><span class="p">(</span><span class="s2">&#34;circle-info&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">tooltip</span><span class="o">=</span><span class="s2">&#34;Number of observations to draw from the dataset for each analysis run.&#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="s2">&#34;Sample size&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">align</span><span class="o">=</span><span class="s2">&#34;left&#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">value</span><span class="o">=</span><span class="mi">100</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nb">min</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nb">max</span><span class="o">=</span><span class="mi">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">step</span><span class="o">=</span><span class="mi">10</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>
<h3 id="toolbars-in-text-areas">Toolbars in text areas
</h3>
<p>The <code>input_submit_textarea()</code> component accepts a <code>toolbar</code> parameter directly, making it easy to add contextual controls for AI chat interfaces and message composers:</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/toolbar_textarea.png" alt="Example of a toolbar in an input submit textarea" />
<figcaption aria-hidden="true">Example of a toolbar in an input submit textarea</figcaption>
</figure>
<div class="code-block"><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></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">faicons</span> <span class="kn">import</span> <span class="n">icon_svg</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">reactive</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="nb">input</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></span><span class="line"><span class="cl"><span class="n">ui</span><span class="o">.</span><span class="n">page_opts</span><span class="p">(</span><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></span><span class="line"><span class="cl"><span class="n">messages</span> <span class="o">=</span> <span class="n">reactive</span><span class="o">.</span><span class="n">value</span><span class="p">([])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">card</span><span class="p">(</span><span class="n">full_screen</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">height</span><span class="o">=</span><span class="s2">&#34;250px&#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">card_header</span><span class="p">(</span><span class="s2">&#34;Message Composer&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">card_body</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_submit_textarea</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;message&#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="s2">&#34;Message&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">placeholder</span><span class="o">=</span><span class="s2">&#34;Compose your message...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">rows</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">toolbar</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">toolbar</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">toolbar_input_select</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;priority&#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="s2">&#34;Priority&#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 class="s2">&#34;Low&#34;</span><span class="p">,</span> <span class="s2">&#34;Medium&#34;</span><span class="p">,</span> <span class="s2">&#34;High&#34;</span><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;Medium&#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">icon_svg</span><span class="p">(</span><span class="s2">&#34;flag&#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">toolbar_divider</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">toolbar_input_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;attach&#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="s2">&#34;Attach&#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">icon_svg</span><span class="p">(</span><span class="s2">&#34;paperclip&#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">align</span><span class="o">=</span><span class="s2">&#34;right&#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="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">card</span><span class="p">(</span><span class="n">full_screen</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">height</span><span class="o">=</span><span class="s2">&#34;250px&#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">card_header</span><span class="p">(</span><span class="s2">&#34;Sent Messages&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">ui</span><span class="o">.</span><span class="n">card_body</span><span class="p">():</span>
</span></span><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">messages_output</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">            <span class="n">msg_list</span> <span class="o">=</span> <span class="n">messages</span><span class="o">.</span><span class="n">get</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="ow">not</span> <span class="n">msg_list</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">return</span> <span class="n">ui</span><span class="o">.</span><span class="n">p</span><span class="p">(</span><span class="s2">&#34;No messages sent yet.&#34;</span><span class="p">,</span> <span class="n">style</span><span class="o">=</span><span class="s2">&#34;color: #888;&#34;</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">ui</span><span class="o">.</span><span class="n">div</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="o">*</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">p</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                        <span class="sa">f</span><span class="s2">&#34;[</span><span class="si">{</span><span class="n">msg</span><span class="p">[</span><span class="s1">&#39;priority&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">] </span><span class="si">{</span><span class="n">msg</span><span class="p">[</span><span class="s1">&#39;text&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">style</span><span class="o">=</span><span class="s2">&#34;margin: 4px 0;&#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="k">for</span> <span class="n">msg</span> <span class="ow">in</span> <span class="nb">reversed</span><span class="p">(</span><span class="n">msg_list</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="nd">@reactive.effect</span>
</span></span><span class="line"><span class="cl"><span class="nd">@reactive.event</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">message_text</span> <span class="o">=</span> <span class="nb">input</span><span class="o">.</span><span class="n">message</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">message_text</span> <span class="ow">and</span> <span class="n">message_text</span><span class="o">.</span><span class="n">strip</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">current_messages</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">messages</span><span class="o">.</span><span class="n">get</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="n">current_messages</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="p">{</span><span class="s2">&#34;text&#34;</span><span class="p">:</span> <span class="n">message_text</span><span class="p">,</span> <span class="s2">&#34;priority&#34;</span><span class="p">:</span> <span class="nb">input</span><span class="o">.</span><span class="n">priority</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">messages</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">current_messages</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Toolbars are available in <code>py-shiny</code> and forthcoming in 






<a href="https://rstudio.github.io/bslib/" target="_blank" rel="noopener"><code>bslib</code></a>
 for R. For a complete walkthrough with full app examples, see the 






<a href="https://shiny.posit.co/py/components/layout/toolbar/" target="_blank" rel="noopener">Toolbar component page</a>
.</p>
<h2 id="opentelemetry">OpenTelemetry
</h2>
<p>Starting with Shiny <code>v1.6.0</code>, 






<a href="https://opentelemetry.io/" target="_blank" rel="noopener">OpenTelemetry</a>
 support is built directly into the framework.</p>
<p>OpenTelemetry (OTel) is a vendor-neutral observability standard that lets you collect telemetry data &mdash; traces, logs, and metrics &mdash; and send it to any compatible backend. For Shiny apps, this means you can finally answer questions like:</p>
<ul>
<li>Why is my app slow for certain users?</li>
<li>Which reactive expressions are taking the most time?</li>
<li>How long does it take for outputs to render?</li>
<li>What sequence of events occurs when a user interacts with my app?</li>
</ul>
<h3 id="getting-started">Getting started
</h3>
<p>The fastest way to get started is with 






<a href="https://logfire.pydantic.dev/" target="_blank" rel="noopener">Pydantic Logfire</a>
, which provides zero-configuration OTel setup:</p>
<div class="code-block"><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-bash" data-lang="bash"><span class="line"><span class="cl">pip install logfire
</span></span><span class="line"><span class="cl">logfire auth</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Then set an environment variable to tell Shiny what level of tracing to collect:</p>
<div class="code-block"><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"><span class="nb">export</span> <span class="nv">SHINY_OTEL_COLLECT</span><span class="o">=</span>reactivity</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>That&rsquo;s it &mdash; no changes to your app code required. Run your app and visit 






<a href="https://logfire.pydantic.dev/" target="_blank" rel="noopener">logfire.pydantic.dev</a>
 to see traces.</p>
<h3 id="otel-is-great-for-genai-apps">OTel is great for GenAI apps
</h3>
<p>Shiny&rsquo;s OTel integration pairs especially well with Generative AI applications. When a user reports that your chatbot feels slow, traces make it easy to pinpoint whether the delay is in the AI model request, streaming, tool execution, or a downstream reactive calculation.</p>
<p>The image below shows a trace from a weather forecast app powered by a Generative AI model. A single user session is captured in full detail:</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/weather_app_ui.png" alt="The weather forecast Shiny app powered by a Generative AI model" />
<figcaption aria-hidden="true">The weather forecast Shiny app powered by a Generative AI model</figcaption>
</figure>
<figure>
<img src="https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/weather_app_logfire.png" alt="Trace in Logfire showing a full user session with reactive updates, model calls, and a tool invocation" />
<figcaption aria-hidden="true">Trace in Logfire showing a full user session with reactive updates, model calls, and a tool invocation</figcaption>
</figure>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Collection levels</span>
</div>
<div class="callout-body">
<p><code>SHINY_OTEL_COLLECT</code> accepts three levels of 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> [Default] - Everything (currently equivalent to &ldquo;reactivity&rdquo;)</li>
</ul>
</div>
</div>
<h3 id="what-gets-traced-automatically">What gets traced automatically
</h3>
<p>Shiny automatically creates spans for all of the following &mdash; no manual instrumentation needed:</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 calculations such as <code>@reactive.calc</code>, <code>@reactive.effect</code>, <code>@render.*</code>, and other reactive constructs</li>
</ul>
<h3 id="works-with-any-otel-backend">Works with any OTel backend
</h3>
<p>Logfire is our recommended starting point, but Shiny&rsquo;s OTel integration is fully vendor-neutral. You can send traces to 






<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://grafana.com/products/cloud/" target="_blank" rel="noopener">Grafana Cloud</a>
, 






<a href="https://langfuse.com/" target="_blank" rel="noopener">Langfuse</a>
, or any other OTLP-compatible backend.</p>
<p>For local debugging without a backend, install the OpenTelemetry SDK and use the console exporter:</p>
<div class="code-block"><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 <span class="s2">&#34;shiny[otel]&#34;</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Full documentation &mdash; including custom spans, database instrumentation, and production considerations &mdash; is available in the 






<a href="https://shiny.posit.co/py/docs/opentelemetry.html" target="_blank" rel="noopener">OpenTelemetry guide</a>
.</p>
<h2 id="in-closing">In closing
</h2>
<p>We&rsquo;re excited to bring you these new features in Shiny <code>v1.6</code>. As always, if you have questions or feedback, 






<a href="https://discord.gg/yMGCamUMnS" target="_blank" rel="noopener">join us on Discord</a>
 or 






<a href="https://github.com/posit-dev/py-shiny/issues/new" target="_blank" rel="noopener">open an issue on GitHub</a>
. Happy Shiny-ing!</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-04-02_shiny-python-1.6/py-shiny-otel-toolbars.png" length="133106" type="image/png" />
    </item>
    <item>
      <title>How we chose Positron&#39;s Python type checker</title>
      <link>https://opensource.posit.co/blog/2026-03-31_python-type-checkers/</link>
      <pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-31_python-type-checkers/</guid>
      <dc:creator>Austin Dickey</dc:creator><description><![CDATA[<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-03-31_python-type-checkers/images/social.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>The open-source Python type checker and language server ecosystem has exploded. Over the past year or two, four language server extensions have appeared, each with a different take on what Python type checking should look like. We evaluated each of them to decide which one to bundle with Positron to enhance the Python data science experience.</p>
<h2 id="background">Background
</h2>
<p>The Language Server Protocol (LSP) is a cross-language, cross-IDE specification that allows different IDE extensions to contribute smart features like tab completions, hover info, and more. The four<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> Python extensions in this post are powered by type checkers, which are Python-specific tools that catch bugs in your code before runtime by guessing and checking the types of your variables. They do this by <em>statically analyzing</em> your code before you run it.</p>
<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>Positron&rsquo;s built-in language server uses your running Python session to provide runtime-aware completions and hover previews too! Beyond what&rsquo;s in code, it knows your DataFrame column names, your dictionary keys, your environment variables, and more. But the tools evaluated in this post handle the <em>static analysis</em> side: type checking, go-to-definition, rename, and code actions. Both run concurrently, and Positron merges their results.</p>
</div>
</div>
<p>With AI tools writing more of your code, a good language server helps you read and navigate code you didn&rsquo;t write. LLM-generated code also introduces bugs that type checkers catch before you run anything. For data scientists, who rely on code to be the reproducibility layer, and who can&rsquo;t automate away human judgment, what matters is a tool that helps you understand and trust your code.</p>
<p>We did this evaluation in November 2025 but have refreshed the data in this post at the time of publish.</p>
<h2 id="the-contenders">The contenders
</h2>
<table>
  <thead>
      <tr>
          <th>Tool</th>
          <th>Backing</th>
          <th>Language</th>
          <th>License</th>
          <th style="text-align: center">Stars</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>






<a href="https://github.com/facebook/pyrefly" target="_blank" rel="noopener"><strong>Pyrefly</strong></a>
</td>
          <td>Meta</td>
          <td>Rust</td>
          <td>MIT</td>
          <td style="text-align: center">5.5K</td>
      </tr>
      <tr>
          <td>






<a href="https://github.com/astral-sh/ty" target="_blank" rel="noopener"><strong>ty</strong></a>
</td>
          <td>Astral (OpenAI)</td>
          <td>Rust</td>
          <td>MIT</td>
          <td style="text-align: center">17.8K</td>
      </tr>
      <tr>
          <td>






<a href="https://github.com/detachhead/basedpyright" target="_blank" rel="noopener"><strong>Basedpyright</strong></a>
</td>
          <td>Community</td>
          <td>TypeScript</td>
          <td>MIT</td>
          <td style="text-align: center">3.2K</td>
      </tr>
      <tr>
          <td>






<a href="https://github.com/zubanls/zuban" target="_blank" rel="noopener"><strong>Zuban</strong></a>
</td>
          <td>Indie</td>
          <td>Rust</td>
          <td>AGPL-3.0</td>
          <td style="text-align: center">1K</td>
      </tr>
  </tbody>
</table>
<p><strong>Pyrefly</strong> is Meta&rsquo;s successor to Pyre. It takes a fast, aggressive approach to type inference, being able to catch issues even in code with no type annotations. It reached 






<a href="https://github.com/facebook/pyrefly/releases/tag/0.42.0" target="_blank" rel="noopener">beta status</a>
 in November 2025.</p>
<p><strong>ty</strong> is from Astral, the team behind uv and ruff. 






<a href="https://openai.com/index/openai-to-acquire-astral/" target="_blank" rel="noopener">OpenAI announced its acquisition of Astral</a>
 recently; Astral has stated that ty, ruff, and uv will remain open source and MIT-licensed. It&rsquo;s the newest project, with a focus on speed and tight integration with the Astral toolchain. It reached 






<a href="https://astral.sh/blog/ty" target="_blank" rel="noopener">beta status</a>
 in December 2025 and follows a &ldquo;gradual guarantee&rdquo; philosophy (more on that below).</p>
<p><strong>Basedpyright</strong> is a community fork of Microsoft&rsquo;s Pyright type checker, with additional type-checking rules and LSP features baked in. It&rsquo;s the most mature of the four and has the largest contributor base.</p>
<p><strong>Zuban</strong> is from David Halter, the author of Jedi (the longtime Python autocompletion library). It aims for mypy compatibility and ships as a pip-installable tool.</p>
<h2 id="what-we-tested">What we tested
</h2>
<p>We tested each language server across several dimensions, roughly following the 






<a href="https://github.com/posit-dev/positron/issues/10300" target="_blank" rel="noopener">rubric we outlined publicly</a>
:</p>
<ul>
<li><strong>Feature completeness</strong>: Completions, hover, go-to-definition, rename, code actions, diagnostics, inlay hints, call hierarchy</li>
<li><strong>Correctness</strong>: How well does the type checker handle real-world Python code?</li>
<li><strong>Performance</strong>: Startup time and time to first completion</li>
<li><strong>Ecosystem</strong>: License, community health, development velocity, production readiness</li>
</ul>
<p>We tested inside Positron with a mix of data science and general Python code.</p>
<h2 id="feature-completeness">Feature completeness
</h2>
<p>Here are some screenshots of hovers, tab-completions, and diagnostics from each extension:</p>
<div class="panel-tabset">
<ul id="tabset-1" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-1-1">Pyrefly</a></li>
<li><a href="#tabset-1-2">ty</a></li>
<li><a href="#tabset-1-3">Basedpyright</a></li>
<li><a href="#tabset-1-4">Zuban</a></li>
</ul>
<div id="tabset-1-1">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_python-type-checkers/images/pyrefly.png" alt="Clean documentation with some extra info; simple completions" />
<figcaption aria-hidden="true">Clean documentation with some extra info; simple completions</figcaption>
</figure>
</div>
<div id="tabset-1-2">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_python-type-checkers/images/ty.png" alt="Red diagnostic due to invalid syntax; completions have extra info" />
<figcaption aria-hidden="true">Red diagnostic due to invalid syntax; completions have extra info</figcaption>
</figure>
</div>
<div id="tabset-1-3">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_python-type-checkers/images/basedpyright.png" alt="One-line hover docs; completions for all dunder methods" />
<figcaption aria-hidden="true">One-line hover docs; completions for all dunder methods</figcaption>
</figure>
</div>
<div id="tabset-1-4">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_python-type-checkers/images/zuban.png" alt="Extra hover docs from the class docstring; syntax diagnostic" />
<figcaption aria-hidden="true">Extra hover docs from the class docstring; syntax diagnostic</figcaption>
</figure>
</div>
</div>
<p>All four provide the core features you&rsquo;d expect: completions, hover documentation, go-to-definition, semantic highlighting, and diagnostics. The differences show up in the details.</p>
<h3 id="pyrefly">Pyrefly
</h3>
<p>Strong feature set. The hover documentation is the best of the four; <strong>Pyrefly</strong> renders it cleanly and sometimes includes hyperlinks to class definitions.</p>
<h3 id="ty">ty
</h3>
<p>Fast and clean, now in beta. The completion details can sometimes feel a little overwhelming, but can help when expanded.</p>
<h3 id="basedpyright">Basedpyright
</h3>
<p>Handles type checking comprehensively well. The main friction point: it surfaces a lot of warnings out of the box. If you&rsquo;re doing exploratory data science, a wall of type errors on your first <code>pandas</code> import can feel hostile. You can tune this down, but the defaults are oriented toward stricter use cases like package development.</p>
<h3 id="zuban">Zuban
</h3>
<p>The least mature of the four so far. Installation requires a two-step process (<code>pip install zuban</code>, then configure the interpreter), and the analysis is tied to that specific Python installation on saved files only. Third-party library completions only work when stubs are available, not from installed packages. Symbol renaming once broke standard library code in our testing.</p>
<h2 id="type-checking-philosophy">Type checking philosophy
</h2>
<p>The bigger difference between these tools isn&rsquo;t features but how they think about type checking.</p>
<h3 id="gradual-guarantee-vsaggressive-inference">Gradual guarantee vs. aggressive inference
</h3>
<p><strong>ty</strong> follows what&rsquo;s called the <em>gradual guarantee</em>: removing a type annotation from correct code should never introduce a type error. The idea is that type checking should be additive. You opt in by adding types, and the checker only flags things it&rsquo;s sure about.</p>
<p>The other extensions take the opposite approach. They always infer types from your code, even when you haven&rsquo;t written any annotations. This means they can catch bugs in completely untyped code, but it also means they may flag code that runs perfectly fine.</p>
<p>For example:</p>
<div class="code-block"><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">my_list</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">my_list</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">&#34;foo&#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"># Pyrefly: bad-argument-type</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ty: &lt;no error&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Basedpyright: reportArgumentType</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Zuban: arg-type</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p><strong>Pyrefly</strong> infers <code>my_list</code> as <code>list[int]</code> and flags the <code>append(&quot;foo&quot;)</code> call as a type error. <strong>ty</strong> sees no annotations and stays silent. The code is dynamically typed and that&rsquo;s fine.</p>
<p>If you&rsquo;re doing exploratory data analysis and don&rsquo;t want to annotate everything, <strong>ty</strong>&rsquo;s restraint might be more comfortable. But if you&rsquo;re writing a library and want to catch bugs early, <strong>Pyrefly</strong>&rsquo;s aggressiveness is helpful. For example:</p>
<div class="code-block"><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-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">process</span><span class="p">(</span><span class="n">data</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">str</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="n">process</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>  <span class="c1"># Raises a runtime AttributeError</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pyrefly: unsupported-operation</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ty: &lt;no error&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Basedpyright: reportOperatorIssue</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Zuban: operator</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p><strong>Basedpyright</strong> and <strong>Zuban</strong> land somewhere in between, with <strong>Basedpyright</strong> leaning toward stricter checking and <strong>Zuban</strong> aiming for mypy compatibility. Each of these extensions has the ability to suppress certain diagnostics you actually see when typing if you wish.</p>
<p>For a deeper dive on this topic, Edward Li&rsquo;s 






<a href="https://blog.edward-li.com/tech/comparing-pyrefly-vs-ty/" target="_blank" rel="noopener">comparison of <strong>Pyrefly</strong> and <strong>ty</strong></a>
 and Rob Hand&rsquo;s 






<a href="https://sinon.github.io/future-python-type-checkers/" target="_blank" rel="noopener">overview of future Python type checkers</a>
 are both worth reading, though some bugs have been fixed since they were published.</p>
<h2 id="performance">Performance
</h2>
<p>We measured startup time (how long until the language server responds to an <code>initialize</code> request) and time to first completion (how long a <code>textDocument/completion</code> request takes after initialization) in a relatively small repository. We ran each measurement five times and averaged. As always, these results only represent our computer&rsquo;s experimental setup.</p>
<table>
  <thead>
      <tr>
          <th>LSP</th>
          <th style="text-align: center">Avg. startup (s)</th>
          <th style="text-align: center">Avg. first completion (ms)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Pyrefly</strong></td>
          <td style="text-align: center">5.8</td>
          <td style="text-align: center">190</td>
      </tr>
      <tr>
          <td><strong>ty</strong></td>
          <td style="text-align: center">2.2</td>
          <td style="text-align: center">88</td>
      </tr>
      <tr>
          <td><strong>Basedpyright</strong></td>
          <td style="text-align: center">3.1</td>
          <td style="text-align: center">112</td>
      </tr>
      <tr>
          <td><strong>Zuban</strong></td>
          <td style="text-align: center">N/A<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></td>
          <td style="text-align: center">97</td>
      </tr>
  </tbody>
</table>
<p><strong>ty</strong> was the fastest across the board. But the practical differences are small: a 3-second difference in startup happens once per session, and a 100ms difference in completions is imperceptible. All four are fast enough that differences are negligible for daily use.</p>
<h2 id="ecosystem-health">Ecosystem health
</h2>
<p>We also looked at each project&rsquo;s development velocity and community health metrics. A language server you rely on daily needs to keep up with Python&rsquo;s evolution.</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th style="text-align: center"><strong>Pyrefly</strong></th>
          <th style="text-align: center"><strong>ty</strong></th>
          <th style="text-align: center"><strong>Basedpyright</strong></th>
          <th style="text-align: center"><strong>Zuban</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GitHub stars</td>
          <td style="text-align: center">5.5K</td>
          <td style="text-align: center">17.8K</td>
          <td style="text-align: center">3.2K</td>
          <td style="text-align: center">1K</td>
      </tr>
      <tr>
          <td>Contributors</td>
          <td style="text-align: center">162</td>
          <td style="text-align: center">186<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></td>
          <td style="text-align: center">82</td>
          <td style="text-align: center">17</td>
      </tr>
      <tr>
          <td>License</td>
          <td style="text-align: center">MIT</td>
          <td style="text-align: center">MIT</td>
          <td style="text-align: center">MIT</td>
          <td style="text-align: center">AGPL-3.0</td>
      </tr>
      <tr>
          <td>Releases (since Nov 2025)</td>
          <td style="text-align: center">17</td>
          <td style="text-align: center">29</td>
          <td style="text-align: center">10</td>
          <td style="text-align: center">9</td>
      </tr>
      <tr>
          <td>Release cadence</td>
          <td style="text-align: center">~weekly</td>
          <td style="text-align: center">~twice weekly</td>
          <td style="text-align: center">~biweekly</td>
          <td style="text-align: center">~biweekly</td>
      </tr>
      <tr>
          <td>Issues opened (90 days)</td>
          <td style="text-align: center">540</td>
          <td style="text-align: center">789</td>
          <td style="text-align: center">40</td>
          <td style="text-align: center">125</td>
      </tr>
      <tr>
          <td>Issues closed (90 days)</td>
          <td style="text-align: center">531</td>
          <td style="text-align: center">712</td>
          <td style="text-align: center">20</td>
          <td style="text-align: center">111</td>
      </tr>
  </tbody>
</table>
<p><strong>ty</strong> and <strong>Pyrefly</strong> are shipping fast. Both are on a weekly release cadence or higher with high issue throughput. <strong>ty</strong>&rsquo;s issue volume is notable: 789 issues opened in 90 days reflects both heavy adoption and active bug reporting. <strong>Pyrefly</strong> is closing more issues than it&rsquo;s opening, a good sign for a beta project.</p>
<p>Response times are quick. In a spot-check of recent issues, <strong>ty</strong> and <strong>Pyrefly</strong> both had first responses from core maintainers within minutes to hours. <strong>Basedpyright</strong>&rsquo;s maintainer responds quickly too, though at a lower volume. <strong>Zuban</strong>&rsquo;s maintainer often replies within an hour.</p>
<h2 id="what-we-chose">What we chose
</h2>
<p>We bundled <strong>Pyrefly</strong> as Positron&rsquo;s default Python language server.</p>
<p>The deciding factors:</p>
<ul>
<li><strong>Pyrefly</strong>&rsquo;s clean design decisions felt like the best fit for Positron. The hover docs are rendered and hyperlinked, with sources for type inference. The type inference catches real bugs without requiring you to annotate everything. While it has the strictest type checking, this is configured to a moderate level by default.</li>
<li>It has active development with strong backing. Meta has committed to making <strong>Pyrefly</strong> genuinely open-source and community-driven, with biweekly office hours and a public Discord. Development velocity is high.</li>
<li>It is MIT licensed, which allows us to bundle it into Positron.</li>
</ul>
<p>It wasn&rsquo;t a runaway winner. <strong>Basedpyright</strong> is more mature and feature-complete. <strong>ty</strong> has a lot of long-term potential, especially for ruff users and fans of the gradual guarantee, and is closing feature gaps fast. But for the specific use case of &ldquo;Python data science in an IDE,&rdquo; <strong>Pyrefly</strong> had the best balance of features, UX, and readiness.</p>
<h2 id="how-to-switch">How to switch
</h2>
<p>This space is competitive and moving fast, and you shouldn&rsquo;t feel locked in. Positron makes it straightforward to switch language servers:</p>
<ol>
<li>Open the <strong>Extensions</strong> view (<code>Ctrl-Shift-X</code> (linux), <code>Ctrl-Shift-X</code> (windows), <code>Command-Shift-X</code> (mac)).</li>
<li>Search for and install the language server you want to try (e.g., <code>basedpyright</code>, <code>ty</code>, or <code>zuban</code>).</li>
<li>Disable <strong>Pyrefly</strong>: search for <code>pyrefly</code> in Extensions, click <strong>Disable</strong>.</li>
<li>Reload the window with the command <em>Developer: Reload Window</em>.</li>
</ol>
<p>Or, if you want to keep <strong>Pyrefly</strong> installed but prevent it from auto-activating, you can use the 






<a href="positron://settings/extensions.allowed"><code>extensions.allowed</code></a>
 setting:</p>
<div class="code-block"><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-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;extensions.allowed&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;meta.pyrefly&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;*&#34;</span><span class="p">:</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="p">}</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<h2 id="whats-next">What&rsquo;s next
</h2>
<p>We started bundling <strong>Pyrefly</strong> in November and have been quite pleased with the results. It solved some longstanding user-requested issues (like better semantic highlighting) and feels snappier to users than our previous internal solution.</p>
<p><strong>ty</strong> is adding features at an aggressive pace and will likely close its remaining gaps. OpenAI&rsquo;s acquisition of Astral adds resources but also uncertainty; it&rsquo;s unclear how it will affect <strong>ty</strong>&rsquo;s priorities. <strong>Pyrefly</strong> continues to improve its type checking and performance (a recent release noted 






<a href="https://github.com/facebook/pyrefly/releases/tag/0.57.0" target="_blank" rel="noopener">20% faster PyTorch benchmarks</a>
). <strong>Basedpyright</strong> tracks upstream Pyright closely and keeps shipping.</p>
<p>Both <strong>ty</strong> and <strong>Pyrefly</strong> have been receptive to PRs that improve the experience for Positron users, which suggests they care about working well across editors, not just VS Code. For example, both contribute hover, completions, and semantic highlighting in the Positron Console.</p>
<p>We&rsquo;ll keep evaluating as these tools mature! Want to try Positron? 






<a href="https://positron.posit.co/download.html" target="_blank" rel="noopener">Download it here</a>
.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Another LSP extension is Pylance, which may be familiar to VS Code users, but due to licensing restrictions, Code-OSS forks like Positron cannot use it.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><strong>Zuban</strong> requires a multi-step manual startup, so we couldn&rsquo;t measure this automatically.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Edit (2026-04-01): A previous version of this post undercounted the number of contributors to <strong>ty</strong>. The updated script to fetch stats lives 






<a href="https://github.com/posit-dev/positron-website/blob/main/blog/posts/2026-03-31-python-type-checkers/fetch_stats.py" target="_blank" rel="noopener">here</a>
.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-31_python-type-checkers/images/social.png" length="30311" type="image/png" />
    </item>
    <item>
      <title>Typst Books, Article Layout, and `typst-gather`</title>
      <link>https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/</link>
      <pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/</guid>
      <dc:creator>Gordon Woodhull</dc:creator><description><![CDATA[<p>Typst is a lightning-fast typesetting system that provides a modern alternative to LaTeX.</p>
<p>The Typst ecosystem is thriving, and Quarto 1.9 brings Typst much closer to feature parity with LaTeX:</p>
<ul>
<li>Typst books</li>
<li>Article layout in Typst</li>
<li>Bundling of Typst packages for offline rendering</li>
</ul>
<h2 id="typst-books">Typst books
</h2>
<p>In Quarto 1.9, a project with type <code>book</code> and format <code>typst</code> is now rendered as a single document with multiple chapters and other book content.</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-0">
  <div class="code-with-filename-label" id="code-filename-0"><span class="font-mono text-sm">_quarto.yml</span></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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">project</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">book</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">book</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;My Book&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">author</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Jane Doe&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">chapters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">index.qmd</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">intro.qmd</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">summary.qmd</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">typst</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="grid gap-12 items-start md:grid-cols-4">
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-book-part-page.png" data-fig-alt="A Typst book rendered with the orange-book extension, showing the part one page with a colored background and table of contents" alt="Part page" />
<figcaption aria-hidden="true">Part page</figcaption>
</figure>
</div>
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-book-1.png" data-fig-alt="A Typst book rendered with the orange-book extension, showing the chapter one page with colored headers and sidebar navigation" alt="Chapter page" />
<figcaption aria-hidden="true">Chapter page</figcaption>
</figure>
</div>
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-book-2.png" data-fig-alt="A Typst book rendered with the orange-book extension, showing the second page from chapter one with colored headers and sidebar navigation" alt="Chapter content" />
<figcaption aria-hidden="true">Chapter content</figcaption>
</figure>
</div>
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-book-3.png" data-fig-alt="A Typst book rendered with the orange-book extension, showing the chapter two page with colored headers and sidebar navigation" alt="Next chapter" />
<figcaption aria-hidden="true">Next chapter</figcaption>
</figure>
</div>
</div>
<p>All book features previously available in the LaTeX format are now available in Typst:</p>
<ul>
<li>Parts and Chapters</li>
<li>Appendices</li>
<li>Cross-references and chapter-based numbering</li>
<li>Table of Contents</li>
</ul>
<p>List-of-Figures and List-of-Tables support is 






<a href="https://github.com/quarto-dev/quarto-cli/issues/14081" target="_blank" rel="noopener">coming soon</a>
.</p>
<p>The default Typst book uses the bundled Quarto 






<a href="https://github.com/quarto-ext/orange-book" target="_blank" rel="noopener">quarto-orange-book</a>
 extension, which uses 


  
  
  





<a href="#typst-gather"><code>typst-gather</code></a>
 to bundle the Typst 






<a href="https://typst.app/universe/package/orange-book" target="_blank" rel="noopener">orange-book</a>
 package. Orange-book provides a textbook-style layout with colored chapter headers and sidebars.</p>
<p>The orange-book extension supports 






<a href="https://quarto.org/docs/authoring/brand.html" target="_blank" rel="noopener">brand.yml</a>
 customization &mdash; it uses the <code>primary</code> color for chapter headers and sidebars, and the <code>medium</code> logo on the title page. The screenshots above were generated with this <code>_brand.yml</code>:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-1">
  <div class="code-with-filename-label" id="code-filename-1"><span class="font-mono text-sm">_brand.yml</span></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-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">color</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">primary</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;#F36619&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">secondary</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;#2E86AB&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">logo</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">images</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">test-logo</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">logo.svg</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">alt</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Test Logo&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">medium</span><span class="p">:</span><span class="w"> </span><span class="l">test-logo</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Since Typst books are implemented as Quarto 






<a href="https://quarto.org/docs/extensions/formats.html" target="_blank" rel="noopener">Format Extensions</a>
, you can customize the appearance by creating your own extension. Typst partials define the overall book structure, while Lua filters handle the necessary AST transformations.</p>
<h2 id="article-layout-in-typst">Article layout in Typst
</h2>
<p>Also in Quarto 1.9, all 






<a href="https://quarto.org/docs/authoring/article-layout.html" target="_blank" rel="noopener">Article Layout</a>
 features now work in Typst, via the Typst 






<a href="https://typst.app/universe/package/marginalia/" target="_blank" rel="noopener">Marginalia</a>
 package.</p>
<p>Specifically:</p>
<ul>
<li>Figures, tables, code listings, and equations can be placed in the margin using the <code>.column-margin</code> class or the <code>column: margin</code> code cell option.</li>
<li>You can also target specific output types with <code>fig-column: margin</code> or <code>tbl-column: margin</code>.</li>
<li>Figure, table, and code listing captions can be placed in the margin with <code>cap-location: margin</code> (or <code>fig-cap-location: margin</code> and <code>tbl-cap-location: margin</code> for specific types).</li>
<li>Footnotes and citations can be displayed in the margin with <code>reference-location: margin</code> and <code>citation-location: margin</code>. When margin citations are enabled, the bibliography is suppressed.</li>
<li>Asides (<code>.aside</code> class) place content in the margin without a footnote number.</li>
</ul>
<div class="grid gap-12 items-start md:grid-cols-3">
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-article.png" data-group="article" data-fig-alt="A page of a Typst article with a margin note and a margin figure using the Marginalia package" alt="Margin note and figure" />
<figcaption aria-hidden="true">Margin note and figure</figcaption>
</figure>
</div>
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-article-2.png" data-group="article" data-fig-alt="A page of a Typst article using margin captions" alt="Margin captions" />
<figcaption aria-hidden="true">Margin captions</figcaption>
</figure>
</div>
<div class="prose max-w-none">
<figure>
<img src="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-article-3.png" data-group="article" data-fig-alt="A page of a Typst article using margin references" alt="Margin references" />
<figcaption aria-hidden="true">Margin references</figcaption>
</figure>
</div>
</div>
<div class="callout callout-warning" role="note" aria-label="Warning">
<div class="callout-header">
<span class="callout-title">Books with article layout are functional, but need work</span>
</div>
<div class="callout-body">
<p>You can combine book and article layout, but there are some layout quirks when combining the two. We&rsquo;ll work with the orange-book author to integrate Marginalia into the book template.</p>
</div>
</div>
<h2 id="typst-gather"><code>typst-gather</code>
</h2>
<p>Quarto 1.9 automatically stages Typst packages &mdash; from your extensions, from Quarto&rsquo;s bundled extensions, and from Quarto itself &mdash; into the <code>.quarto/</code> cache directory before calling <code>typst compile</code>. This means Typst documents render offline without needing network access.</p>
<p>To make this work, extension authors use the new 






<a href="https://quarto.org/docs/advanced/typst/typst-gather.html" target="_blank" rel="noopener"><code>typst-gather</code></a>
 tool, which scans their <code>.typ</code> files for <code>@preview</code> imports and downloads the packages into the extension directory. Authors run <code>quarto call typst-gather</code> and commit the results. Users of the extension will have the packages staged without any downloads.</p>
<p>This means 


  
  
  





<a href="https://quarto.org/docs/output-formats/typst-custom.html#custom-formats" target="_blank" rel="noopener">Custom Typst Formats</a>
 can depend on Typst packages without copying and pasting Typst code, making them simpler and easier to maintain.</p>
<p>Both Typst books and article layout are built on <code>typst-gather</code> &mdash; orange-book depends on the Typst 






<a href="https://typst.app/universe/package/orange-book" target="_blank" rel="noopener">orange-book</a>
 package, and article layout depends on 






<a href="https://typst.app/universe/package/marginalia/" target="_blank" rel="noopener">Marginalia</a>
. As the Typst package ecosystem grows, we&rsquo;re excited to see what the community builds with Typst packages.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/typst-article-landscape.png" length="409281" type="image/png" />
    </item>
    <item>
      <title>RAG with raghilda</title>
      <link>https://opensource.posit.co/blog/2026-03-30_rag-with-raghilda/</link>
      <pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-30_rag-with-raghilda/</guid>
      <dc:creator>Daniel Falbel</dc:creator>
      <dc:creator>Tomasz Kalinowski</dc:creator><description><![CDATA[<p>We&rsquo;re happy to introduce Raghilda, a new Python package for building RAG (Retrieval-Augmented Generation) solutions.</p>
<p>RAG is a simple concept that comes up anytime you want to <em>retrieve</em> content for an LLM to improve or <em>augment</em> the <em>generated</em> output.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-03-30_rag-with-raghilda/rag-diagram.svg"
      alt="Flowchart comparing two pipelines: Without RAG shows User Query flowing directly to LLM to Response. With RAG shows User Query flowing through a Retrieve Relevant Documents step before reaching the LLM."  title="Without RAG, the LLM generates a response using only the user query. With RAG, relevant documents are retrieved and provided to the LLM before it generates a response." 
      loading="lazy"
    ><figcaption class="text-sm text-center text-gray-500">Without RAG, the LLM generates a response using only the user query. With RAG, relevant documents are retrieved and provided to the LLM before it generates a response.</figcaption>
  </figure></div>
</p>
<p>LLMs are great at reasoning and generating text, but their knowledge is frozen at training time.
They can&rsquo;t access private documents, recent information, or anything that wasn&rsquo;t in the training
data. When asked about these topics, they either refuse to answer or — worse — hallucinate
a confident-sounding response. RAG solves this by giving the model access to relevant
information at query time, without needing to retrain it.</p>
<p>In practice, most tools built on top of LLMs already do this. ChatGPT uses web search to
include recent news in its answers. Claude Code reads the codebase using tools like <code>grep</code>,
<code>list_files</code>, and <code>symbol_search</code> before generating code. RAG is what makes LLMs useful for
tasks that require specific, up-to-date, or private knowledge.</p>
<p>Modern LLMs support context windows of 100K tokens or more, which might seem like it makes
RAG unnecessary — just paste everything into the prompt. But this doesn&rsquo;t work well in
practice. LLMs suffer from &ldquo;lost in the middle&rdquo; (sometimes called &ldquo;context rot&rdquo;): they pay less
attention to information buried in the middle of a very long prompt, so relevant content gets
missed. On top of that, sending your entire knowledge base with every query is expensive, slow,
and most real-world document collections are too large to fit in a single context window anyway.
Long context and RAG are complementary — a larger window lets you include more retrieved
chunks, but retrieval is what gives you precision: the model sees 5 relevant paragraphs instead
of 500 irrelevant pages.</p>
<h2 id="raghilda">raghilda
</h2>
<p>Building a good retrieval system is the hard part of RAG.
raghilda is a Python framework designed to handle it. You
give it URLs or file paths, and it takes care of the rest.
The defaults are opinionated but transparent — every step is
exposed and replaceable. You can swap the chunker, change the
embedding provider, or write a custom ingestion function
without fighting the framework.</p>
<ul>
<li>
<p><strong>Document processing.</strong> Converting HTML pages, PDFs, and DOCX files into clean text is
surprisingly messy. raghilda handles this automatically, converting documents to Markdown.
For websites, <code>find_links()</code> crawls and discovers pages so you don&rsquo;t have to list them
by hand.</p>
</li>
<li>
<p><strong>Smart chunking.</strong> Naive fixed-size chunking can split a code block or paragraph in half.
raghilda&rsquo;s Markdown chunker splits text at semantic boundaries — headings, paragraphs,
sentences — and preserves the heading hierarchy so each chunk retains context about where
it came from.</p>
</li>
<li>
<p><strong>Multiple storage backends.</strong> raghilda supports DuckDB (local, zero-config), ChromaDB,
and OpenAI Vector Stores. The API is the same across backends, so you can start with a
local DuckDB file and move to a hosted solution later without rewriting your code.</p>
</li>
<li>
<p><strong>Hybrid retrieval.</strong> Pure vector search finds semantically similar content but misses exact
keyword matches. raghilda combines semantic search, BM25 keyword matching, and attribute
filters — so you can search by meaning, by keywords, and by metadata (e.g. source URL,
document type, or any custom attribute) all at once.</p>
</li>
</ul>
<h2 id="how-it-works">How it works
</h2>
<p>A retrieval system has two phases: <strong>ingestion</strong> — turning
your documents into a searchable store — and <strong>retrieval</strong> —
finding the right chunks given a query. raghilda exposes both
phases clearly, with each step exposed as an
individual call you can customize or replace.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-03-30_rag-with-raghilda/pipeline-diagram.svg"
      alt="Flowchart showing two phases: Ingestion flows from URLs/Files through Read as Markdown, Chunk, Embed, to Store. Retrieval fans a Query out to Semantic Search, BM25 Search, and Attribute Filters, which merge into Relevant Chunks."  title="raghilda&#39;s two phases: ingestion prepares your documents for search, retrieval finds the relevant chunks at query time." 
      loading="lazy"
    ><figcaption class="text-sm text-center text-gray-500">raghilda&rsquo;s two phases: ingestion prepares your documents for search, retrieval finds the relevant chunks at query time.</figcaption>
  </figure></div>
</p>
<p>Let&rsquo;s walk through a minimal example using a Wikipedia article about Princess Ragnhild
of Norway.</p>
<p>First, you <strong>create a store</strong> with an embedding provider. The store is where your chunks and
their vector embeddings will live:</p>
<div class="code-block"><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="kn">from</span> <span class="nn">raghilda.store</span> <span class="kn">import</span> <span class="n">DuckDBStore</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.embedding</span> <span class="kn">import</span> <span class="n">EmbeddingOpenAI</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">store</span> <span class="o">=</span> <span class="n">DuckDBStore</span><span class="o">.</span><span class="n">create</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">location</span><span class="o">=</span><span class="s2">&#34;ragnhild.db&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">embed</span><span class="o">=</span><span class="n">EmbeddingOpenAI</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 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>
<p>Then you <strong>read</strong> the document and <strong>chunk</strong> it. <code>read_as_markdown()</code> converts the URL
to Markdown, and <code>MarkdownChunker</code> splits it into overlapping chunks at semantic boundaries:</p>
<div class="code-block"><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">raghilda.read</span> <span class="kn">import</span> <span class="n">read_as_markdown</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">raghilda.chunker</span> <span class="kn">import</span> <span class="n">MarkdownChunker</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">doc</span> <span class="o">=</span> <span class="n">read_as_markdown</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;https://en.wikipedia.org/wiki/Princess_Ragnhild,_Mrs._Lorentzen&#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"># We intentionally use a small chunk size for display purposes.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># In practice, chunk sizes of ~1600 characters are a good</span>
</span></span><span class="line"><span class="cl"><span class="c1"># compromise on size versus retrieval quality.</span>
</span></span><span class="line"><span class="cl"><span class="n">chunker</span> <span class="o">=</span> <span class="n">MarkdownChunker</span><span class="p">(</span><span class="n">chunk_size</span><span class="o">=</span><span class="mi">200</span><span class="p">,</span> <span class="n">target_overlap</span><span class="o">=</span><span class="mf">0.25</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">chunked</span> <span class="o">=</span> <span class="n">chunker</span><span class="o">.</span><span class="n">chunk</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">chunked</span><span class="o">.</span><span class="n">chunks</span><span class="p">)</span><span class="si">}</span><span class="s2"> chunks&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="code-block"><pre tabindex="0"><code>185 chunks</code></pre></div>
<p>Finally, you <strong>upsert</strong> the chunked document into the store
and build the search indexes. Embedding is handled by the
store itself — since the embedding provider is configured at
creation time, all chunks in a store are guaranteed to use
consistent embeddings:</p>
<div class="code-block"><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="n">store</span><span class="o">.</span><span class="n">upsert</span><span class="p">(</span><span class="n">chunked</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">store</span><span class="o">.</span><span class="n">build_index</span><span class="p">()</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>During <strong>retrieval</strong>, you query the store and get back the most relevant chunks. raghilda
runs semantic search and BM25 keyword search, then merges
the results:</p>
<div class="code-block"><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-python" data-lang="python"><span class="line"><span class="cl"><span class="n">chunks</span> <span class="o">=</span> <span class="n">store</span><span class="o">.</span><span class="n">retrieve</span><span class="p">(</span><span class="s2">&#34;Did she move to Brazil?&#34;</span><span class="p">,</span> <span class="n">top_k</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">chunks</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">chunk</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;---&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="code-block"><pre tabindex="0"><code>Lorentzen&#34;), a member of the Lorentzen family of shipping magnates.
In the same year, they moved to Brazil, where her husband was an
industrialist and a main owner of Aracruz Celulose. She lived in
Brazil until her death 59 years later.
---
to Rio de Janeiro, where her husband had substantial business
holdings. Their residence in Brazil was originally temporary, but
they
---</code></pre></div>
<blockquote>
<p><strong>Tip:</strong> In practice, you&rsquo;ll usually work with many documents at
once — just loop over your URLs or file paths and call
<code>upsert()</code> for each one.
See the 






<a href="https://posit-dev.github.io/raghilda/user-guide/getting-started.html" target="_blank" rel="noopener">Getting Started</a>
 guide for
a full walkthrough.</p>
</blockquote>
<h2 id="using-with-an-llm">Using with an LLM
</h2>
<p>A retrieval system on its own just returns text chunks. To
get actual answers, you connect it to an LLM. The simplest
way to do this is to register a search function as a tool
that the LLM can call when it needs information.</p>
<p>Here&rsquo;s an example using







<a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener">chatlas</a>
:</p>
<div class="code-block"><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">ChatOpenAI</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">json</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">search_ragnhild</span><span class="p">(</span><span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Search for information about Princess Ragnhild.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">chunks</span> <span class="o">=</span> <span class="n">store</span><span class="o">.</span><span class="n">retrieve</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">top_k</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">[{</span><span class="s2">&#34;text&#34;</span><span class="p">:</span> <span class="n">c</span><span class="o">.</span><span class="n">text</span><span class="p">,</span> <span class="s2">&#34;context&#34;</span><span class="p">:</span> <span class="n">c</span><span class="o">.</span><span class="n">context</span><span class="p">}</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">chunks</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">chat</span> <span class="o">=</span> <span class="n">ChatOpenAI</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span><span class="o">=</span><span class="s2">&#34;gpt-4.1-mini&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt</span><span class="o">=</span><span class="s2">&#34;Answer questions about Princess Ragnhild &#34;</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;using the search tool. Always search before answering.&#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">chat</span><span class="o">.</span><span class="n">register_tool</span><span class="p">(</span><span class="n">search_ragnhild</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">_</span> <span class="o">=</span> <span class="n">chat</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="s2">&#34;Which year did she move to Brazil?&#34;</span><span class="p">,</span> <span class="n">echo</span><span class="o">=</span><span class="s2">&#34;text&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="code-block"><pre tabindex="0"><code>Princess Ragnhild moved to Brazil in the same year she got married,
1953. Her marriage and the move to Brazil were connected, as her
husband was an industrialist and owner of business holdings in Brazil.
They settled in Rio de Janeiro and lived there until her death in 2012.</code></pre></div>
<p>Compare this with the same question without the search tool:</p>
<div class="code-block"><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">chat_no_rag</span> <span class="o">=</span> <span class="n">ChatOpenAI</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span><span class="o">=</span><span class="s2">&#34;gpt-4.1-mini&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt</span><span class="o">=</span><span class="s2">&#34;Answer questions about Princess Ragnhild.&#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">_</span> <span class="o">=</span> <span class="n">chat_no_rag</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Which year did she move to Brazil?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">echo</span><span class="o">=</span><span class="s2">&#34;text&#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 class="code-block"><pre tabindex="0"><code>Princess Ragnhild moved to Brazil in 1960.</code></pre></div>
<p>With the search tool, the LLM retrieves the relevant chunks
and grounds its answer in the actual document. Without it,
the model has to rely on its training data — and may
hallucinate or give a vague response.</p>
<h2 id="learn-more">Learn more
</h2>
<ul>
<li>






<a href="https://posit-dev.github.io/raghilda/user-guide/getting-started.html" target="_blank" rel="noopener">Getting Started</a>
 — full
walkthrough building a store from a documentation site</li>
<li>






<a href="https://github.com/posit-dev/raghilda/tree/main/examples" target="_blank" rel="noopener">Examples</a>

— complete scripts showing RAG workflows with chatlas,
ChromaDB, and more</li>
<li>






<a href="https://github.com/posit-dev/raghilda" target="_blank" rel="noopener">GitHub repository</a>

— source code, issues, and contributions</li>
</ul>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-30_rag-with-raghilda/thumbnail-wd.jpg" length="113156" type="image/jpeg" />
    </item>
    <item>
      <title>Quarto 1.9</title>
      <link>https://opensource.posit.co/blog/2026-03-24_1.9-release/</link>
      <pubDate>Tue, 24 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-24_1.9-release/</guid>
      <dc:creator>Charlotte Wickham</dc:creator><description><![CDATA[<p>Quarto 1.9 is out! You can get the current release from the 






<a href="https://quarto.org/docs/download/index.html" target="_blank" rel="noopener">download page</a>
.</p>
<p>Sharing your work just got easier with integrated Posit Connect Cloud publishing. Typst users will appreciate book project support and article layouts, while experimental PDF accessibility standards bring PDF/A and PDF/UA compliance to both LaTeX and Typst. This release also introduces LLM-friendly output for websites, the <code>quarto use brand</code> command for keeping your brand assets in sync, and list tables for authoring complex tables with familiar bullet syntax.</p>
<p>You can read about these improvements and some other highlights below. You can find all the changes in this version in the 






<a href="https://quarto.org/docs/download/changelog/1.9/" target="_blank" rel="noopener">Release Notes</a>
.</p>
<h2 id="publish-to-posit-connect-cloud">Publish to Posit Connect Cloud
</h2>
<p>You can now publish documents and websites to 






<a href="https://connect.posit.cloud" target="_blank" rel="noopener">Posit Connect Cloud</a>
 directly from the command line.
For example, publish your Quarto website project with:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-0">
  <div class="code-with-filename-label" id="code-filename-0"><span class="font-mono text-sm">Terminal</span></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></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 publish posit-connect-cloud</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Posit Connect Cloud is a hosted platform for sharing data applications and documents without managing your own infrastructure. It includes a free tier for unlimited static document publishing.
Read more in 






<a href="https://quarto.org/docs/publishing/posit-connect-cloud.html" target="_blank" rel="noopener">Publishing &gt; Posit Connect Cloud</a>
.</p>
<h2 id="improvements-to-typst-support">Improvements to Typst Support
</h2>
<p>Quarto 1.9 brings substantial improvements to Typst output:</p>
<ul>
<li>


  
  
  





<a href="https://quarto.org/docs/books/book-output.html#typst-output" target="_blank" rel="noopener">Book projects</a>
 can now render to Typst via the bundled <code>orange-book</code> extension, with chapter numbering, cross-references, and professional textbook styling.</li>
<li>






<a href="https://quarto.org/docs/authoring/article-layout.html" target="_blank" rel="noopener">Article layout</a>
 support lets you place content in the margins, create full-width figures, or add side notes.</li>
<li>New options: <code>mathfont</code>, <code>codefont</code>, <code>linestretch</code>, <code>linkcolor</code>, <code>citecolor</code>, <code>filecolor</code>, <code>thanks</code>, and <code>abstract-title</code>.</li>
<li>


  
  
  





<a href="https://quarto.org/docs/output-formats/typst.html#theorems" target="_blank" rel="noopener">Theorem styling</a>
 with four appearance options: <code>simple</code>, <code>fancy</code>, <code>clouds</code>, or <code>rainbow</code>.</li>
</ul>
<p>See 





  


  
  
    
  

<a href="https://opensource.posit.co/blog/2026-03-31_typst-books-and-more/">this blog post</a>
 for details on all the Typst improvements.</p>
<h2 id="pdf-accessibility-experimental">PDF Accessibility (Experimental)
</h2>
<p>We&rsquo;re rolling out experimental support for PDF accessibility standards in 1.9. The new <code>pdf-standard</code> option enables PDF/A archival formats and PDF/UA accessibility compliance for both LaTeX and Typst outputs. Alt text from <code>fig-alt</code> attributes now passes through to PDF for screen reader support, and Typst gains support for alt text on cross-referenced equations.</p>
<p>Read more in our 





  


  
  
    
  

<a href="https://opensource.posit.co/blog/2026-03-05_pdf-accessibility-and-standards/">PDF Accessibility and Standards blog post</a>
 or the documentation for 


  
  
  





<a href="https://quarto.org/docs/output-formats/pdf-basics.html#pdf-accessibility-standards" target="_blank" rel="noopener">LaTeX</a>
 and 


  
  
  





<a href="https://quarto.org/docs/output-formats/typst.html#pdf-accessibility-standards" target="_blank" rel="noopener">Typst</a>
.</p>
<h2 id="output-for-llms">Output for LLMs
</h2>
<p>Quarto can now generate 






<a href="https://llmstxt.org/" target="_blank" rel="noopener">llms.txt</a>
 format output for your website, making your content more accessible to large language models and AI-powered tools.</p>
<p>Enable it in your website configuration:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-1">
  <div class="code-with-filename-label" id="code-filename-1"><span class="font-mono text-sm">_quarto.yml</span></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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">website</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;My Documentation&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">llms-txt</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>When you render your site, Quarto creates:</p>
<ul>
<li>An <code>llms.txt</code> index file at the root of your site listing all pages</li>
<li>A <code>.llms.md</code> markdown file alongside each HTML page (e.g., <code>guide.html</code> gets <code>guide.llms.md</code>)</li>
</ul>
<p>The markdown files contain clean versions of your content&mdash;navigation, sidebars, and scripts are stripped out; tables, code blocks, and callouts are converted to standard markdown.</p>
<p>Read more, including how to customize what appears in LLM output, in 






<a href="https://quarto.org/docs/websites/website-llms.html" target="_blank" rel="noopener">Websites &gt; Output for LLMs</a>
.</p>
<h2 id="quarto-use-brand-command"><code>quarto use brand</code> Command
</h2>
<p>Keep your project&rsquo;s brand assets in sync with an external source using the new <code>quarto use brand</code> command:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-2">
  <div class="code-with-filename-label" id="code-filename-2"><span class="font-mono text-sm">Terminal</span></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></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 use brand myorg/shared-brand</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>The command copies brand files from a GitHub repository, local directory, or zip archive into your project&rsquo;s <code>_brand/</code> directory. Quarto walks you through each step&mdash;confirming trust for remote sources, creating the directory if needed, and asking whether to overwrite or remove files.</p>
<p>See 


  
  
  





<a href="https://quarto.org/docs/authoring/brand.html#quarto-use-brand" target="_blank" rel="noopener">Guide &gt; Brand</a>
 for <code>--dry-run</code>, <code>--force</code>, and other options.</p>
<h2 id="list-tables">List Tables
</h2>
<p>List tables provide a new syntax for creating tables with complex content&mdash;multiple paragraphs, code blocks, or nested lists&mdash;using familiar bullet syntax instead of grid table formatting:</p>
<div class="grid gap-12 items-start md:grid-cols-2">
<div class="prose max-w-none">
<div class="code-block"><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-markdown" data-lang="markdown"><span class="line"><span class="cl">::: {.list-table}
</span></span><span class="line"><span class="cl"><span class="k">-</span> - Function
</span></span><span class="line"><span class="cl">  <span class="k">-</span> Description
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> - <span class="sb">`sum()`</span>
</span></span><span class="line"><span class="cl">  <span class="k">-</span> Add values:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    ``<span class="sb">`python
</span></span></span><span class="line"><span class="cl"><span class="sb">    sum([1, 2, 3])
</span></span></span><span class="line"><span class="cl"><span class="sb">    `</span>`<span class="sb">`
</span></span></span><span class="line"><span class="cl"><span class="sb">
</span></span></span><span class="line"><span class="cl"><span class="sb">- - `</span>len()`
</span></span><span class="line"><span class="cl">  <span class="k">-</span> Count items:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">-</span> Works on lists
</span></span><span class="line"><span class="cl">    <span class="k">-</span> Works on strings
</span></span><span class="line"><span class="cl">:::</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<div class="prose max-w-none">
<table>
<thead>
<tr>
<th>Function</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><p><code>sum()</code></p></td>
<td><p>Add values:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="bu">sum</span>([<span class="dv">1</span>, <span class="dv">2</span>, <span class="dv">3</span>])</span></code></pre></div></td>
</tr>
<tr>
<td><p><code>len()</code></p></td>
<td><p>Count items:</p>
<ul>
<li>Works on lists</li>
<li>Works on strings</li>
</ul></td>
</tr>
</tbody>
</table>
</div>
</div>
<p>Each top-level bullet represents a row; nested bullets represent cells. This syntax is much easier to maintain than grid tables, especially when cells contain code or other block elements.</p>
<p>List tables support all the usual table features: captions, cross-references, column widths, and alignment. Thanks to Martin Fischer for the original development, with contributions from Albert Krewinkel and William Lupton.</p>
<p>Find all the details in 


  
  
  





<a href="https://quarto.org/docs/authoring/tables.html#list-tables" target="_blank" rel="noopener">Guide &gt; Tables</a>
.</p>
<h2 id="other-highlights">Other Highlights
</h2>
<ul>
<li>
<p>


  
  
  





<a href="https://quarto.org/docs/websites/website-search.html#search-result-highlighting" target="_blank" rel="noopener">Search Result Highlighting</a>
: Improved highlighting of search terms on destination pages, with persistent marks, automatic tab activation for matches inside tabsets, and cross-element highlighting for multi-word searches.</p>
</li>
<li>
<p>Privacy-focused features for websites:</p>
<ul>
<li>
<p>


  
  
  





<a href="https://quarto.org/docs/websites/website-tools.html#cookie-consent" target="_blank" rel="noopener">A privacy-first default for cookie consent</a>
: The default for cookie consent has changed to <code>type: express</code>, providing opt-in consent that blocks cookies until users explicitly agree. This privacy-conscious default is designed with modern privacy regulations in mind.</p>
</li>
<li>
<p>


  
  
  





<a href="https://quarto.org/docs/websites/website-search.html#cookie-consent-and-user-tracking" target="_blank" rel="noopener">Algolia Search Insights avoids cookies</a>
: Use Algolia Insights now uses persistent cookies only if <code>cookie-consent</code> is active, and the user has opted-in.</p>
</li>
<li>
<p>


  
  
  





<a href="https://quarto.org/docs/websites/website-tools.html#plausible-analytics" target="_blank" rel="noopener">Use Plausible Analytics</a>
: Add privacy-friendly Plausible Analytics to websites via the <code>plausible-analytics</code> configuration option.</p>
</li>
</ul>
</li>
<li>
<p>


  
  
  





<a href="https://quarto.org/docs/authoring/videos.html#accessibility-label" target="_blank" rel="noopener"><code>aria-label</code> for videos</a>
: Improve accessibility of embedded videos by providing custom descriptive labels for screen readers instead of the default &ldquo;Video Player&rdquo; label.</p>
</li>
<li>
<p>


  
  
  





<a href="https://quarto.org/docs/output-formats/pdf-basics.html#syntax-highlighting" target="_blank" rel="noopener">New <code>syntax-highlighting</code> Option</a>
: Replaces the deprecated <code>highlight-style</code> (Pandoc 3.8). Supports style names, custom <code>.theme</code> files, <code>none</code>, or <code>idiomatic</code> for format-native highlighting.</p>
</li>
<li>
<p>Metadata and brand extensions now work without a <code>_quarto.yml</code> project. A temporary default project is created in memory.</p>
</li>
<li>
<p>






<a href="https://quarto.org/docs/extensions/engine.html" target="_blank" rel="noopener">Engine extensions</a>
 allow replacement of the execution engine:</p>
<ul>
<li>Julia is now a bundled extension instead of being built-in.</li>
<li><strong>quarto-marimo</strong> will soon change from a filter extension to an engine extension.</li>
<li>New <code>quarto create extension engine</code> command.</li>
<li>New <code>quarto call build-ts-extension</code> command.</li>
<li>New <strong>Quarto API</strong> for engine extensions to use. (This is in flux and will not be documented for the next few releases, but 






<a href="https://quarto-dev.github.io/dev-notes/posts/2026-03-04/" target="_blank" rel="noopener">there is a dev blog post about it</a>
.)</li>
</ul>
</li>
</ul>
<p>Dependency updates:</p>
<ul>
<li><code>pandoc</code> updated to 3.8.3</li>
<li><code>typst</code> updated to 0.14.2</li>
<li><code>esbuild</code> updated to 0.25.10</li>
<li><code>deno</code> updated to 2.4.5</li>
<li><code>mermaid</code> updated to 11.12.0</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>One of the early proposals for PDF accessibility and alt text in the LaTeX ecosystem was provided to us by 






<a href="https://github.com/Schiano-NOAA" target="_blank" rel="noopener">Sam Schiano</a>
 and 






<a href="https://github.com/sbreitbart-NOAA" target="_blank" rel="noopener">Sophie Breitbart</a>
. We want to thank them for bringing into our attention the approach they used in their 






<a href="https://nmfs-ost.github.io/asar/" target="_blank" rel="noopener"><code>{asar}</code> R package</a>
, which influenced some of our design.</p>
<p>In addition, we&rsquo;d like to say a huge thank you to everyone who contributed to this release by opening issues and pull requests:</p>
<p>






<a href="https://github.com/CoryMcCartan" target="_blank" rel="noopener">CoryMcCartan</a>
,







<a href="https://github.com/DanChaltiel" target="_blank" rel="noopener">DanChaltiel</a>
,







<a href="https://github.com/Data-Wise" target="_blank" rel="noopener">Data-Wise</a>
,







<a href="https://github.com/FrankwaP" target="_blank" rel="noopener">FrankwaP</a>
,







<a href="https://github.com/Joao-O-Santos" target="_blank" rel="noopener">Joao-O-Santos</a>
,







<a href="https://github.com/LukasDSauer" target="_blank" rel="noopener">LukasDSauer</a>
,







<a href="https://github.com/MBe-iUS" target="_blank" rel="noopener">MBe-iUS</a>
,







<a href="https://github.com/MarcoPortmann" target="_blank" rel="noopener">MarcoPortmann</a>
,







<a href="https://github.com/MariaBarrioSchez" target="_blank" rel="noopener">MariaBarrioSchez</a>
,







<a href="https://github.com/MateusMolina" target="_blank" rel="noopener">MateusMolina</a>
,







<a href="https://github.com/Selbosh" target="_blank" rel="noopener">Selbosh</a>
,







<a href="https://github.com/ThePurox" target="_blank" rel="noopener">ThePurox</a>
,







<a href="https://github.com/TucoFernandes" target="_blank" rel="noopener">TucoFernandes</a>
,







<a href="https://github.com/aecoleman" target="_blank" rel="noopener">aecoleman</a>
,







<a href="https://github.com/amirhome61" target="_blank" rel="noopener">amirhome61</a>
,







<a href="https://github.com/andrewheiss" target="_blank" rel="noopener">andrewheiss</a>
,







<a href="https://github.com/azankl" target="_blank" rel="noopener">azankl</a>
,







<a href="https://github.com/bensoltoff" target="_blank" rel="noopener">bensoltoff</a>
,







<a href="https://github.com/bruvellu" target="_blank" rel="noopener">bruvellu</a>
,







<a href="https://github.com/byzheng" target="_blank" rel="noopener">byzheng</a>
,







<a href="https://github.com/cbrnr" target="_blank" rel="noopener">cbrnr</a>
,







<a href="https://github.com/chendaniely" target="_blank" rel="noopener">chendaniely</a>
,







<a href="https://github.com/chi-raag" target="_blank" rel="noopener">chi-raag</a>
,







<a href="https://github.com/christopherkenny" target="_blank" rel="noopener">christopherkenny</a>
,







<a href="https://github.com/coatless" target="_blank" rel="noopener">coatless</a>
,







<a href="https://github.com/cynthiahqy" target="_blank" rel="noopener">cynthiahqy</a>
,







<a href="https://github.com/darwindarak" target="_blank" rel="noopener">darwindarak</a>
,







<a href="https://github.com/davidskalinder" target="_blank" rel="noopener">davidskalinder</a>
,







<a href="https://github.com/dmenne" target="_blank" rel="noopener">dmenne</a>
,







<a href="https://github.com/fconil" target="_blank" rel="noopener">fconil</a>
,







<a href="https://github.com/fkgruber" target="_blank" rel="noopener">fkgruber</a>
,







<a href="https://github.com/fkohrt" target="_blank" rel="noopener">fkohrt</a>
,







<a href="https://github.com/fredguth" target="_blank" rel="noopener">fredguth</a>
,







<a href="https://github.com/gadenbuie" target="_blank" rel="noopener">gadenbuie</a>
,







<a href="https://github.com/apps/github-actions" target="_blank" rel="noopener">github-actions[bot]</a>
,







<a href="https://github.com/gsathler-vi" target="_blank" rel="noopener">gsathler-vi</a>
,







<a href="https://github.com/hamgamb" target="_blank" rel="noopener">hamgamb</a>
,







<a href="https://github.com/herosi" target="_blank" rel="noopener">herosi</a>
,







<a href="https://github.com/icarusz" target="_blank" rel="noopener">icarusz</a>
,







<a href="https://github.com/idavydov" target="_blank" rel="noopener">idavydov</a>
,







<a href="https://github.com/jeremy886" target="_blank" rel="noopener">jeremy886</a>
,







<a href="https://github.com/jkrumbiegel" target="_blank" rel="noopener">jkrumbiegel</a>
,







<a href="https://github.com/jmcphers" target="_blank" rel="noopener">jmcphers</a>
,







<a href="https://github.com/jonas37" target="_blank" rel="noopener">jonas37</a>
,







<a href="https://github.com/jorherre" target="_blank" rel="noopener">jorherre</a>
,







<a href="https://github.com/jreades" target="_blank" rel="noopener">jreades</a>
,







<a href="https://github.com/jromanowska" target="_blank" rel="noopener">jromanowska</a>
,







<a href="https://github.com/jtbayly" target="_blank" rel="noopener">jtbayly</a>
,







<a href="https://github.com/juleswg23" target="_blank" rel="noopener">juleswg23</a>
,







<a href="https://github.com/juliasilge" target="_blank" rel="noopener">juliasilge</a>
,







<a href="https://github.com/kathsherratt" target="_blank" rel="noopener">kathsherratt</a>
,







<a href="https://github.com/kusnezoff-alexander" target="_blank" rel="noopener">kusnezoff-alexander</a>
,







<a href="https://github.com/lrrichter" target="_blank" rel="noopener">lrrichter</a>
,







<a href="https://github.com/lwjohnst86" target="_blank" rel="noopener">lwjohnst86</a>
,







<a href="https://github.com/maelle" target="_blank" rel="noopener">maelle</a>
,







<a href="https://github.com/matthiasbaitsch" target="_blank" rel="noopener">matthiasbaitsch</a>
,







<a href="https://github.com/mipmip" target="_blank" rel="noopener">mipmip</a>
,







<a href="https://github.com/mstrms2000" target="_blank" rel="noopener">mstrms2000</a>
,







<a href="https://github.com/multimeric" target="_blank" rel="noopener">multimeric</a>
,







<a href="https://github.com/mvuorre" target="_blank" rel="noopener">mvuorre</a>
,







<a href="https://github.com/mykolaskrynnyk" target="_blank" rel="noopener">mykolaskrynnyk</a>
,







<a href="https://github.com/nichtich" target="_blank" rel="noopener">nichtich</a>
,







<a href="https://github.com/nithinmkp" target="_blank" rel="noopener">nithinmkp</a>
,







<a href="https://github.com/nrichers" target="_blank" rel="noopener">nrichers</a>
,







<a href="https://github.com/orbsmiv" target="_blank" rel="noopener">orbsmiv</a>
,







<a href="https://github.com/paytonej" target="_blank" rel="noopener">paytonej</a>
,







<a href="https://github.com/petrelharp" target="_blank" rel="noopener">petrelharp</a>
,







<a href="https://github.com/phongphuhanam" target="_blank" rel="noopener">phongphuhanam</a>
,







<a href="https://github.com/pm-gusmano" target="_blank" rel="noopener">pm-gusmano</a>
,







<a href="https://github.com/posit-snyk-bot" target="_blank" rel="noopener">posit-snyk-bot</a>
,







<a href="https://github.com/prosoitos" target="_blank" rel="noopener">prosoitos</a>
,







<a href="https://github.com/rabyj" target="_blank" rel="noopener">rabyj</a>
,







<a href="https://github.com/sasja-san" target="_blank" rel="noopener">sasja-san</a>
,







<a href="https://github.com/sbwiecko" target="_blank" rel="noopener">sbwiecko</a>
,







<a href="https://github.com/serialc" target="_blank" rel="noopener">serialc</a>
,







<a href="https://github.com/spaette" target="_blank" rel="noopener">spaette</a>
,







<a href="https://github.com/spraetor" target="_blank" rel="noopener">spraetor</a>
,







<a href="https://github.com/stragu" target="_blank" rel="noopener">stragu</a>
,







<a href="https://github.com/szimmer" target="_blank" rel="noopener">szimmer</a>
,







<a href="https://github.com/the-solipsist" target="_blank" rel="noopener">the-solipsist</a>
,







<a href="https://github.com/thomasp85" target="_blank" rel="noopener">thomasp85</a>
,







<a href="https://github.com/yyzeng" target="_blank" rel="noopener">yyzeng</a>
,







<a href="https://github.com/zhe00a" target="_blank" rel="noopener">zhe00a</a>
.</p>
<p>The airplane departure emoji in the 






<a href="thumbnail.png">listing and social card image</a>
 for this post comes from <a href="https://openmoji.org/" class="external">OpenMoji</a>&ndash; the open-source emoji and icon project. License: <a href="https://creativecommons.org/licenses/by-sa/4.0/#" class="external">CC BY-SA 4.0</a></p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-24_1.9-release/thumbnail.png" length="57935" type="image/png" />
    </item>
    <item>
      <title>2026 Posit Internships</title>
      <link>https://opensource.posit.co/blog/2026-03-20_2026-internships/</link>
      <pubDate>Fri, 20 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-20_2026-internships/</guid>
      <dc:creator>Max Kuhn</dc:creator>
      <dc:creator>Carson Sievert</dc:creator>
      <dc:creator>Tomasz Kalinowski</dc:creator>
      <dc:creator>Andrew Holz</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 are once again chuffed to offer summer internships.</p>
<p>Our internship program has been a great success over the years. If you want to know what it is like, many of our alumni have written about it:</p>
<ul>
<li>2016: 






<a href="https://www.data-imaginist.com/posts/2016-10-31-becoming-the-intern/" target="_blank" rel="noopener">Thomas Lin Pederson</a>
</li>
<li>2017: 






  
  

<a href="https://opensource.posit.co/blog/2017-09-08_lucy-internship/">Lucy D&rsquo;Agostino McGowan</a>
 and 






  
  

<a href="https://opensource.posit.co/blog/2017-09-08_ggplot2-internship/">Kara Woo</a>
</li>
<li>2018: 






<a href="https://www.alexpghayes.com/post/2018-08-10_a-summer-with-rstudio/" target="_blank" rel="noopener">Alex Hayes</a>
, 






<a href="https://fbchow.rbind.io/2018/07/27/rstudio-summer-internship/" target="_blank" rel="noopener">Fanny Chow</a>
, 






<a href="https://irene.rbind.io/post/summer-rstudio/" target="_blank" rel="noopener">Irene Steves</a>
, and 






<a href="https://www.danaseidel.com/2018-09-01-ATidySummer/" target="_blank" rel="noopener">Dana Paige Seidel</a>
</li>
<li>2019: 






  
  

<a href="https://opensource.posit.co/blog/2019-12-02_this-is-not-like-the-others/">Marly Gotti</a>
 and 






<a href="https://dewey.dunnington.ca/post/2019/a-summer-of-rstudio-and-ggplot2/" target="_blank" rel="noopener">Dewey Dunnington</a>
</li>
<li>2020: 






  
  

<a href="https://opensource.posit.co/blog/2020-06-30_tidymodels-internship/">Simon Couch</a>
</li>
<li>2022: 






<a href="https://www.mm218.dev/posts/2022-08-15-last-summer/" target="_blank" rel="noopener">Mike Mahoney</a>
</li>
<li>2025: 






<a href="https://franceslinyc.github.io/posts/2025-08-20-posit-internship/" target="_blank" rel="noopener">Frances Lin</a>
.</li>
</ul>
<p>Three past interns are current Posit employees: Thomas Lin Pederson, Kara Woo, and Simon Couch.</p>
<h2 id="2026-positions">2026 Positions
</h2>
<p>This year, we have four positions in different groups. The positions are US-based and range from 10-12 weeks, starting on May 26, 2026. See the link at the bottom for the details.</p>
<h3 id="skills-and-evals-intern-pydata-team">Skills and Evals Intern (PyData Team)
</h3>
<p>The PyData team is looking for an intern to help make AI agents better at using our Python open-source projects by writing skills and evaluations for common user tasks.</p>
<p>The core of the role is to identify the tasks users perform with our tools — such as 






<a href="https://plotnine.org/" target="_blank" rel="noopener">Plotnine</a>
 and 






<a href="https://posit-dev.github.io/great-tables/articles/intro.html" target="_blank" rel="noopener">Great Tables</a>
 — translate them into clear skill definitions that agents can use, and build evaluations that measure whether agents can reliably complete those tasks. This includes writing prompts, creating example workflows, and developing automated tests that measure how well agents perform. A major focus will be on applying the emerging skills format, while the broader goal is to improve documentation, examples, and API design across the PyData ecosystem in ways that make our tools work better with AI-assisted workflows.</p>
<h3 id="r-modeling-intern-tidymodels-team">R Modeling Intern (Tidymodels Team)
</h3>
<p>The 






<a href="https://www.tidymodels.org/" target="_blank" rel="noopener">tidymodels</a>
 R internship is focused on different tasks, including: expand content on 






<a href="https://www.tidymodels.org/" target="_blank" rel="noopener">tidymodels.org</a>
, expanding tabular deep learning models (in 






<a href="https://brulee.tidymodels.org/" target="_blank" rel="noopener">brulee</a>
), additional performance metrics for 


  
  
  





<a href="https://www.tidymodels.org/learn/#category=survival%20analysis" target="_blank" rel="noopener">survival analysis models</a>
, modernizing the 






<a href="https://topepo.github.io/caret/" target="_blank" rel="noopener">caret package</a>
, and/or Rust bindings for predictive models. The intern is welcome to suggest R-based projects focused on modeling and/or data analysis.</p>
<h3 id="shiny-accessibility-and-testing-intern-shiny-team">Shiny Accessibility and Testing Intern (Shiny Team)
</h3>
<p>The 






<a href="https://shiny.posit.co/" target="_blank" rel="noopener">Shiny</a>
 team is looking for an intern to help advance accessibility and testing across the Shiny framework. You&rsquo;ll audit Shiny components against Web Content Accessibility Guidelines (WCAG), implement fixes, improve test coverage, and contribute to documentation that helps the broader community build accessible Shiny apps.</p>
<p>Some of the harder problems in this role aren&rsquo;t strictly code problems. Shiny&rsquo;s components are built for flexible, abstract usage, so you can&rsquo;t always anticipate how they&rsquo;ll end up on a page. Making them accessible means understanding HTML semantics and WCAG well enough to exercise good judgment and make sensible compromises when there isn&rsquo;t one clear right answer. Candidates should be comfortable with Git and GitHub, have solid working knowledge of HTML/CSS, and have experience in at least one of R, Python, or JavaScript. Familiarity with WCAG, assistive technology, automated testing frameworks, or open-source workflows is a plus.</p>
<h3 id="software-engineering-intern-posit-connect-team">Software Engineering Intern (Posit Connect Team)
</h3>
<p>The 






<a href="https://posit.co/products/enterprise/connect/" target="_blank" rel="noopener">Posit Connect</a>
 team is looking for an intern to contribute to the development and quality of Connect, Posit&rsquo;s professional platform for publishing and sharing data science and AI applications at scale. The primary focus of the internship will be to contribute reports and applications to the 






<a href="https://docs.posit.co/connect/user/publishing-connect-gallery/" target="_blank" rel="noopener">Connect Gallery</a>
, an open-source collection of useful extensions and example content. These Python, R, and 






<a href="https://quarto.org/" target="_blank" rel="noopener">Quarto</a>
 projects help data science teams realize the full potential of the product and allow us to experiment with new features. In the process of building these apps, you will have the opportunity to contribute to the Connect product as well.</p>
<h2 id="applying">Applying
</h2>
<p>To apply, make sure that you have a GitHub handle and follow this link:</p>
<p><strong>






<a href="https://posit.co/job-detail/?gh_jid=7674250003" target="_blank" rel="noopener"><code>https://posit.co/job-detail/?gh_jid=7674250003</code></a>
</strong></p>
<p>We can&rsquo;t wait to get started and look forward to reading your applications.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-20_2026-internships/thumbnail-wd.jpg" length="36135" type="image/jpeg" />
    </item>
    <item>
      <title>Native Jupyter Notebook Support Has Arrived in Positron</title>
      <link>https://opensource.posit.co/blog/2026-03-16_notebooks-march-announcement/</link>
      <pubDate>Mon, 16 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-16_notebooks-march-announcement/</guid>
      <dc:creator>Cindy Tong</dc:creator><description><![CDATA[<p>Positron now ships with a native 






<a href="https://positron.posit.co/positron-notebook-editor.html" target="_blank" rel="noopener">Jupyter Notebook Editor</a>
, a new unified experience we built from the ground up for working with Jupyter notebooks within Positron.</p>
<h2 id="why-we-built-our-own-notebook-editor">Why we built our own notebook editor
</h2>
<p>We built the Positron Notebook Editor to treat your .ipynb files as first-class citizens in an IDE tailored specifically for data science workflows.</p>
<p>Up to this point, Positron used the 






<a href="https://positron.posit.co/legacy-notebook-editor.html" target="_blank" rel="noopener">legacy Code OSS notebook editor</a>
 that powers VS Code. While functional, this editor was designed for general-purpose development and not specifically for data science workflows. The tradeoffs show up in small ways that compound over time: limited context for AI assistance, no deep integration with your variables or data, and a user experience that treats <code>.ipynb</code> files as just another file type.</p>
<p>We wanted notebooks to feel like a first-class part of a data science IDE, so we built our own native notebook editor.</p>
<p>If you missed the 






<a href="https://posit.co/blog/announcing-the-positron-notebook-editor-for-jupyter-notebooks/" target="_blank" rel="noopener">original February announcement</a>
, that post covers our initial reasoning in more detail.</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-03-16_notebooks-march-announcement/positron-notebook.png" alt="Positron Notebook Editor" />
<figcaption aria-hidden="true">Positron Notebook Editor</figcaption>
</figure>
<h2 id="whats-included-out-of-the-box">What&rsquo;s included out of the box
</h2>
<p>The Positron Notebook Editor brings the core capabilities of Positron directly into your notebook workflow:</p>
<p><strong>






<a href="https://positron.posit.co/variables-pane.html" target="_blank" rel="noopener">Variables Pane</a>
</strong>: Variables update in real time as you run cells. No need to print or inspect manually.</p>
<p><strong>






<a href="https://positron.posit.co/data-explorer.html" target="_blank" rel="noopener">Data Explorer</a>
</strong>: When a cell returns a Pandas or Polars DataFrame, you get an inline data viewer. Open the full Data Explorer to sort, filter, and profile your data. Any filtering or cleaning you do can be converted into code, so your analysis stays reproducible without writing repetitive <code>df.head()</code> or <code>df.describe()</code> calls.</p>























  
  
    <div class="w-full aspect-video">
      <video
        src="https://positron.posit.co/videos/notebook-data-explorer-variables.mp4"
        class="w-full h-full object-contain"
        
        controls></video>
    </div>
  




<p><strong>






<a href="https://positron.posit.co/assistant.html" target="_blank" rel="noopener">AI Assistant</a>
</strong>: The Assistant has access to your notebook&rsquo;s full context, including cell states, execution history, and outputs like images and tables. It can suggest edits, reorder cells, and run code with your permission. You can inspect exactly what context it&rsquo;s using and follow along as it works.</p>























  
  
    <div class="w-full aspect-video">
      <video
        src="https://positron.posit.co/videos/notebook-ai-quick-actions.mp4"
        class="w-full h-full object-contain"
        
        controls></video>
    </div>
  




<p><strong>






<a href="https://positron.posit.co/help-pane.html" target="_blank" rel="noopener">Help Pane</a>
</strong>: Python and R documentation is available inline, with hyperlinks, without switching to a browser.</p>























  
  
    <div class="w-full aspect-video">
      <video
        src="https://positron.posit.co/videos/notebook-help.mp4"
        class="w-full h-full object-contain"
        
        controls></video>
    </div>
  




<p><strong>






<a href="https://positron.posit.co/publish-to-connect.html" target="_blank" rel="noopener">Publisher</a>
</strong>: Deploy your <code>.ipynb</code> notebooks directly to Connect or Connect Cloud, where you can manage access, schedule runs, and view telemetry.</p>























  
  
    <div class="w-full aspect-video">
      <video
        src="https://positron.posit.co/videos/notebook-publish-connect.mp4"
        class="w-full h-full object-contain"
        
        controls></video>
    </div>
  




<h2 id="a-sample-notebook-workflow">A sample notebook workflow
</h2>
<p>Now that you have all these capabilities in one place, your workflow might look something like this:</p>
<ol>
<li>Import your data using Pandas or Polars.</li>
<li>Run your notebook cells and watch variables update in the pane as cells run.</li>
<li>Explore your DataFrame in the inline Data Explorer. Sort and filter without writing any code.</li>
<li>Use Assistant to generate a visualization based on your filtered data or AI quick actions to recommend next steps.</li>
<li>When the analysis is ready to share, use an AI action to add markdown headers and notes.</li>
<li>Publish the notebook to Connect or Connect Cloud to share with your colleagues.</li>
</ol>
<h2 id="whats-coming-next">What&rsquo;s coming next
</h2>
<p>The roadmap includes SQL support, improved version control, R improvements, and more. You can view and vote on items in the 






<a href="https://github.com/posit-dev/positron/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22area%3A%20notebooks-jupyter%22" target="_blank" rel="noopener">GitHub roadmap</a>
.</p>
<h2 id="get-started-with-the-alpha">Get started with the alpha
</h2>
<ol>
<li>






<a href="https://positron.posit.co/download.html" target="_blank" rel="noopener">Download Positron</a>
 and install a release from February 2026 or later.</li>
<li>Enable the alpha by setting 






<a href="positron://settings/positron.notebook.enabled"><code>positron.notebook.enabled</code></a>
 to <code>true</code> in your settings.</li>
<li>Try the 






<a href="https://github.com/posit-dev/positron-demos-notebooks" target="_blank" rel="noopener">tutorial repository</a>
 for examples that use the new features.</li>
<li>Share feedback in 






<a href="https://github.com/posit-dev/positron/discussions" target="_blank" rel="noopener">GitHub Discussions</a>
 or 






<a href="https://scheduler.zoom.us/cindy-tong/improving-the-positron-notebook-experience" target="_blank" rel="noopener">book time to talk with us directly</a>
.</li>
</ol>
<p>We&rsquo;re excited to hear how you use the Positron Notebook Editor as we continuously improve the experience.</p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2026-03-16_notebooks-march-announcement/positron-notebook.png" length="444187" type="image/png" />
    </item>
    <item>
      <title>Recreating Septa Transit Timetables in Python</title>
      <link>https://opensource.posit.co/blog/2026-03-12_septa-timetables/</link>
      <pubDate>Thu, 12 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-12_septa-timetables/</guid>
      <dc:creator>Michael Chow</dc:creator>
      <dc:creator>Rich Iannone</dc:creator><description><![CDATA[<script src="https://cdn.jsdelivr.net/npm/requirejs@2.3.6/require.min.js" integrity="sha384-c9c+LnTbwQ3aujuU7ULEPVvgLs+Fn6fJUvIGTsuu1ZcCf11fiEubah0ttpca4ntM sha384-6V1/AdqZRWk1KAlWbKBlGhN7VG4iE/yAZcO6NZPMF8od0vukrvr0tg4qY6NSrItx" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js" integrity="sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2" crossorigin="anonymous" data-relocate-top="true"></script>
<script type="application/javascript">define('jquery', [],function() {return window.jQuery;})</script>
<p>Recently, Rich and I were poking around transit data, and we were struck by the amount of structuring that goes into transit timetables.</p>
<p>For example, consider this weekend rail schedule table from SEPTA, Philadelphia&rsquo;s transit agency.</p>
<img src="https://opensource.posit.co/blog/2026-03-12_septa-timetables/./example-timetable.png" style="max-width: 700px; display: block; margin-left: auto; margin-right: auto;" />
<p>Notice these big pieces:</p>
<ul>
<li>The vertical text on the left indicating trains are traveling &ldquo;TO CENTER CITY&rdquo;.</li>
<li>The blue header, and spanner columns (&ldquo;Services&rdquo; and &ldquo;Train Number&rdquo;) grouping related columns.</li>
<li>The striped background for easier reading. Also the black background indicating stations in Center City (the urban core).</li>
</ul>
<p>Tables like this often have to be created in tools like Illustrator, and updated by hand. At the same time, when agencies automate table creation, they often sacrifice a lot of the assistive features and helpful affordances of the table.</p>
<p>We set out to recreate this table in Great Tables (and by we I mean 99% Rich). In this post, I&rsquo;ll walk quickly through how we recreated it, and share some other examples of transit timetables in the wild. For the theory behind why tables like this are useful, see 






  
  
    
  

<a href="https://opensource.posit.co/blog/2024-04-04_design-philosophy/">The Design Philosophy of Great Tables</a>
.</p>
<h2 id="the-final-result">The final result
</h2>
<p>Here&rsquo;s a look at our quick version in Great Tables.
In this post we&rsquo;ll walk through quickly how we created it, but wanted to treat you to the final result up front! Note that the table is fully in HTML for accessibility.</p>
<details class="code-fold">
<summary>Code</summary>
<div class="code-block"><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></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">great_tables</span> <span class="kn">import</span> <span class="n">GT</span><span class="p">,</span> <span class="n">html</span><span class="p">,</span> <span class="n">style</span><span class="p">,</span> <span class="n">loc</span><span class="p">,</span> <span class="n">google_font</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars</span> <span class="k">as</span> <span class="nn">pl</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars.selectors</span> <span class="k">as</span> <span class="nn">cs</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">stops</span> <span class="o">=</span> <span class="n">pl</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s2">&#34;chw-stops.csv&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">times</span> <span class="o">=</span> <span class="n">pl</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s2">&#34;times.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="n">stop_times</span> <span class="o">=</span> <span class="n">times</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">other</span><span class="o">=</span><span class="n">stops</span><span class="p">,</span> <span class="n">on</span><span class="o">=</span><span class="s2">&#34;stop_name&#34;</span><span class="p">,</span> <span class="n">maintain_order</span><span class="o">=</span><span class="s2">&#34;left&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">select</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">pl</span><span class="o">.</span><span class="n">lit</span><span class="p">(</span><span class="s2">&#34;To Center City&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">alias</span><span class="p">(</span><span class="s2">&#34;direction&#34;</span><span class="p">),</span> <span class="n">pl</span><span class="o">.</span><span class="n">col</span><span class="p">(</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></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">h_m_p</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">h</span><span class="p">,</span> <span class="n">m</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="p">[</span><span class="nb">int</span><span class="p">(</span><span class="n">part</span><span class="p">)</span> <span class="k">for</span> <span class="n">part</span> <span class="ow">in</span> <span class="n">s</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;:&#34;</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">    <span class="n">ap</span> <span class="o">=</span> <span class="s2">&#34;a&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">h</span> <span class="o">&gt;</span> <span class="mi">12</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">h</span> <span class="o">-=</span> <span class="mi">12</span>
</span></span><span class="line"><span class="cl">        <span class="n">ap</span> <span class="o">=</span> <span class="s2">&#34;p&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">h</span><span class="si">}</span><span class="s2">:</span><span class="si">{</span><span class="n">m</span><span class="si">:</span><span class="s2">02d</span><span class="si">}{</span><span class="n">ap</span><span class="si">}</span><span class="s2">&#34;</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 class="k">def</span> <span class="nf">tick</span><span class="p">(</span><span class="n">b</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;&amp;check;&#34;</span> <span class="k">if</span> <span class="n">b</span> <span class="k">else</span> <span class="s2">&#34;&#34;</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 class="n">transit_table</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">stop_times</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_stub</span><span class="p">(</span><span class="n">groupname_col</span><span class="o">=</span><span class="s2">&#34;direction&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_header</span><span class="p">(</span><span class="s2">&#34;Saturdays, Sundays, and Major Holidays&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_hide</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;stop_url&#34;</span><span class="p">,</span> <span class="s2">&#34;zone_id&#34;</span><span class="p">,</span> <span class="s2">&#34;stop_desc&#34;</span><span class="p">,</span> <span class="s2">&#34;stop_lat&#34;</span><span class="p">,</span> <span class="s2">&#34;stop_lon&#34;</span><span class="p">,</span> <span class="s2">&#34;stop_id&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt</span><span class="p">(</span><span class="n">h_m_p</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="n">cs</span><span class="o">.</span><span class="n">matches</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^[0-9]</span><span class="si">{4}</span><span class="s2">$&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt</span><span class="p">(</span><span class="n">tick</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="n">cs</span><span class="o">.</span><span class="n">starts_with</span><span class="p">(</span><span class="s2">&#34;service_&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_label</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">stop_name</span><span class="o">=</span><span class="s2">&#34;Stations&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">service_access</span><span class="o">=</span><span class="s2">&#34;A&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">service_cash</span><span class="o">=</span><span class="s2">&#34;C&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">service_park</span><span class="o">=</span><span class="s2">&#34;P&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">fare_zone</span><span class="o">=</span><span class="n">html</span><span class="p">(</span><span class="s2">&#34;Fare&lt;br&gt;Zone&#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="o">.</span><span class="n">tab_spanner</span><span class="p">(</span><span class="n">label</span><span class="o">=</span><span class="s2">&#34;Services&#34;</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="n">cs</span><span class="o">.</span><span class="n">starts_with</span><span class="p">(</span><span class="s2">&#34;service_&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_spanner</span><span class="p">(</span><span class="n">label</span><span class="o">=</span><span class="s2">&#34;Train Number&#34;</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="n">cs</span><span class="o">.</span><span class="n">matches</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^[0-9]</span><span class="si">{4}</span><span class="s2">$&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_move_to_start</span><span class="p">(</span><span class="s2">&#34;fare_zone&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_move_to_start</span><span class="p">(</span><span class="n">cs</span><span class="o">.</span><span class="n">starts_with</span><span class="p">(</span><span class="s2">&#34;service_&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_width</span><span class="p">(</span><span class="n">cases</span><span class="o">=</span><span class="p">{</span><span class="n">c</span><span class="p">:</span> <span class="s2">&#34;20px&#34;</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">stop_times</span><span class="o">.</span><span class="n">columns</span> <span class="k">if</span> <span class="n">c</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;service_&#34;</span><span class="p">)})</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_width</span><span class="p">(</span><span class="n">cases</span><span class="o">=</span><span class="p">{</span><span class="n">c</span><span class="p">:</span> <span class="s2">&#34;60px&#34;</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">stop_times</span><span class="o">.</span><span class="n">columns</span> <span class="k">if</span> <span class="n">c</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;8&#34;</span><span class="p">)})</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">opt_row_striping</span><span class="p">(</span><span class="n">row_striping</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_align</span><span class="p">(</span><span class="n">align</span><span class="o">=</span><span class="s2">&#34;center&#34;</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="s2">&#34;fare_zone&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_align</span><span class="p">(</span><span class="n">align</span><span class="o">=</span><span class="s2">&#34;right&#34;</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="n">cs</span><span class="o">.</span><span class="n">matches</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^[0-9]</span><span class="si">{4}</span><span class="s2">$&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># style header</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_style</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">header</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="n">style</span><span class="o">=</span><span class="n">style</span><span class="o">.</span><span class="n">css</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;background-color: rgb(66, 99, 128) !important; color: white !important; font-size: 24px !important; font-weight: bold !important; border-width: 0px !important;&#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="c1"># style vertical text on left</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_style</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">row_groups</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># TODO: rotate text vertically</span>
</span></span><span class="line"><span class="cl">        <span class="n">style</span><span class="o">=</span><span class="n">style</span><span class="o">.</span><span class="n">css</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;writing-mode: sideways-lr; padding-bottom: 25% !important; font-size: 24px !important; font-weight: bold !important; text-transform: uppercase !important;&#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="o">.</span><span class="n">tab_style</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">style</span><span class="o">=</span><span class="n">style</span><span class="o">.</span><span class="n">css</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important;&#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">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">body</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">rows</span><span class="o">=</span><span class="nb">list</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="o">-</span><span class="mi">4</span><span class="p">,</span> <span class="o">-</span><span class="mi">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="o">.</span><span class="n">tab_style</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">style</span><span class="o">=</span><span class="n">style</span><span class="o">.</span><span class="n">css</span><span class="p">(</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="s2">                border-top: none !important;
</span></span></span><span class="line"><span class="cl"><span class="s2">                border-bottom: none !important;
</span></span></span><span class="line"><span class="cl"><span class="s2">                border-right: solid white 2px !important;
</span></span></span><span class="line"><span class="cl"><span class="s2">                color: white !important;
</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="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">body</span><span class="p">(</span><span class="n">columns</span><span class="o">=~</span><span class="n">cs</span><span class="o">.</span><span class="n">matches</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^[0-9]</span><span class="si">{4}</span><span class="s2">$&#34;</span><span class="p">),</span> <span class="n">rows</span><span class="o">=</span><span class="nb">list</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="o">-</span><span class="mi">4</span><span class="p">,</span> <span class="o">-</span><span class="mi">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="o">.</span><span class="n">tab_style</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">style</span><span class="o">=</span><span class="n">style</span><span class="o">.</span><span class="n">css</span><span class="p">(</span><span class="s2">&#34;border-right: solid black 2px !important;&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">body</span><span class="p">(</span><span class="n">columns</span><span class="o">=~</span><span class="n">cs</span><span class="o">.</span><span class="n">matches</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^[0-9]</span><span class="si">{4}</span><span class="s2">$&#34;</span><span class="p">),</span> <span class="n">rows</span><span class="o">=</span><span class="nb">list</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">))</span> <span class="o">+</span> <span class="p">[</span><span class="mi">13</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="o">.</span><span class="n">tab_options</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">row_striping_background_color</span><span class="o">=</span><span class="s2">&#34;#A9A9A9&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">row_group_as_column</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="o">.</span><span class="n">opt_table_outline</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">opt_table_font</span><span class="p">(</span><span class="n">font</span><span class="o">=</span><span class="n">google_font</span><span class="p">(</span><span class="s2">&#34;IBM Plex Sans&#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">transit_table</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
</details>
<div id="ratqybkrxx" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans&display=swap');
#ratqybkrxx table {
          font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', 'Fira Sans', 'Droid Sans', Arial, sans-serif;
          -webkit-font-smoothing: antialiased;
          -moz-osx-font-smoothing: grayscale;
        }
<p>#ratqybkrxx thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#ratqybkrxx p { margin: 0; padding: 0; }
#ratqybkrxx .gt_table { 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: 3px; border-top-color: #D3D3D3; border-right-style: solid; border-right-width: 3px; border-right-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 3px; border-bottom-color: #D3D3D3; border-left-style: solid; border-left-width: 3px; border-left-color: #D3D3D3; }
#ratqybkrxx .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#ratqybkrxx .gt_title { color: #333333; font-size: 125%; font-weight: initial; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; border-bottom-color: #FFFFFF; border-bottom-width: 0; }
#ratqybkrxx .gt_subtitle { color: #333333; font-size: 85%; font-weight: initial; padding-top: 3px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; border-top-color: #FFFFFF; border-top-width: 0; }
#ratqybkrxx .gt_heading { background-color: #FFFFFF; text-align: center; border-bottom-color: #FFFFFF; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; }
#ratqybkrxx .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#ratqybkrxx .gt_col_headings { 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; }
#ratqybkrxx .gt_col_heading { 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: 5px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; }
#ratqybkrxx .gt_column_spanner_outer { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; padding-top: 0; padding-bottom: 0; padding-left: 4px; padding-right: 4px; }
#ratqybkrxx .gt_column_spanner_outer:first-child { padding-left: 0; }
#ratqybkrxx .gt_column_spanner_outer:last-child { padding-right: 0; }
#ratqybkrxx .gt_column_spanner { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 5px; overflow-x: hidden; display: inline-block; width: 100%; }
#ratqybkrxx .gt_spanner_row { border-bottom-style: hidden; }
#ratqybkrxx .gt_group_heading { padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; 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; vertical-align: middle; text-align: left; }
#ratqybkrxx .gt_empty_group_heading { padding: 0.5px; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; vertical-align: middle; }
#ratqybkrxx .gt_from_md&gt; :first-child { margin-top: 0; }
#ratqybkrxx .gt_from_md&gt; :last-child { margin-bottom: 0; }
#ratqybkrxx .gt_row { 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; }
#ratqybkrxx .gt_stub { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #D3D3D3; padding-left: 5px; padding-right: 5px; }
#ratqybkrxx .gt_stub_row_group { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #D3D3D3; padding-left: 5px; padding-right: 5px; vertical-align: top; }
#ratqybkrxx .gt_row_group_first td { border-top-width: 2px; }
#ratqybkrxx .gt_row_group_first th { border-top-width: 2px; }
#ratqybkrxx .gt_striped { color: #333333; background-color: #A9A9A9; }
#ratqybkrxx .gt_table_body { border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#ratqybkrxx .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#ratqybkrxx .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#ratqybkrxx .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#ratqybkrxx .gt_sourcenotes { color: #333333; background-color: #FFFFFF; border-bottom-style: none; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; }
#ratqybkrxx .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#ratqybkrxx .gt_left { text-align: left; }
#ratqybkrxx .gt_center { text-align: center; }
#ratqybkrxx .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#ratqybkrxx .gt_font_normal { font-weight: normal; }
#ratqybkrxx .gt_font_bold { font-weight: bold; }
#ratqybkrxx .gt_font_italic { font-style: italic; }
#ratqybkrxx .gt_super { font-size: 65%; }
#ratqybkrxx .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#ratqybkrxx .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table class="gt_table" data-quarto-postprocess="true" style="table-layout: fixed;" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="14" class="gt_heading gt_title gt_font_normal" style="background-color: rgb(66, 99, 128) !important; color: white !important; font-size: 24px !important; font-weight: bold !important; border-width: 0px !important">Saturdays, Sundays, and Major Holidays</th>
</tr>
<tr class="gt_col_headings gt_spanner_row">
<th rowspan="2" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col"></th>
<th colspan="3" id="Services" class="gt_center gt_columns_top_border gt_column_spanner_outer" data-quarto-table-cell-role="th" scope="colgroup"><span class="gt_column_spanner">Services</span></th>
<th rowspan="2" id="fare_zone" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" scope="col">Fare<br />
Zone</th>
<th rowspan="2" id="stop_name" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">Stations</th>
<th colspan="8" id="Train-Number" class="gt_center gt_columns_top_border gt_column_spanner_outer" data-quarto-table-cell-role="th" scope="colgroup"><span class="gt_column_spanner">Train Number</span></th>
</tr>
<tr class="gt_col_headings">
<th id="service_access" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">A</th>
<th id="service_cash" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">C</th>
<th id="service_park" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">P</th>
<th id="8210" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8210</th>
<th id="8716" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8716</th>
<th id="8318" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8318</th>
<th id="8322" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8322</th>
<th id="8338" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8338</th>
<th id="8242" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8242</th>
<th id="8750" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8750</th>
<th id="8756" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8756</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td rowspan="14" class="gt_row gt_left gt_stub_row_group" data-quarto-table-cell-role="th" style="writing-mode: sideways-lr; padding-bottom: 25% !important; font-size: 24px !important; font-weight: bold !important; text-transform: uppercase !important">To Center City</td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_center" style="border-right: solid black 2px !important">2</td>
<td class="gt_row gt_left" style="border-right: solid black 2px !important">Chestnut Hill West</td>
<td class="gt_row gt_right">6:51a</td>
<td class="gt_row gt_right">8:08a</td>
<td class="gt_row gt_right">8:49a</td>
<td class="gt_row gt_right">9:49a</td>
<td class="gt_row gt_right">1:52p</td>
<td class="gt_row gt_right">2:49p</td>
<td class="gt_row gt_right">4:48p</td>
<td class="gt_row gt_right">6:20p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_center gt_striped" style="border-right: solid black 2px !important">2</td>
<td class="gt_row gt_left gt_striped" style="border-right: solid black 2px !important">Highland</td>
<td class="gt_row gt_right gt_striped">6:52a</td>
<td class="gt_row gt_right gt_striped">8:09a</td>
<td class="gt_row gt_right gt_striped">8:50a</td>
<td class="gt_row gt_right gt_striped">9:50a</td>
<td class="gt_row gt_right gt_striped">1:53p</td>
<td class="gt_row gt_right gt_striped">2:50p</td>
<td class="gt_row gt_right gt_striped">4:49p</td>
<td class="gt_row gt_right gt_striped">6:21p</td>
</tr>
<tr>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_center" style="border-right: solid black 2px !important">1</td>
<td class="gt_row gt_left" style="border-right: solid black 2px !important">St. Martins</td>
<td class="gt_row gt_right">6:54a</td>
<td class="gt_row gt_right">8:11a</td>
<td class="gt_row gt_right">8:52a</td>
<td class="gt_row gt_right">9:52a</td>
<td class="gt_row gt_right">1:55p</td>
<td class="gt_row gt_right">2:52p</td>
<td class="gt_row gt_right">4:51p</td>
<td class="gt_row gt_right">6:23p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_center gt_striped" style="border-right: solid black 2px !important">1</td>
<td class="gt_row gt_left gt_striped" style="border-right: solid black 2px !important">Richard Allen Lane</td>
<td class="gt_row gt_right gt_striped">6:56a</td>
<td class="gt_row gt_right gt_striped">8:13a</td>
<td class="gt_row gt_right gt_striped">8:54a</td>
<td class="gt_row gt_right gt_striped">9:54a</td>
<td class="gt_row gt_right gt_striped">1:57p</td>
<td class="gt_row gt_right gt_striped">2:54p</td>
<td class="gt_row gt_right gt_striped">4:53p</td>
<td class="gt_row gt_right gt_striped">6:25p</td>
</tr>
<tr>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_center" style="border-right: solid black 2px !important">1</td>
<td class="gt_row gt_left" style="border-right: solid black 2px !important">Carpenter</td>
<td class="gt_row gt_right">6:58a</td>
<td class="gt_row gt_right">8:15a</td>
<td class="gt_row gt_right">8:56a</td>
<td class="gt_row gt_right">9:56a</td>
<td class="gt_row gt_right">1:59p</td>
<td class="gt_row gt_right">2:56p</td>
<td class="gt_row gt_right">4:55p</td>
<td class="gt_row gt_right">6:27p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_center gt_striped" style="border-right: solid black 2px !important">1</td>
<td class="gt_row gt_left gt_striped" style="border-right: solid black 2px !important">Upsal</td>
<td class="gt_row gt_right gt_striped">7:00a</td>
<td class="gt_row gt_right gt_striped">8:17a</td>
<td class="gt_row gt_right gt_striped">8:58a</td>
<td class="gt_row gt_right gt_striped">9:58a</td>
<td class="gt_row gt_right gt_striped">2:01p</td>
<td class="gt_row gt_right gt_striped">2:58p</td>
<td class="gt_row gt_right gt_striped">4:57p</td>
<td class="gt_row gt_right gt_striped">6:29p</td>
</tr>
<tr>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_center" style="border-right: solid black 2px !important">C</td>
<td class="gt_row gt_left" style="border-right: solid black 2px !important">Tulpehocken</td>
<td class="gt_row gt_right">7:02a</td>
<td class="gt_row gt_right">8:19a</td>
<td class="gt_row gt_right">9:00a</td>
<td class="gt_row gt_right">10:00a</td>
<td class="gt_row gt_right">2:03p</td>
<td class="gt_row gt_right">3:00p</td>
<td class="gt_row gt_right">4:59p</td>
<td class="gt_row gt_right">6:31p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_center gt_striped" style="border-right: solid black 2px !important">C</td>
<td class="gt_row gt_left gt_striped" style="border-right: solid black 2px !important">Chelten Avenue</td>
<td class="gt_row gt_right gt_striped">7:04a</td>
<td class="gt_row gt_right gt_striped">8:21a</td>
<td class="gt_row gt_right gt_striped">9:02a</td>
<td class="gt_row gt_right gt_striped">10:02a</td>
<td class="gt_row gt_right gt_striped">2:05p</td>
<td class="gt_row gt_right gt_striped">3:02p</td>
<td class="gt_row gt_right gt_striped">5:01p</td>
<td class="gt_row gt_right gt_striped">6:33p</td>
</tr>
<tr>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_center" style="border-right: solid black 2px !important">C</td>
<td class="gt_row gt_left" style="border-right: solid black 2px !important">Queen Lane</td>
<td class="gt_row gt_right">7:06a</td>
<td class="gt_row gt_right">8:23a</td>
<td class="gt_row gt_right">9:04a</td>
<td class="gt_row gt_right">10:04a</td>
<td class="gt_row gt_right">2:07p</td>
<td class="gt_row gt_right">3:04p</td>
<td class="gt_row gt_right">5:03p</td>
<td class="gt_row gt_right">6:35p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_center gt_striped" style="border-right: solid black 2px !important">C</td>
<td class="gt_row gt_left gt_striped" style="border-right: solid black 2px !important">North Philadelphia</td>
<td class="gt_row gt_right gt_striped">7:12a</td>
<td class="gt_row gt_right gt_striped">8:29a</td>
<td class="gt_row gt_right gt_striped">9:12a</td>
<td class="gt_row gt_right gt_striped">10:12a</td>
<td class="gt_row gt_right gt_striped">2:15p</td>
<td class="gt_row gt_right gt_striped">3:12p</td>
<td class="gt_row gt_right gt_striped">5:09p</td>
<td class="gt_row gt_right gt_striped">6:41p</td>
</tr>
<tr>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">✓</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important"></td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">✓</td>
<td class="gt_row gt_center" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">2</td>
<td class="gt_row gt_left" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">Gray 30th Street</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">7:23a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">8:42a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">9:23a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">10:23a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">2:26p</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">3:23p</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">5:20p</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">6:54p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important"></td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important"></td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">✓</td>
<td class="gt_row gt_center gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">2</td>
<td class="gt_row gt_left gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">Suburban Station</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">7:28a</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">8:47a</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">9:28a</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">10:28a</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">2:31p</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">3:28p</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">5:25p</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">6:59p</td>
</tr>
<tr>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important"></td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important"></td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">✓</td>
<td class="gt_row gt_center" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">2</td>
<td class="gt_row gt_left" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">Jefferson Station</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">7:33a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">8:52a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">9:33a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">10:33a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">2:36p</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">3:33p</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">5:30p</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">7:04p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_center gt_striped" style="border-right: solid black 2px !important">2</td>
<td class="gt_row gt_left gt_striped" style="border-right: solid black 2px !important">Temple University</td>
<td class="gt_row gt_right gt_striped">7:37a</td>
<td class="gt_row gt_right gt_striped">8:57a</td>
<td class="gt_row gt_right gt_striped">9:37a</td>
<td class="gt_row gt_right gt_striped">10:37a</td>
<td class="gt_row gt_right gt_striped">2:40p</td>
<td class="gt_row gt_right gt_striped">3:37p</td>
<td class="gt_row gt_right gt_striped">5:35p</td>
<td class="gt_row gt_right gt_striped">7:08p</td>
</tr>
</tbody>
</table>
</div>
<h2 id="reading-in-stops-and-times">Reading in stops and times
</h2>
<p>For this example, I simplified SEPTA&rsquo;s transit data down to two pieces:</p>
<ul>
<li><code>chw-stops.csv</code> - detailed information about each stop location.</li>
<li><code>times.csv</code> - when a train arrives at a stop on the Chesnut Hill West line. Each row is a stop location, and each column is a trip (e.g. the 6:51am train).</li>
</ul>
<p>To make the final table we joined these two together, to get the trips and stop information together.</p>
<div class="code-block"><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-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars</span> <span class="k">as</span> <span class="nn">pl</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">stops</span> <span class="o">=</span> <span class="n">pl</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s2">&#34;chw-stops.csv&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">times</span> <span class="o">=</span> <span class="n">pl</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s2">&#34;times.csv&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<p>Here&rsquo;s a quick preview of stops.</p>
<div class="code-block"><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">stops</span><span class="o">.</span><span class="n">select</span><span class="p">(</span><span class="s2">&#34;stop_name&#34;</span><span class="p">,</span> <span class="s2">&#34;service_access&#34;</span><span class="p">,</span> <span class="s2">&#34;service_cash&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">head</span><span class="p">()</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div><style>
.dataframe > thead > tr,
.dataframe > tbody > tr {
  text-align: right;
  white-space: pre-wrap;
}
</style>
<small>shape: (5, 3)</small>
<table>
  <thead>
      <tr>
          <th>stop_name</th>
          <th>service_access</th>
          <th>service_cash</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>str</td>
          <td>i64</td>
          <td>i64</td>
      </tr>
      <tr>
          <td>&quot;Gray 30th Street&quot;</td>
          <td>1</td>
          <td>0</td>
      </tr>
      <tr>
          <td>&quot;Suburban Station&quot;</td>
          <td>0</td>
          <td>0</td>
      </tr>
      <tr>
          <td>&quot;Jefferson Station&quot;</td>
          <td>0</td>
          <td>0</td>
      </tr>
      <tr>
          <td>&quot;Temple University&quot;</td>
          <td>1</td>
          <td>0</td>
      </tr>
      <tr>
          <td>&quot;Chestnut Hill West&quot;</td>
          <td>0</td>
          <td>0</td>
      </tr>
  </tbody>
</table>
</div>
<p>Notice that the table above has the name of each stop, and a 1 or 0 in the <code>service_access</code> column to indicate whether the stop is wheelchair accessible. Note that a big challenge for this specific route is that sometimes boarding the train requires using steps, and sometimes the station requires using steps. For example, Chelton Ave (not shown) does not require steps to board the train, but the station itself is not wheelchair accessible because of steps to get to the platform.</p>
<p>Here&rsquo;s a quick preview of the times.</p>
<div class="code-block"><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">times</span><span class="o">.</span><span class="n">head</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div><style>
.dataframe > thead > tr,
.dataframe > tbody > tr {
  text-align: right;
  white-space: pre-wrap;
}
</style>
<small>shape: (3, 9)</small>
<table>
  <thead>
      <tr>
          <th>stop_name</th>
          <th>8210</th>
          <th>8716</th>
          <th>8318</th>
          <th>8322</th>
          <th>8338</th>
          <th>8242</th>
          <th>8750</th>
          <th>8756</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>str</td>
      </tr>
      <tr>
          <td>&quot;Chestnut Hill West&quot;</td>
          <td>&quot;06:51:00&quot;</td>
          <td>&quot;08:08:00&quot;</td>
          <td>&quot;08:49:00&quot;</td>
          <td>&quot;09:49:00&quot;</td>
          <td>&quot;13:52:00&quot;</td>
          <td>&quot;14:49:00&quot;</td>
          <td>&quot;16:48:00&quot;</td>
          <td>&quot;18:20:00&quot;</td>
      </tr>
      <tr>
          <td>&quot;Highland&quot;</td>
          <td>&quot;06:52:00&quot;</td>
          <td>&quot;08:09:00&quot;</td>
          <td>&quot;08:50:00&quot;</td>
          <td>&quot;09:50:00&quot;</td>
          <td>&quot;13:53:00&quot;</td>
          <td>&quot;14:50:00&quot;</td>
          <td>&quot;16:49:00&quot;</td>
          <td>&quot;18:21:00&quot;</td>
      </tr>
      <tr>
          <td>&quot;St. Martins&quot;</td>
          <td>&quot;06:54:00&quot;</td>
          <td>&quot;08:11:00&quot;</td>
          <td>&quot;08:52:00&quot;</td>
          <td>&quot;09:52:00&quot;</td>
          <td>&quot;13:55:00&quot;</td>
          <td>&quot;14:52:00&quot;</td>
          <td>&quot;16:51:00&quot;</td>
          <td>&quot;18:23:00&quot;</td>
      </tr>
  </tbody>
</table>
</div>
<p>Notice that each trip is a column (i.e. a train leaving from Chesnut Hill West at a specific time), and each row is a stop. For example, the 8210 train is the 6:51am train. (Note that schedules and train numbers can change, so this data may be out of date).</p>
<p>Joining these together gives us <code>stop_times</code>, with trips and stop information on the columns.</p>
<div class="code-block"><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-python" data-lang="python"><span class="line"><span class="cl"><span class="n">stop_times</span> <span class="o">=</span> <span class="n">times</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">other</span><span class="o">=</span><span class="n">stops</span><span class="p">,</span> <span class="n">on</span><span class="o">=</span><span class="s2">&#34;stop_name&#34;</span><span class="p">,</span> <span class="n">maintain_order</span><span class="o">=</span><span class="s2">&#34;left&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">select</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">pl</span><span class="o">.</span><span class="n">lit</span><span class="p">(</span><span class="s2">&#34;To Center City&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">alias</span><span class="p">(</span><span class="s2">&#34;direction&#34;</span><span class="p">),</span> <span class="n">pl</span><span class="o">.</span><span class="n">col</span><span class="p">(</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></span><span class="line"><span class="cl"><span class="n">stop_times</span><span class="o">.</span><span class="n">head</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div><style>
.dataframe > thead > tr,
.dataframe > tbody > tr {
  text-align: right;
  white-space: pre-wrap;
}
</style>
<small>shape: (3, 20)</small>
<table>
  <thead>
      <tr>
          <th>direction</th>
          <th>stop_name</th>
          <th>8210</th>
          <th>8716</th>
          <th>8318</th>
          <th>8322</th>
          <th>8338</th>
          <th>8242</th>
          <th>8750</th>
          <th>8756</th>
          <th>service_access</th>
          <th>service_cash</th>
          <th>service_park</th>
          <th>fare_zone</th>
          <th>stop_id</th>
          <th>stop_desc</th>
          <th>stop_lat</th>
          <th>stop_lon</th>
          <th>zone_id</th>
          <th>stop_url</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>str</td>
          <td>i64</td>
          <td>i64</td>
          <td>i64</td>
          <td>str</td>
          <td>i64</td>
          <td>str</td>
          <td>f64</td>
          <td>f64</td>
          <td>str</td>
          <td>str</td>
      </tr>
      <tr>
          <td>&quot;To Center City&quot;</td>
          <td>&quot;Chestnut Hill West&quot;</td>
          <td>&quot;06:51:00&quot;</td>
          <td>&quot;08:08:00&quot;</td>
          <td>&quot;08:49:00&quot;</td>
          <td>&quot;09:49:00&quot;</td>
          <td>&quot;13:52:00&quot;</td>
          <td>&quot;14:49:00&quot;</td>
          <td>&quot;16:48:00&quot;</td>
          <td>&quot;18:20:00&quot;</td>
          <td>0</td>
          <td>0</td>
          <td>1</td>
          <td>&quot;2&quot;</td>
          <td>90801</td>
          <td>null</td>
          <td>40.076389</td>
          <td>-75.208333</td>
          <td>&quot;2S&quot;</td>
          <td>null</td>
      </tr>
      <tr>
          <td>&quot;To Center City&quot;</td>
          <td>&quot;Highland&quot;</td>
          <td>&quot;06:52:00&quot;</td>
          <td>&quot;08:09:00&quot;</td>
          <td>&quot;08:50:00&quot;</td>
          <td>&quot;09:50:00&quot;</td>
          <td>&quot;13:53:00&quot;</td>
          <td>&quot;14:50:00&quot;</td>
          <td>&quot;16:49:00&quot;</td>
          <td>&quot;18:21:00&quot;</td>
          <td>0</td>
          <td>0</td>
          <td>1</td>
          <td>&quot;2&quot;</td>
          <td>90802</td>
          <td>null</td>
          <td>40.070556</td>
          <td>-75.211111</td>
          <td>&quot;2S&quot;</td>
          <td>null</td>
      </tr>
      <tr>
          <td>&quot;To Center City&quot;</td>
          <td>&quot;St. Martins&quot;</td>
          <td>&quot;06:54:00&quot;</td>
          <td>&quot;08:11:00&quot;</td>
          <td>&quot;08:52:00&quot;</td>
          <td>&quot;09:52:00&quot;</td>
          <td>&quot;13:55:00&quot;</td>
          <td>&quot;14:52:00&quot;</td>
          <td>&quot;16:51:00&quot;</td>
          <td>&quot;18:23:00&quot;</td>
          <td>0</td>
          <td>0</td>
          <td>1</td>
          <td>&quot;1&quot;</td>
          <td>90803</td>
          <td>null</td>
          <td>40.065833</td>
          <td>-75.204444</td>
          <td>&quot;2S&quot;</td>
          <td>null</td>
      </tr>
  </tbody>
</table>
</div>
<p>Notice that in the table above, the first row tells us when each train leaves Chesnut Hill West, and information about the Chesnut Hill West stop.</p>
<h2 id="creating-the-table">Creating the table
</h2>
<p>Below is the code for the table, with 5 key activities marked with comments. For example, the first is creating high level structure, like the header and the left-hand &ldquo;To Center City&rdquo; stub. Others include formatting in checkmarks, customizing columns (e.g. their width), and styling (e.g. setting background colors and fonts).</p>
<p>It&rsquo;s a lot to take in, but worth it!:</p>
<div class="code-block"><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><span class="lnt"> 99
</span><span class="lnt">100
</span><span class="lnt">101
</span><span class="lnt">102
</span><span class="lnt">103
</span><span class="lnt">104
</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">great_tables</span> <span class="kn">import</span> <span class="n">GT</span><span class="p">,</span> <span class="n">html</span><span class="p">,</span> <span class="n">style</span><span class="p">,</span> <span class="n">loc</span><span class="p">,</span> <span class="n">google_font</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars</span> <span class="k">as</span> <span class="nn">pl</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">polars.selectors</span> <span class="k">as</span> <span class="nn">cs</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 class="k">def</span> <span class="nf">h_m_p</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">h</span><span class="p">,</span> <span class="n">m</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="p">[</span><span class="nb">int</span><span class="p">(</span><span class="n">part</span><span class="p">)</span> <span class="k">for</span> <span class="n">part</span> <span class="ow">in</span> <span class="n">s</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;:&#34;</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">    <span class="n">ap</span> <span class="o">=</span> <span class="s2">&#34;a&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">h</span> <span class="o">&gt;</span> <span class="mi">12</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">h</span> <span class="o">-=</span> <span class="mi">12</span>
</span></span><span class="line"><span class="cl">        <span class="n">ap</span> <span class="o">=</span> <span class="s2">&#34;p&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">h</span><span class="si">}</span><span class="s2">:</span><span class="si">{</span><span class="n">m</span><span class="si">:</span><span class="s2">02d</span><span class="si">}{</span><span class="n">ap</span><span class="si">}</span><span class="s2">&#34;</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 class="k">def</span> <span class="nf">tick</span><span class="p">(</span><span class="n">b</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;&amp;check;&#34;</span> <span class="k">if</span> <span class="n">b</span> <span class="k">else</span> <span class="s2">&#34;&#34;</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 class="n">transit_table</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">stop_times</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 left-hand stub, top header, and hide extra cols --------</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_stub</span><span class="p">(</span><span class="n">groupname_col</span><span class="o">=</span><span class="s2">&#34;direction&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_header</span><span class="p">(</span><span class="s2">&#34;Saturdays, Sundays, and Major Holidays&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_hide</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;stop_url&#34;</span><span class="p">,</span> <span class="s2">&#34;zone_id&#34;</span><span class="p">,</span> <span class="s2">&#34;stop_desc&#34;</span><span class="p">,</span> <span class="s2">&#34;stop_lat&#34;</span><span class="p">,</span> <span class="s2">&#34;stop_lon&#34;</span><span class="p">,</span> <span class="s2">&#34;stop_id&#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"># custom functions for checkmarks and time formatting -----------</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt</span><span class="p">(</span><span class="n">h_m_p</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="n">cs</span><span class="o">.</span><span class="n">matches</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^[0-9]</span><span class="si">{4}</span><span class="s2">$&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt</span><span class="p">(</span><span class="n">tick</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="n">cs</span><span class="o">.</span><span class="n">starts_with</span><span class="p">(</span><span class="s2">&#34;service_&#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"># relabel columns and add spanners (labels over columns) --------</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_label</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">stop_name</span><span class="o">=</span><span class="s2">&#34;Stations&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">service_access</span><span class="o">=</span><span class="s2">&#34;A&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">service_cash</span><span class="o">=</span><span class="s2">&#34;C&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">service_park</span><span class="o">=</span><span class="s2">&#34;P&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">fare_zone</span><span class="o">=</span><span class="n">html</span><span class="p">(</span><span class="s2">&#34;Fare&lt;br&gt;Zone&#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="o">.</span><span class="n">tab_spanner</span><span class="p">(</span><span class="n">label</span><span class="o">=</span><span class="s2">&#34;Services&#34;</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="n">cs</span><span class="o">.</span><span class="n">starts_with</span><span class="p">(</span><span class="s2">&#34;service_&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_spanner</span><span class="p">(</span><span class="n">label</span><span class="o">=</span><span class="s2">&#34;Train Number&#34;</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="n">cs</span><span class="o">.</span><span class="n">matches</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^[0-9]</span><span class="si">{4}</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="c1"># move columns around and setting their width and alignment -----</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_move_to_start</span><span class="p">(</span><span class="s2">&#34;fare_zone&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_move_to_start</span><span class="p">(</span><span class="n">cs</span><span class="o">.</span><span class="n">starts_with</span><span class="p">(</span><span class="s2">&#34;service_&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_width</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">cases</span><span class="o">=</span><span class="p">{</span><span class="n">c</span><span class="p">:</span> <span class="s2">&#34;18px&#34;</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">stop_times</span><span class="o">.</span><span class="n">columns</span> <span class="k">if</span> <span class="n">c</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;service_&#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="o">.</span><span class="n">cols_width</span><span class="p">(</span><span class="n">cases</span><span class="o">=</span><span class="p">{</span><span class="n">c</span><span class="p">:</span> <span class="s2">&#34;60px&#34;</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">stop_times</span><span class="o">.</span><span class="n">columns</span> <span class="k">if</span> <span class="n">c</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;8&#34;</span><span class="p">)})</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_align</span><span class="p">(</span><span class="n">align</span><span class="o">=</span><span class="s2">&#34;center&#34;</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="s2">&#34;fare_zone&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">cols_align</span><span class="p">(</span><span class="n">align</span><span class="o">=</span><span class="s2">&#34;right&#34;</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="n">cs</span><span class="o">.</span><span class="n">matches</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^[0-9]</span><span class="si">{4}</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="c1"># styles: striping, vertical text, background colors, fonts -----</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># style header</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_style</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">header</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="n">style</span><span class="o">=</span><span class="n">style</span><span class="o">.</span><span class="n">css</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;background-color: rgb(66, 99, 128) !important; color: white !important; font-size: 24px !important; font-weight: bold !important; border-width: 0px !important;&#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="c1"># style vertical text on left</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_style</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">row_groups</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="n">style</span><span class="o">=</span><span class="n">style</span><span class="o">.</span><span class="n">css</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;writing-mode: sideways-lr; padding-bottom: 25% !important; font-size: 24px !important; font-weight: bold !important; text-transform: uppercase !important;&#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="o">.</span><span class="n">tab_style</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">style</span><span class="o">=</span><span class="n">style</span><span class="o">.</span><span class="n">css</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important;&#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">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">body</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">rows</span><span class="o">=</span><span class="nb">list</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="o">-</span><span class="mi">4</span><span class="p">,</span> <span class="o">-</span><span class="mi">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="o">.</span><span class="n">tab_style</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">style</span><span class="o">=</span><span class="n">style</span><span class="o">.</span><span class="n">css</span><span class="p">(</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="s2">                border-top: none !important;
</span></span></span><span class="line"><span class="cl"><span class="s2">                border-bottom: none !important;
</span></span></span><span class="line"><span class="cl"><span class="s2">                border-right: solid white 2px !important;
</span></span></span><span class="line"><span class="cl"><span class="s2">                color: white !important;
</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="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">body</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">columns</span><span class="o">=~</span><span class="n">cs</span><span class="o">.</span><span class="n">matches</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^[0-9]</span><span class="si">{4}</span><span class="s2">$&#34;</span><span class="p">),</span> <span class="n">rows</span><span class="o">=</span><span class="nb">list</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="o">-</span><span class="mi">4</span><span class="p">,</span> <span class="o">-</span><span class="mi">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="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_style</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">style</span><span class="o">=</span><span class="n">style</span><span class="o">.</span><span class="n">css</span><span class="p">(</span><span class="s2">&#34;border-right: solid black 2px !important;&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">locations</span><span class="o">=</span><span class="n">loc</span><span class="o">.</span><span class="n">body</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">columns</span><span class="o">=~</span><span class="n">cs</span><span class="o">.</span><span class="n">matches</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^[0-9]</span><span class="si">{4}</span><span class="s2">$&#34;</span><span class="p">),</span> <span class="n">rows</span><span class="o">=</span><span class="nb">list</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">))</span> <span class="o">+</span> <span class="p">[</span><span class="mi">13</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="o">.</span><span class="n">tab_options</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">row_striping_background_color</span><span class="o">=</span><span class="s2">&#34;#A9A9A9&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">row_group_as_column</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="o">.</span><span class="n">opt_row_striping</span><span class="p">(</span><span class="n">row_striping</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">opt_table_outline</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">opt_table_font</span><span class="p">(</span><span class="n">font</span><span class="o">=</span><span class="n">google_font</span><span class="p">(</span><span class="s2">&#34;IBM Plex Sans&#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">transit_table</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="dvrkhovtdj" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans&display=swap');
#dvrkhovtdj table {
          font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', 'Fira Sans', 'Droid Sans', Arial, sans-serif;
          -webkit-font-smoothing: antialiased;
          -moz-osx-font-smoothing: grayscale;
        }
<p>#dvrkhovtdj thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#dvrkhovtdj p { margin: 0; padding: 0; }
#dvrkhovtdj .gt_table { 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: 3px; border-top-color: #D3D3D3; border-right-style: solid; border-right-width: 3px; border-right-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 3px; border-bottom-color: #D3D3D3; border-left-style: solid; border-left-width: 3px; border-left-color: #D3D3D3; }
#dvrkhovtdj .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#dvrkhovtdj .gt_title { color: #333333; font-size: 125%; font-weight: initial; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; border-bottom-color: #FFFFFF; border-bottom-width: 0; }
#dvrkhovtdj .gt_subtitle { color: #333333; font-size: 85%; font-weight: initial; padding-top: 3px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; border-top-color: #FFFFFF; border-top-width: 0; }
#dvrkhovtdj .gt_heading { background-color: #FFFFFF; text-align: center; border-bottom-color: #FFFFFF; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; }
#dvrkhovtdj .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#dvrkhovtdj .gt_col_headings { 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; }
#dvrkhovtdj .gt_col_heading { 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: 5px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; }
#dvrkhovtdj .gt_column_spanner_outer { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; padding-top: 0; padding-bottom: 0; padding-left: 4px; padding-right: 4px; }
#dvrkhovtdj .gt_column_spanner_outer:first-child { padding-left: 0; }
#dvrkhovtdj .gt_column_spanner_outer:last-child { padding-right: 0; }
#dvrkhovtdj .gt_column_spanner { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 5px; overflow-x: hidden; display: inline-block; width: 100%; }
#dvrkhovtdj .gt_spanner_row { border-bottom-style: hidden; }
#dvrkhovtdj .gt_group_heading { padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; 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; vertical-align: middle; text-align: left; }
#dvrkhovtdj .gt_empty_group_heading { padding: 0.5px; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; vertical-align: middle; }
#dvrkhovtdj .gt_from_md&gt; :first-child { margin-top: 0; }
#dvrkhovtdj .gt_from_md&gt; :last-child { margin-bottom: 0; }
#dvrkhovtdj .gt_row { 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; }
#dvrkhovtdj .gt_stub { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #D3D3D3; padding-left: 5px; padding-right: 5px; }
#dvrkhovtdj .gt_stub_row_group { color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #D3D3D3; padding-left: 5px; padding-right: 5px; vertical-align: top; }
#dvrkhovtdj .gt_row_group_first td { border-top-width: 2px; }
#dvrkhovtdj .gt_row_group_first th { border-top-width: 2px; }
#dvrkhovtdj .gt_striped { color: #333333; background-color: #A9A9A9; }
#dvrkhovtdj .gt_table_body { border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#dvrkhovtdj .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#dvrkhovtdj .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#dvrkhovtdj .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#dvrkhovtdj .gt_sourcenotes { color: #333333; background-color: #FFFFFF; border-bottom-style: none; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; }
#dvrkhovtdj .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#dvrkhovtdj .gt_left { text-align: left; }
#dvrkhovtdj .gt_center { text-align: center; }
#dvrkhovtdj .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#dvrkhovtdj .gt_font_normal { font-weight: normal; }
#dvrkhovtdj .gt_font_bold { font-weight: bold; }
#dvrkhovtdj .gt_font_italic { font-style: italic; }
#dvrkhovtdj .gt_super { font-size: 65%; }
#dvrkhovtdj .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#dvrkhovtdj .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table class="gt_table" data-quarto-postprocess="true" style="table-layout: fixed;" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="14" class="gt_heading gt_title gt_font_normal" style="background-color: rgb(66, 99, 128) !important; color: white !important; font-size: 24px !important; font-weight: bold !important; border-width: 0px !important">Saturdays, Sundays, and Major Holidays</th>
</tr>
<tr class="gt_col_headings gt_spanner_row">
<th rowspan="2" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col"></th>
<th colspan="3" id="Services" class="gt_center gt_columns_top_border gt_column_spanner_outer" data-quarto-table-cell-role="th" scope="colgroup"><span class="gt_column_spanner">Services</span></th>
<th rowspan="2" id="fare_zone" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" scope="col">Fare<br />
Zone</th>
<th rowspan="2" id="stop_name" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">Stations</th>
<th colspan="8" id="Train-Number" class="gt_center gt_columns_top_border gt_column_spanner_outer" data-quarto-table-cell-role="th" scope="colgroup"><span class="gt_column_spanner">Train Number</span></th>
</tr>
<tr class="gt_col_headings">
<th id="service_access" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">A</th>
<th id="service_cash" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">C</th>
<th id="service_park" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">P</th>
<th id="8210" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8210</th>
<th id="8716" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8716</th>
<th id="8318" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8318</th>
<th id="8322" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8322</th>
<th id="8338" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8338</th>
<th id="8242" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8242</th>
<th id="8750" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8750</th>
<th id="8756" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">8756</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td rowspan="14" class="gt_row gt_left gt_stub_row_group" data-quarto-table-cell-role="th" style="writing-mode: sideways-lr; padding-bottom: 25% !important; font-size: 24px !important; font-weight: bold !important; text-transform: uppercase !important">To Center City</td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_center" style="border-right: solid black 2px !important">2</td>
<td class="gt_row gt_left" style="border-right: solid black 2px !important">Chestnut Hill West</td>
<td class="gt_row gt_right">6:51a</td>
<td class="gt_row gt_right">8:08a</td>
<td class="gt_row gt_right">8:49a</td>
<td class="gt_row gt_right">9:49a</td>
<td class="gt_row gt_right">1:52p</td>
<td class="gt_row gt_right">2:49p</td>
<td class="gt_row gt_right">4:48p</td>
<td class="gt_row gt_right">6:20p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_center gt_striped" style="border-right: solid black 2px !important">2</td>
<td class="gt_row gt_left gt_striped" style="border-right: solid black 2px !important">Highland</td>
<td class="gt_row gt_right gt_striped">6:52a</td>
<td class="gt_row gt_right gt_striped">8:09a</td>
<td class="gt_row gt_right gt_striped">8:50a</td>
<td class="gt_row gt_right gt_striped">9:50a</td>
<td class="gt_row gt_right gt_striped">1:53p</td>
<td class="gt_row gt_right gt_striped">2:50p</td>
<td class="gt_row gt_right gt_striped">4:49p</td>
<td class="gt_row gt_right gt_striped">6:21p</td>
</tr>
<tr>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_center" style="border-right: solid black 2px !important">1</td>
<td class="gt_row gt_left" style="border-right: solid black 2px !important">St. Martins</td>
<td class="gt_row gt_right">6:54a</td>
<td class="gt_row gt_right">8:11a</td>
<td class="gt_row gt_right">8:52a</td>
<td class="gt_row gt_right">9:52a</td>
<td class="gt_row gt_right">1:55p</td>
<td class="gt_row gt_right">2:52p</td>
<td class="gt_row gt_right">4:51p</td>
<td class="gt_row gt_right">6:23p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_center gt_striped" style="border-right: solid black 2px !important">1</td>
<td class="gt_row gt_left gt_striped" style="border-right: solid black 2px !important">Richard Allen Lane</td>
<td class="gt_row gt_right gt_striped">6:56a</td>
<td class="gt_row gt_right gt_striped">8:13a</td>
<td class="gt_row gt_right gt_striped">8:54a</td>
<td class="gt_row gt_right gt_striped">9:54a</td>
<td class="gt_row gt_right gt_striped">1:57p</td>
<td class="gt_row gt_right gt_striped">2:54p</td>
<td class="gt_row gt_right gt_striped">4:53p</td>
<td class="gt_row gt_right gt_striped">6:25p</td>
</tr>
<tr>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_center" style="border-right: solid black 2px !important">1</td>
<td class="gt_row gt_left" style="border-right: solid black 2px !important">Carpenter</td>
<td class="gt_row gt_right">6:58a</td>
<td class="gt_row gt_right">8:15a</td>
<td class="gt_row gt_right">8:56a</td>
<td class="gt_row gt_right">9:56a</td>
<td class="gt_row gt_right">1:59p</td>
<td class="gt_row gt_right">2:56p</td>
<td class="gt_row gt_right">4:55p</td>
<td class="gt_row gt_right">6:27p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_center gt_striped" style="border-right: solid black 2px !important">1</td>
<td class="gt_row gt_left gt_striped" style="border-right: solid black 2px !important">Upsal</td>
<td class="gt_row gt_right gt_striped">7:00a</td>
<td class="gt_row gt_right gt_striped">8:17a</td>
<td class="gt_row gt_right gt_striped">8:58a</td>
<td class="gt_row gt_right gt_striped">9:58a</td>
<td class="gt_row gt_right gt_striped">2:01p</td>
<td class="gt_row gt_right gt_striped">2:58p</td>
<td class="gt_row gt_right gt_striped">4:57p</td>
<td class="gt_row gt_right gt_striped">6:29p</td>
</tr>
<tr>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_center" style="border-right: solid black 2px !important">C</td>
<td class="gt_row gt_left" style="border-right: solid black 2px !important">Tulpehocken</td>
<td class="gt_row gt_right">7:02a</td>
<td class="gt_row gt_right">8:19a</td>
<td class="gt_row gt_right">9:00a</td>
<td class="gt_row gt_right">10:00a</td>
<td class="gt_row gt_right">2:03p</td>
<td class="gt_row gt_right">3:00p</td>
<td class="gt_row gt_right">4:59p</td>
<td class="gt_row gt_right">6:31p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_center gt_striped" style="border-right: solid black 2px !important">C</td>
<td class="gt_row gt_left gt_striped" style="border-right: solid black 2px !important">Chelten Avenue</td>
<td class="gt_row gt_right gt_striped">7:04a</td>
<td class="gt_row gt_right gt_striped">8:21a</td>
<td class="gt_row gt_right gt_striped">9:02a</td>
<td class="gt_row gt_right gt_striped">10:02a</td>
<td class="gt_row gt_right gt_striped">2:05p</td>
<td class="gt_row gt_right gt_striped">3:02p</td>
<td class="gt_row gt_right gt_striped">5:01p</td>
<td class="gt_row gt_right gt_striped">6:33p</td>
</tr>
<tr>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_center" style="border-right: solid black 2px !important">C</td>
<td class="gt_row gt_left" style="border-right: solid black 2px !important">Queen Lane</td>
<td class="gt_row gt_right">7:06a</td>
<td class="gt_row gt_right">8:23a</td>
<td class="gt_row gt_right">9:04a</td>
<td class="gt_row gt_right">10:04a</td>
<td class="gt_row gt_right">2:07p</td>
<td class="gt_row gt_right">3:04p</td>
<td class="gt_row gt_right">5:03p</td>
<td class="gt_row gt_right">6:35p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_center gt_striped" style="border-right: solid black 2px !important">C</td>
<td class="gt_row gt_left gt_striped" style="border-right: solid black 2px !important">North Philadelphia</td>
<td class="gt_row gt_right gt_striped">7:12a</td>
<td class="gt_row gt_right gt_striped">8:29a</td>
<td class="gt_row gt_right gt_striped">9:12a</td>
<td class="gt_row gt_right gt_striped">10:12a</td>
<td class="gt_row gt_right gt_striped">2:15p</td>
<td class="gt_row gt_right gt_striped">3:12p</td>
<td class="gt_row gt_right gt_striped">5:09p</td>
<td class="gt_row gt_right gt_striped">6:41p</td>
</tr>
<tr>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">✓</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important"></td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">✓</td>
<td class="gt_row gt_center" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">2</td>
<td class="gt_row gt_left" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">Gray 30th Street</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">7:23a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">8:42a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">9:23a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">10:23a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">2:26p</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">3:23p</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">5:20p</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">6:54p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important"></td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important"></td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">✓</td>
<td class="gt_row gt_center gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">2</td>
<td class="gt_row gt_left gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">Suburban Station</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">7:28a</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">8:47a</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">9:28a</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">10:28a</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">2:31p</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">3:28p</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">5:25p</td>
<td class="gt_row gt_right gt_striped" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">6:59p</td>
</tr>
<tr>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important"></td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important"></td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">✓</td>
<td class="gt_row gt_center" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">2</td>
<td class="gt_row gt_left" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important; border-top: none !important; border-bottom: none !important; border-right: solid white 2px !important; color: white !important">Jefferson Station</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">7:33a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">8:52a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">9:33a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">10:33a</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">2:36p</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">3:33p</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">5:30p</td>
<td class="gt_row gt_right" style="background-color: black !important; color: white !important; border-top: none !important; border-bottom: none !important">7:04p</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important"></td>
<td class="gt_row gt_right gt_striped" style="border-right: solid black 2px !important">✓</td>
<td class="gt_row gt_center gt_striped" style="border-right: solid black 2px !important">2</td>
<td class="gt_row gt_left gt_striped" style="border-right: solid black 2px !important">Temple University</td>
<td class="gt_row gt_right gt_striped">7:37a</td>
<td class="gt_row gt_right gt_striped">8:57a</td>
<td class="gt_row gt_right gt_striped">9:37a</td>
<td class="gt_row gt_right gt_striped">10:37a</td>
<td class="gt_row gt_right gt_striped">2:40p</td>
<td class="gt_row gt_right gt_striped">3:37p</td>
<td class="gt_row gt_right gt_striped">5:35p</td>
<td class="gt_row gt_right gt_striped">7:08p</td>
</tr>
</tbody>
</table>
</div>
<h2 id="other-schedules-in-the-wild">Other schedules in the wild
</h2>
<p>MetroTransit in Minneapolis uses a transposed format, with stops as columns and trips as rows. Here&rsquo;s an example from their 






<a href="https://www.metrotransit.org/route/2" target="_blank" rel="noopener">Route 2 bus timetable</a>
:</p>
<img src="https://opensource.posit.co/blog/2026-03-12_septa-timetables/./metrotransit-route2.png" style="max-width: 600px; display: block; margin-left: auto; margin-right: auto;" />
<p>This is useful when there a lot of trips, because with trips on the rows readers can scroll down (versus needing to scroll sideways).</p>
<p>The MTA in New York City is similar. Here&rsquo;s an example of their 






<a href="https://www.mta.info/schedules/bus/bx1" target="_blank" rel="noopener">bx1 bus route timetable</a>
:</p>
<img src="https://opensource.posit.co/blog/2026-03-12_septa-timetables/./mta-route-bx1.png" style="max-width: 600px; display: block; margin-left: auto; margin-right: auto;" />
<p>What I like about all these tables is they highlight the structure behind bus and train routes. Sometimes they skip certain stops. But realistically, what makes them a route is that trips tend to make the same stops over and over.</p>
<p>A common alternative to using these tables is to do routing from a set start to end point. For example, below is a form for selecting a start and end point on SEPTA&rsquo;s website, with a resulting table of departure and arrival times.</p>
<img src="https://opensource.posit.co/blog/2026-03-12_septa-timetables/./septa-routing.png" style="max-width: 600px; display: block; margin-left: auto; margin-right: auto;" />
<p>Notice that the table has removed a lot of information about intermediate stops people might not care about.</p>
<h2 id="in-conclusion">In conclusion
</h2>
<p>Transit tables are richly structured displays of information.
They take advantage often of the fact that a train route like Chesnut Hill West is a fixed set of stops&ndash;so that stops can be on the rows, and arrival times for trips throughout the day can be on the columns.</p>
<p>This is intuitive to people reading transit timetables, but can get tricky to display on the web. Timetables are a core part of navigating transit networks, so it was a fun experiment to try replicating one of Septa&rsquo;s timetables in Great Tables!</p>
]]></description>
    </item>
    <item>
      <title>Outgrowing your laptop with R and Positron</title>
      <link>https://opensource.posit.co/blog/2026-03-05_outgrow-your-laptop/</link>
      <pubDate>Thu, 05 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-03-05_outgrow-your-laptop/</guid>
      <dc:creator>Julia Silge</dc:creator><description><![CDATA[<h2 id="my-data-is-too-big-for-my-laptop">My data is too big for my laptop!
</h2>
<p>Last week, I had the pleasure of giving a talk to 






<a href="https://www.meetup.com/rladies-abuja/" target="_blank" rel="noopener">R-Ladies Abuja</a>
 about how Positron can grow with you as you work on data that is too large for your laptop. The talk was recorded, and you can find it on YouTube here:</p>















  

  
  
  
    
    
  

  
  










  
  
    <div class="w-full aspect-video">
      <iframe
        src="https://www.youtube.com/embed/sPZsH0eaUpQ"
        class="w-full h-full"
        
        frameborder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowfullscreen></iframe>
    </div>
  




<p>I opened this talk discussing how I first learned about these &ldquo;beyond your laptop&rdquo; technologies, typically in a organization where these technologies were already in use and seemed specific to infrastructure there. I later came to understand that these technologies are actually related to each other and understanding one can really help when you need to pick up another one. I pointed out some of the Positron features that are designed to make it easier to work with these technologies:</p>
<ul>
<li>






<a href="https://positron.posit.co/data-explorer.html" target="_blank" rel="noopener">Data Explorer</a>
</li>
<li>






<a href="https://positron.posit.co/connections-pane.html" target="_blank" rel="noopener">Connections Pane</a>
</li>
<li>






<a href="https://positron.posit.co/remote-ssh.html" target="_blank" rel="noopener">Remote SSH sessions</a>
</li>
</ul>
<h2 id="check-out-my-slides">Check out my slides
</h2>
<p>If you&rsquo;d like to check out my slides, they are 






<a href="https://juliasilge.github.io/rladies-abuja/" target="_blank" rel="noopener">available as well</a>
:</p>
<iframe src="https://juliasilge.github.io/rladies-abuja/" width="100%" style="aspect-ratio: 16/9; border: none;">
</iframe>
]]></description>
    </item>
    <item>
      <title>PDF Accessibility and Standards</title>
      <link>https://opensource.posit.co/blog/2026-03-05_pdf-accessibility-and-standards/</link>
      <pubDate>Thu, 05 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/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>
<div class="grid gap-12 items-start md:grid-cols-2">
<div class="prose max-w-none">
<p><strong>PDF (LaTeX)</strong></p>
<div class="code-block"><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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">format</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pdf</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">pdf-standard</span><span class="p">:</span><span class="w"> </span><span class="l">ua-2</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<div class="prose max-w-none">
<p><strong>Typst</strong></p>
<div class="code-block"><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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">format</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">typst</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">pdf-standard</span><span class="p">:</span><span class="w"> </span><span class="l">ua-1</span></span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
</div>
<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>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-2">
  <div class="code-with-filename-label" id="code-filename-2"><span class="font-mono text-sm">Terminal</span></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></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>
</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://opensource.posit.co/blog/2026-03-05_pdf-accessibility-and-standards/thumbnail.png" length="41719" type="image/png" />
    </item>
    <item>
      <title>Where Questions Become Queries: Meet querychat</title>
      <link>https://opensource.posit.co/blog/2026-01-22_querychat-python-r/</link>
      <pubDate>Thu, 22 Jan 2026 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2026-01-22_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://opensource.posit.co/blog/2026-01-22_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://opensource.posit.co/blog/2026-01-22_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="code-block"><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></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="code-block"><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></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="code-block"><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>
</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="code-block"><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></div>
<p>Or, if you&rsquo;re using <code>uv</code>, add it like so:</p>
<div class="code-block"><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></div>
<p>Once installed, import it like this:</p>
<div class="code-block"><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>
<div id="tabset-3-2">
<p>You can get <code>querychat</code> from CRAN using:</p>
<div class="code-block"><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></div>
<p>Alternatively, if you want the latest development version, you can install <code>querychat</code> from GitHub using:</p>
<div class="code-block"><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></div>
<p>Once installed, load the package as usual:</p>
<div class="code-block"><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>
</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="code-block"><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></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="code-block"><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>
<div id="tabset-4-2">
<div class="code-block"><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></div>
<p>Or, alternatively, you could write:</p>
<div class="code-block"><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></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://opensource.posit.co/blog/2026-01-22_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://opensource.posit.co/blog/2026-01-22_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="code-block"><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></span></code></pre></td></tr></table>
</div>
</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="code-block"><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></span></code></pre></td></tr></table>
</div>
</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="code-block"><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></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="code-block"><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></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="code-block"><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></span></code></pre></td></tr></table>
</div>
</div></div>
<div class="code-block"><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></div>
<p>The conclusion?</p>
<div class="code-block"><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></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://opensource.posit.co/blog/2026-01-22_querychat-python-r/diamonds-drop-py.png" style="width:50.0%" data-fig-align="center" />
</div>
<div id="tabset-6-2">
<img src="https://opensource.posit.co/blog/2026-01-22_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://opensource.posit.co/blog/2025-09-05_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="code-block"><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>
<div id="tabset-7-2">
<div class="code-block"><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>
</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="code-block"><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>
<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="code-block"><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>
</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="code-block"><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></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="code-block"><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></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="code-block"><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>
<div id="tabset-10-2">
<div class="code-block"><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>
</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="code-block"><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></div>
<p>Which give us this nice greeting:</p>
<div class="code-block"><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>
<div id="tabset-11-2">
<div class="code-block"><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></div>
<p>Which give us this nice greeting:</p>
<div class="code-block"><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>
</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="code-block"><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">- z (float) — Depth in mm</span></span></code></pre></td></tr></table>
</div>
</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="code-block"><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>
<div id="tabset-12-2">
<div class="code-block"><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>
</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="code-block"><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">- Maintain consistent spelling in British English.</span></span></code></pre></td></tr></table>
</div>
</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="code-block"><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>
<div id="tabset-13-2">
<div class="code-block"><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>
</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="code-block"><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>
<div id="tabset-14-2">
<div class="code-block"><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>
</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="code-block"><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></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="code-block"><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>
<div class="code-block"><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></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://opensource.posit.co/blog/2026-01-22_querychat-python-r/diamonds-bespoke-py.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
<div id="tabset-15-2">
<div class="code-block"><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></div>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://opensource.posit.co/blog/2026-01-22_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://opensource.posit.co/blog/2026-01-22_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="code-block"><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>
<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="code-block"><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>
</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="code-block"><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></div>
<p><code>shescores_data_description.md</code>:</p>
<div class="code-block"><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">- match_id (string): Unique identifier for the match, typically based on date and team names.</span></span></code></pre></td></tr></table>
</div>
</div></div>
<p><code>shescores_extra_instrucions.md</code>:</p>
<div class="code-block"><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></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="code-block"><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>
<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="code-block"><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>
</div>
<p>It results in a lot less code and logic too. Win-win. Thanks <code>querychat</code> !</p>
<figure>
<img src="https://opensource.posit.co/blog/2026-01-22_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="code-block"><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></div>
<p>We can then use this database in our <code>QueryChat</code> instance like so:</p>
<div class="code-block"><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></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="code-block"><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></div>
<p>If you have a SQLite database, connecting to it works in the same manner:</p>
<div class="code-block"><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></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="code-block"><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>
<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="code-block"><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>
</div>
<p>So, what&rsquo;s in this prompt? Let&rsquo;s highlight a few bits:</p>
<div class="code-block"><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>
<div class="code-block"><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></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="code-block"><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></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://opensource.posit.co/blog/2026-01-22_querychat-python-r/querychat-python-r-header.png" length="871482" type="image/png" />
    </item>
    <item>
      <title>Shiny Talks from posit::conf(2025)</title>
      <link>https://opensource.posit.co/blog/2025-12-02_conf-2025-shinytalks/</link>
      <pubDate>Tue, 02 Dec 2025 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2025-12-02_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://opensource.posit.co/blog/2025-12-02_conf-2025-shinytalks/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://opensource.posit.co/blog/2025-12-02_conf-2025-shinytalks/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://opensource.posit.co/blog/2025-12-02_conf-2025-shinytalks/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://opensource.posit.co/blog/2025-12-02_conf-2025-shinytalks/conf-recordings-banner.png" length="243314" type="image/png" />
    </item>
    <item>
      <title>posit::conf(2025) Quarto talks</title>
      <link>https://opensource.posit.co/blog/2025-11-24_conf-talk-videos/</link>
      <pubDate>Mon, 24 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/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://opensource.posit.co/blog/2025-10-27_conf-workshops-materials/">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://opensource.posit.co/blog/2025-11-24_conf-talk-videos/thumbnail.png" length="221625" type="image/png" />
    </item>
    <item>
      <title>Tool Calling UI in shinychat</title>
      <link>https://opensource.posit.co/blog/2025-11-20_shinychat-tool-ui/</link>
      <pubDate>Thu, 20 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2025-11-20_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://opensource.posit.co/blog/2025-11-20_shinychat-tool-ui/index_files/libs/shinychat-0.3.0/chat/chat.js" type="module"></script>
<script src="https://opensource.posit.co/blog/2025-11-20_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="code-block"><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>
<div id="tabset-1-2">
<div class="code-block"><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>
</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="code-block"><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>
<div id="tabset-2-2">
<div class="code-block"><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>
</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="code-block"><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></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>
<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-06 14:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 46,&#10;    &quot;dewpoint&quot;: -3.3333,&#10;    &quot;humidity&quot;: 45,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 10,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Partly Sunny&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-06 15:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 48,&#10;    &quot;dewpoint&quot;: -3.3333,&#10;    &quot;humidity&quot;: 42,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Partly Sunny&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-06 16:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 49,&#10;    &quot;dewpoint&quot;: -3.8889,&#10;    &quot;humidity&quot;: 39,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Mostly Sunny&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-06 17:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 51,&#10;    &quot;dewpoint&quot;: -3.8889,&#10;    &quot;humidity&quot;: 36,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Mostly 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-06 18:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 51,&#10;    &quot;dewpoint&quot;: -4.4444,&#10;    &quot;humidity&quot;: 34,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Partly Sunny&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-06 19:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 51,&#10;    &quot;dewpoint&quot;: -4.4444,&#10;    &quot;humidity&quot;: 34,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Partly Sunny&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-06 20:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 52,&#10;    &quot;dewpoint&quot;: -5,&#10;    &quot;humidity&quot;: 32,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 10,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Partly Sunny&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-06 21:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 51,&#10;    &quot;dewpoint&quot;: -5,&#10;    &quot;humidity&quot;: 33,&#10;    &quot;p_rain&quot;: 1,&#10;    &quot;wind_speed&quot;: 9,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Partly 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-06 22:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 48,&#10;    &quot;dewpoint&quot;: -4.4444,&#10;    &quot;humidity&quot;: 39,&#10;    &quot;p_rain&quot;: 1,&#10;    &quot;wind_speed&quot;: 8,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Mostly Clear&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-06 23:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 47,&#10;    &quot;dewpoint&quot;: -3.8889,&#10;    &quot;humidity&quot;: 42,&#10;    &quot;p_rain&quot;: 4,&#10;    &quot;wind_speed&quot;: 6,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Mostly Clear&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="code-block"><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>
<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>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-6">
  <div class="code-with-filename-label" id="code-filename-6"><span class="font-mono text-sm">app.py</span></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></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></div>
<p>For Shiny Core mode:</p>
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-7">
  <div class="code-with-filename-label" id="code-filename-7"><span class="font-mono text-sm">app.py</span></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></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>
</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="code-block"><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>
<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="code-block"><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></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="code-block"><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></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-06 14:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 46,&#10;    &quot;dewpoint&quot;: -3.3333,&#10;    &quot;humidity&quot;: 45,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 10,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Partly Sunny&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-06 15:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 48,&#10;    &quot;dewpoint&quot;: -3.3333,&#10;    &quot;humidity&quot;: 42,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Partly Sunny&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-06 16:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 49,&#10;    &quot;dewpoint&quot;: -3.8889,&#10;    &quot;humidity&quot;: 39,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Mostly Sunny&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-06 17:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 51,&#10;    &quot;dewpoint&quot;: -3.8889,&#10;    &quot;humidity&quot;: 36,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Mostly 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-06 18:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 51,&#10;    &quot;dewpoint&quot;: -4.4444,&#10;    &quot;humidity&quot;: 34,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Partly Sunny&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-06 19:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 51,&#10;    &quot;dewpoint&quot;: -4.4444,&#10;    &quot;humidity&quot;: 34,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Partly Sunny&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-06 20:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 52,&#10;    &quot;dewpoint&quot;: -5,&#10;    &quot;humidity&quot;: 32,&#10;    &quot;p_rain&quot;: 0,&#10;    &quot;wind_speed&quot;: 10,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Partly Sunny&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-06 21:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 51,&#10;    &quot;dewpoint&quot;: -5,&#10;    &quot;humidity&quot;: 33,&#10;    &quot;p_rain&quot;: 1,&#10;    &quot;wind_speed&quot;: 9,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Partly 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-06 22:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 48,&#10;    &quot;dewpoint&quot;: -4.4444,&#10;    &quot;humidity&quot;: 39,&#10;    &quot;p_rain&quot;: 1,&#10;    &quot;wind_speed&quot;: 8,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Mostly Clear&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-06 23:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 47,&#10;    &quot;dewpoint&quot;: -3.8889,&#10;    &quot;humidity&quot;: 42,&#10;    &quot;p_rain&quot;: 4,&#10;    &quot;wind_speed&quot;: 6,&#10;    &quot;wind_dir&quot;: &quot;W&quot;,&#10;    &quot;skies&quot;: &quot;Mostly Clear&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="code-block"><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>
<div id="tabset-5-2">
<p>Return a <code>ToolResult</code> with display options:</p>
<div class="code-block"><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>
</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;cgjzgjcedr&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-06 14: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;44&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;-2.777778&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;51&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;0&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;W&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;Partly Sunny&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-06 15: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;45&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;-2.777778&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;49&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;0&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;W&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;Partly Sunny&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-06 16: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;46&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;-2.777778&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;47&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;0&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;W&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;Mostly Sunny&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-06 17: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;47&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;-3.333333&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;44&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;0&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;W&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;Partly Sunny&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-06 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;48&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;-3.333333&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;42&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;0&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;W&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;Mostly Sunny&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-06 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;48&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;-3.888889&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;40&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;0&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;W&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;Partly Sunny&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-06 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;49&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;-4.444444&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;37&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;0&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;W&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;Partly Sunny&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-06 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;48&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;-3.888889&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;40&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;1&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;W&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;Partly Cloudy&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-06 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;48&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;-3.888889&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;40&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;1&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;W&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;Mostly Clear&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-06 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;47&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;-3.888889&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;42&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;3&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;9&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;W&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;Mostly Clear&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;  &#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="code-block"><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>
<div id="tabset-6-2">
<div class="code-block"><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>
</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="code-block"><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></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="code-block"><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></div>
<h3 id="core-mode">Core mode
</h3>
<div class="code-block"><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></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="code-block"><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></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="code-block"><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></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://opensource.posit.co/blog/2025-11-20_shinychat-tool-ui/feature.png" length="147726" type="image/png" />
    </item>
    <item>
      <title>Python Open-Source Developer</title>
      <link>https://opensource.posit.co/blog/2025-11-12_python-open-source-developer/</link>
      <pubDate>Wed, 12 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2025-11-12_python-open-source-developer/</guid>
      <dc:creator>Max Kuhn</dc:creator><description><![CDATA[<p>We are hiring a Python open-source developer with a specialization in data analysis and modeling tools. Since deep learning models already have extensive support in Python, our focus is on the analysis and modeling of <em>tabular data</em>. Our primary goal is to develop packages that enhance the existing capabilities of frameworks such as scikit-learn. Note that the current position doesn&rsquo;t involve building or publishing models created here; <em>this is a pure package developer role</em>.</p>
<p>We think that the tidymodels 


  
  
  





<a href="https://www.tmwr.org/software-modeling#fundamentals-for-modeling-software" target="_blank" rel="noopener">philosophy</a>
 can enhance modeling in Python, and we believe that learning more about modeling in Python will provide us with ideas on how to improve tidymodels. To clarify, we are not creating a tidymodels Python package; however, some of its APIs may be beneficial to Python users. Two examples are 






<a href="https://recipes.tidymodels.org/" target="_blank" rel="noopener">recipes</a>
 and broom&rsquo;s 






<a href="https://broom.tidymodels.org/" target="_blank" rel="noopener">tidy and augment</a>
 verbs.</p>
<p>There are numerous ways that individuals can contribute to improving the Python ecosystem. We have a lot of ideas, but we also want to know what our new hire believes is most important. A sample of potential projects that have been on our mind:</p>
<ul>
<li>
<p>Grid search for model tuning has a fairly bad reputation for being inefficient. However, space-filling designs (SFDs) are excellent tools for making small tuning grids that methodically cover the entire parameter space. Users would benefit from having an integrated tool that can create grids from their pipelines using optimal SFDs.</p>
</li>
<li>
<p>There are occasions where we might decline to produce a prediction, perhaps due to our prediction data being extrapolations from the training set. <em>Applicability Domain</em> methods measure extrapolation and can help determine where the model&rsquo;s predictions are likely to be poor. An API that can take a model object and the training set could generate a score that informs users when their predictions are <del>hallucinated</del> unlikely to be accurate.</p>
</li>
<li>
<p>Python deserves an original (Python-only) implementation of the popular 






<a href="https://scholar.google.com/scholar?hl=en&amp;as_sdt=0%2C7&amp;q=Regularization&#43;Paths&#43;for&#43;Generalized&#43;Linear&#43;Models&#43;via&#43;Coordinate&#43;Descent&amp;btnG=" target="_blank" rel="noopener">glmnet model</a>
. There are Python libraries that wrap the original Fortran code; however, this approach isn&rsquo;t easily supported, nor is it able to facilitate the numerous extensions of this particular model (e.g., 






<a href="https://scholar.google.com/scholar?hl=en&amp;as_sdt=0%2C7&amp;q=%22A&#43;sparse-group&#43;lasso%22&amp;btnG=" target="_blank" rel="noopener">group-wise penalties</a>
, 






<a href="https://scholar.google.com/scholar?hl=en&amp;as_sdt=0%2C7&amp;q=%22Nearly&#43;unbiased&#43;variable&#43;selection&#43;under&#43;minimax&#43;concave&#43;penalty%22&amp;btnG=" target="_blank" rel="noopener">MCP</a>
 penalties, etc). This would be a substantial and in-depth project.</p>
</li>
</ul>
<p>We have about a dozen other project ideas.</p>
<p>Open-source developers at Posit often have a broader role than just writing and testing code. We are often the folks defining what is deemed important and prioritizing our work. Additionally, we frequently undertake tasks that are typically considered part of developer relations, such as reaching out to the community, creating additional technical content, speaking at conferences, and teaching workshops. If you are a driven and independent developer, you might be interested in this position. You&rsquo;ll have a lot of agency here and in an environment that encourages quality. Our developers tend to feel a personal stake in our work and want it to be as good as it can be. We&rsquo;re a completely remote team, flexible in terms of how and when we work, and we do a good job of minimizing administrative overhead for engineers.</p>
<p><strong>To learn more about the position and to apply, visit the 






<a href="https://posit.co/job-detail/?gh_jid=7510613003" target="_blank" rel="noopener">Careers page</a>
.</strong></p>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2025-11-12_python-open-source-developer/thumbnail-sq.jpg" length="30429" type="image/jpeg" />
    </item>
    <item>
      <title>posit::conf(2025) Quarto workshop materials</title>
      <link>https://opensource.posit.co/blog/2025-10-27_conf-workshops-materials/</link>
      <pubDate>Mon, 27 Oct 2025 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2025-10-27_conf-workshops-materials/</guid>
      <dc:creator>Charlotte Wickham</dc:creator><description><![CDATA[<p>At posit::conf(2025), we were thrilled to offer two comprehensive Quarto workshops designed to help users at different stages of their journey with Quarto. Whether you were looking to create beautifully branded outputs or extend Quarto&rsquo;s functionality with custom solutions, these workshops provided hands-on learning experiences with expert instructors.</p>
<p>All materials from both workshops are available online. You can access the full workshop websites, source code, and exercises to learn at your own pace or adapt them for your own teaching; they are both released with a 






<a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener">CC BY-SA 4.0</a>
 license.</p>
<h2 id="branded-websites-presentations-dashboards-and-pdfs-with-quarto">Branded Websites, Presentations, Dashboards, and PDFs with Quarto
</h2>
<p><a href="https://posit-conf-2025.github.io/quarto-brand/" class="rounded">Workshop website</a>
<a href="https://github.com/posit-conf-2025/quarto-brand/" class="rounded"><i></i> Source</a>
<a href="https://github.com/posit-dev/quarto-brand-exercises/" class="rounded"><i></i> Exercises</a></p>
<p>Led by 






<a href="https://ivelasq.rbind.io/" target="_blank" rel="noopener">Isabella Velásquez</a>
, Posit, PBC and 






<a href="https://www.linkedin.com/in/sarakaltman" target="_blank" rel="noopener">Sara Altman</a>
, Posit, PBC.</p>
<blockquote>
<p>Designed for data scientists, analysts, and content creators, this immersive session will teach you how to craft cohesive reports and presentations while refining your workflow with Quarto&rsquo;s latest features.</p>
<p>You will learn how to create dynamic websites, professional PDF documents, engaging presentations, and interactive dashboards using Quarto. This workshop highlights Quarto&rsquo;s powerful theming capabilities, including the new support for brand.yml, which ensures that your work maintains a professional and cohesive style across all formats.</p>
<p>By the end of the session, you&rsquo;ll be equipped to:</p>
<ul>
<li>Build and deploy Quarto websites.</li>
<li>Generate professional presentations and PDF reports.</li>
<li>Create interactive dashboards for data visualization and reporting.</li>
<li>Use brand.yml to define and apply consistent theming across all outputs.</li>
</ul>
<p>Whether you&rsquo;re looking to enhance your personal projects or streamline organizational outputs, this workshop will equip you with the tools to create polished, professional results.</p>
</blockquote>
<h2 id="extending-quarto">Extending Quarto
</h2>
<p><a href="https://posit-conf-2025.github.io/quarto-extend/" class="rounded">Workshop website</a>
<a href="https://github.com/posit-conf-2025/quarto-extend/" class="rounded"><i></i> Source</a>
<a href="https://github.com/posit-conf-2025/quarto-extend-exercises/" class="rounded"><i></i> Exercises</a></p>
<p>Led by 






<a href="https://mine-cr.com/" target="_blank" rel="noopener">Mine Çetinkaya-Rundel</a>
, Posit, PBC + Duke University and 






<a href="https://www.cwick.co.nz/" target="_blank" rel="noopener">Charlotte Wickham</a>
 Posit, PBC.</p>
<blockquote>
<p>In this workshop, we will dive deep into ways of customizing your Quarto
outputs with tooling beyond built-in features. This workshop is designed
for data scientists, analysts, and technical writers looking to extend
Quarto&rsquo;s capabilities to suit their unique workflows better.</p>
<p>Participants will learn how to create custom extensions, including new
formats, templates, and filters, to enhance their document production
process. Through hands-on exercises and real-world examples, you&rsquo;ll gain
practical skills in:</p>
<ul>
<li>Developing and integrating custom formats to support diverse outputs
while reducing repetition across projects.</li>
<li>Substituting Quarto&rsquo;s templates with your own to customize formats
beyond the built-in options.</li>
<li>Implementing filters to automate and streamline content
transformation.</li>
</ul>
<p>By the end of the workshop, you will be able to leverage Quarto&rsquo;s
extensibility to create powerful, tailored solutions for your
documentation needs. Whether you have just worked on a few Quarto
projects or are an everyday user, this workshop will equip you with the
tools and knowledge to take your document workflows to the next level.</p>
</blockquote>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2025-10-27_conf-workshops-materials/thumbnail.png" length="192305" type="image/png" />
    </item>
    <item>
      <title>Quarto Wizard 1.0.0: Democratising Quarto Extension Management</title>
      <link>https://opensource.posit.co/blog/2025-10-20_quarto-wizard-1-0-0/</link>
      <pubDate>Mon, 20 Oct 2025 00:00:00 +0000</pubDate>
      <guid>https://opensource.posit.co/blog/2025-10-20_quarto-wizard-1-0-0/</guid>
      <dc:creator>Mickaël CANOUIL, _Ph.D._</dc:creator><description><![CDATA[<style>
.quarto-wizard {
   display: inline-block;
   aspect-ratio: 1 / 1;
   height: 1em;
   margin-bottom: -0.15em;
   mask-image: url('assets/media/quarto-wizard.svg');
   mask-size: contain;
   mask-repeat: no-repeat;
   mask-position: center;
   background-color: currentColor;
   vertical-align: baseline;
 }
 .hero-banner {
   border-radius: 1.5rem;
   box-shadow: 0 4px 24px #060c37;
   margin-bottom: 2rem;
 }
 </style>
<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Community Contribution</span>
</div>
<div class="callout-body">
<p>The <span class="quarto-wizard" title="Quarto Wizard Logo" aria-label="Quarto Wizard Logo"></span> Quarto Wizard extension and listing directory website are built and maintained by 






<a href="https://mickael.canouil.fr" target="_blank" rel="noopener">Mickaël CANOUIL, <em>Ph.D.</em></a>
.</p>
<p>In this post, he explains what it is and how it can help you manage Quarto extensions directly from Positron or Visual Studio Code.</p>
</div>
</div>
<img src="https://opensource.posit.co/blog/2025-10-20_quarto-wizard-1-0-0/featured.png" class="hero-banner" data-fig-align="center" data-fig-alt="Cartoon dog wizard wearing blue hat with red band holding magic wand creating HTML and CSS code scrolls in starry night scene." width="600" />
<p>I&rsquo;m absolutely thrilled to announce <strong><span class="quarto-wizard" title="Quarto Wizard Logo" aria-label="Quarto Wizard Logo"></span> Quarto Wizard 1.0.0</strong>, a groundbreaking extension for Visual Studio Code and Positron that transforms how you interact with the Quarto ecosystem.
If you&rsquo;ve ever found yourself wrestling with command-line extension management or struggling to discover the perfect template for your project, this tool is about to become your new best friend.</p>
<p>Install it today from the 






<a href="https://marketplace.visualstudio.com/items?itemName=mcanouil.quarto-wizard" target="_blank" rel="noopener">VS Code marketplace</a>
 or 






<a href="https://open-vsx.org/extension/mcanouil/quarto-wizard" target="_blank" rel="noopener">Open VSX Registry</a>
:</p>
<div class="grid gap-12 items-start md:grid-cols-2">
<div class="prose max-w-none">
<ul>
<li>
<p>Via VS Code or Positron Extensions view:</p>
<ul>
<li>Search for &ldquo;Quarto Wizard&rdquo;.</li>
<li>Click &ldquo;Install&rdquo;.</li>
</ul>
<p><img src="https://opensource.posit.co/blog/2025-10-20_quarto-wizard-1-0-0/assets/media/extensions-marketplace-light.png" title="Extensions View: Marketplace" class="light-content img-thumbnail rounded-3 border-light" data-fig-align="center" data-group="quarto-wizard-light" data-fig-alt="Visual Studio Code Extensions Marketplace showing Quarto Wizard search
results with install button.
" width="500" /></p>
</li>
</ul>
</div>
<div class="prose max-w-none">
<ul>
<li>
<p>Via the command line:</p>
<div class="panel-tabset">
<ul id="tabset-1" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-1-1">Visual Studio Code</a></li>
<li><a href="#tabset-1-2">Positron</a></li>
</ul>
<div id="tabset-1-1">
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-0">
  <div class="code-with-filename-label" id="code-filename-0"><span class="font-mono text-sm">Terminal</span></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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">code --install-extension mcanouil.quarto-wizard</span></span></code></pre></td></tr></table>
</div>
</div></div>
<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>Be sure to execute the command <em>Shell Command: Install &lsquo;code&rsquo; command in PATH</em> from Visual Studio Code&rsquo;s Command Palette (<code>Cmd-Shift-P</code> (mac), <code>Ctrl-Shift-P</code> (linux), <code>Ctrl-Shift-P</code> (windows)) if you haven&rsquo;t done so already.</p>
</div>
</div>
</div>
<div id="tabset-1-2">
<div class="code-block code-with-filename" role="group" aria-labelledby="code-filename-1">
  <div class="code-with-filename-label" id="code-filename-1"><span class="font-mono text-sm">Terminal</span></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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">positron --install-extension mcanouil.quarto-wizard</span></span></code></pre></td></tr></table>
</div>
</div></div>
<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>Be sure to execute the command <em>Shell Command: Install &lsquo;positron&rsquo; command in PATH</em> from Positron&rsquo;s Command Palette (<code>Cmd-Shift-P</code> (mac), <code>Ctrl-Shift-P</code> (linux), <code>Ctrl-Shift-P</code> (windows)) if you haven&rsquo;t done so already.</p>
</div>
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
<p>Quarto has revolutionised scientific and technical publishing by enabling reproducible documents that seamlessly blend code, narrative text, and visualisation.
However, one persistent friction point has been managing the rich and ever-growing ecosystem of extensions and templates&mdash;until now.</p>
<h2 id="quarto-wizard-your-gui-for-quarto-extensions"><span class="quarto-wizard" title="Quarto Wizard Logo" aria-label="Quarto Wizard Logo"></span> Quarto Wizard: Your GUI for Quarto extensions
</h2>
<p>I designed <strong>Quarto Wizard</strong> to address a fundamental challenge I&rsquo;ve observed in the community: whilst Quarto&rsquo;s command-line interface is powerful, many users prefer visual interfaces for discovering, installing, and managing extensions.</p>
<h3 id="seamless-ide-integration">Seamless IDE integration
</h3>
<p><strong>Quarto Wizard integrates beautifully with both the VS Code and Positron ecosystems</strong>, appearing as a dedicated icon in the Activity Bar alongside your other development tools.
This provides instant access to extension management without disrupting your coding flow, whether you&rsquo;re in Microsoft&rsquo;s VS Code or Posit&rsquo;s new Positron IDE.</p>
<p><img src="https://opensource.posit.co/blog/2025-10-20_quarto-wizard-1-0-0/assets/media/vscode-activity-bar-light.png" title="Quarto Wizard Explorer View (Light)" class="light-content img-thumbnail rounded-3 border-light" data-fig-align="center" data-group="quarto-wizard-light" data-fig-alt="Quarto Wizard Extensions Installed panel in Visual Studio Code showing
no extensions installed message with green Install Extensions button.
" width="500" /></p>
<p>The solution is <strong>multi-modal installation</strong>: you can now install extensions through multiple pathways that suit your workflow: from the command line, through the web directory, or via the <strong>Quarto Wizard</strong> GUI in your IDE.</p>
<div class="grid gap-12 items-center md:grid-cols-2">
<div class="prose max-w-none">
<p><img src="https://opensource.posit.co/blog/2025-10-20_quarto-wizard-1-0-0/assets/media/vscode-install-light.png" title="Quarto Wizard: Install Extensions (Light)" class="light-content img-thumbnail rounded-3 border-light" data-fig-align="center" data-group="quarto-wizard-light" data-fig-alt="Quarto Wizard extension selection dialog showing list of available
extensions with checkboxes including LIVE, HIGHLIGHT TEXT, GITHUB, and
other Quarto extensions.
" width="500" /></p>
</div>
</div>
<h3 id="intelligent-extension-management">Intelligent extension management
</h3>
<p>The <strong>&ldquo;Recently Installed Extensions&rdquo;</strong> feature helps track your workflow and easily reproduce project setups across different environments.
This is invaluable for researchers collaborating across multiple machines or teaching workshops where consistent setups are essential, regardless of whether team members use VS Code or Positron.</p>
<p>What makes this particularly powerful is that <strong>Quarto Wizard</strong> tracks which extensions were installed through its interface by adding <code>source</code> metadata to the <code>_extensions.yml</code> file, enabling seamless updates and removals.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>
This source tracking transforms extension maintenance from manual archaeology into an effortless workflow.
The extension maintains detailed metadata about installed extensions, enabling batch operations and dependency tracking.
The Explorer View provides a comprehensive overview of all installed extensions with visual indicators for updates and management options.</p>
<p><img src="https://opensource.posit.co/blog/2025-10-20_quarto-wizard-1-0-0/assets/media/vscode-update-light.png" title="Quarto Wizard: Explorer View Update (Light)" class="light-content img-thumbnail rounded-3 border-light" data-fig-align="center" data-group="quarto-wizard-light" data-fig-alt="Quarto Wizard Extensions Installed panel showing expanded iconify
extension details with update button and version information.
" width="500" /></p>
<h3 id="template-workflow-simplified">Template workflow simplified
</h3>
<p>Beyond extension management, I&rsquo;ve designed <strong>Quarto Wizard</strong> to ease the process of discovering and using document templates.
Once you&rsquo;ve selected a template, <strong>Quarto Wizard</strong> lets you customise and save the document.
The file is not created until you confirm, allowing you to adjust the filename and location.</p>
<div class="grid gap-12 items-center md:grid-cols-2">
<div class="prose max-w-none">
<p><img src="https://opensource.posit.co/blog/2025-10-20_quarto-wizard-1-0-0/assets/media/vscode-template-light.png" title="Quarto Wizard: Use Template (Light)" class="light-content img-thumbnail rounded-3 border-light" data-fig-align="center" data-group="quarto-wizard-light" data-fig-alt="Visual Studio Code showing Quarto Wizard with installed extensions list
and document editor displaying invoice template with YAML frontmatter.
" width="500" /></p>
</div>
</div>
<h2 id="powered-by-a-comprehensive-extension-directory">Powered by a comprehensive extension directory
</h2>
<h3 id="a-curated-catalogue-of-250-extensions">A curated catalogue of 250+ extensions
</h3>
<p>At the heart of <strong>Quarto Wizard</strong> lies the 






<a href="https://m.canouil.dev/quarto-extensions/" target="_blank" rel="noopener">Quarto Extensions directory (m.canouil.dev/quarto-extensions/)</a>
, a comprehensive listing I maintain that catalogues extensions from across the entire Quarto ecosystem.</p>
<p><img src="https://opensource.posit.co/blog/2025-10-20_quarto-wizard-1-0-0/assets/media/quarto-extensions-home-light.png" title="Mickaël CANOUIL&#39;s Quarto Extensions directory" class="light-content img-thumbnail rounded-3 border-light" data-fig-align="center" data-group="quarto-wizard-light" data-fig-alt="Quarto Extensions website displaying grid of extension cards including
webr, Reveal.js Clean theme, and Hikmah Academic templates.
" width="500" /></p>
<p>To date, it includes over 250 extensions contributed by the community, covering a vast array of functionalities from citation management to interactive visualisations.
This directory powers <strong>Quarto Wizard</strong>&rsquo;s discovery features, providing rich metadata about each extension including descriptions, licensing, version tags, and GitHub stars.
The directory is continuously updated through GitHub&rsquo;s API, ensuring you always have access to the latest extensions from the community.</p>
<h3 id="one-click-installation-from-the-web">One-click installation from the web
</h3>
<p>What&rsquo;s particularly exciting is that you can install extensions or use templates <strong>directly from the website itself</strong>.
Each extension listed at 






<a href="https://m.canouil.dev/quarto-extensions/" target="_blank" rel="noopener">m.canouil.dev/quarto-extensions/</a>
 includes multiple installation options: traditional command-line via terminal, or one-click installation through <strong>Quarto Wizard</strong> in VS Code, Positron, or VSCodium.
Simply browse the directory, find the extension you need, and choose your preferred installation method.
The website generates the appropriate commands or launches your IDE directly.
This flexibility means teams with mixed technical backgrounds can all access the same powerful extensions.</p>
<p><img src="https://opensource.posit.co/blog/2025-10-20_quarto-wizard-1-0-0/assets/media/quarto-extensions-modal-light.png" title="Mickaël CANOUIL&#39;s Quarto Extensions install modal" class="light-content img-thumbnail rounded-3 border-light" data-fig-align="center" data-group="quarto-wizard-light" data-fig-alt="Quarto Extensions website with Install Options popup showing manual
terminal command and Quarto Wizard installation options for Visual
Studio Code, Positron, and VSCodium.
" width="500" /></p>
<p>For example, you might want to add the 






<a href="https://github.com/mcanouil/quarto-iconify" target="_blank" rel="noopener"><strong>Iconify</strong></a>
 extension to access over 200,000 open source vector icons in your documents, or the 






<a href="https://github.com/mcanouil/quarto-animate" target="_blank" rel="noopener"><strong>Animate</strong></a>
 extension to bring your presentations to life with CSS animations.
Perhaps the 






<a href="https://github.com/mcanouil/quarto-spotlight" target="_blank" rel="noopener"><strong>Spotlight</strong></a>
 extension for Reveal.js catches your eye for creating dramatic Reveal.js presentations that highlight your mouse position.
All of these extensions (and hundreds more) are just a click away.</p>
<h3 id="template-discovery-made-easy">Template discovery made easy
</h3>
<p>Additionally, the Quarto Extensions directory excels at <strong>template discovery and deployment</strong> which is enhanced with powerful filtering options: you can sort by recently updated, filter by popularity, browse by categories (<em>i.e.</em>, Shortcodes, Filters, Formats, Projects, Reveal.js Plugins), or search for specific functionality.
Each extension clearly indicates whether it&rsquo;s a template with <strong>&ldquo;Use&rdquo;</strong> buttons alongside <strong>&ldquo;Install&rdquo;</strong> options.</p>
<p><img src="https://opensource.posit.co/blog/2025-10-20_quarto-wizard-1-0-0/assets/media/quarto-extensions-template-light.png" title="Mickaël CANOUIL&#39;s Quarto Extensions list view filtered by formats" class="light-content img-thumbnail rounded-3 border-light" data-fig-align="center" data-group="quarto-wizard-light" data-fig-alt="Quarto Extensions website in list view showing Template extensions with
install and use buttons.
" width="500" /></p>
<p>Whether you&rsquo;re crafting an academic paper using journal-specific formats, creating professional invoices with my 






<a href="https://github.com/mcanouil/quarto-invoice" target="_blank" rel="noopener"><strong>Invoice</strong></a>
 extension template, or building stunning presentations with themed templates like my 






<a href="https://github.com/mcanouil/quarto-revealjs-coeos" target="_blank" rel="noopener"><strong>Reveal.js Coeos</strong></a>
 extension, browsing available templates becomes as simple as scrolling through a curated gallery.</p>
<p>This directory creates a seamless experience: instead of manually searching GitHub repositories or memorising command-line syntax, you can browse hundreds of extensions with detailed information at your fingertips.
This transforms extension discovery from a treasure hunt into a curated shopping experience.</p>
<h2 id="addressing-real-workflow-friction">Addressing real workflow friction
</h2>
<p>I designed <strong>Quarto Wizard</strong> and the extension directory at 






<a href="https://m.canouil.dev/quarto-extensions/" target="_blank" rel="noopener">m.canouil.dev/quarto-extensions</a>
 to directly tackle several persistent Quarto pain points I&rsquo;ve encountered:</p>
<ul>
<li>
<p><strong>Discovery challenges</strong>: Finding relevant extensions in the growing ecosystem becomes intuitive through the visual browser interface powered by the comprehensive extensions directory.</p>
</li>
<li>
<p><strong>Command-line intimidation</strong>: Users who prefer graphical interfaces no longer need to memorise terminal commands.</p>
</li>
<li>
<p><strong>Document setup complexity</strong>: Template-based document initialisation eliminates manual YAML configuration.</p>
</li>
<li>
<p><strong>Extension maintenance</strong>: Updates, removals, and dependency management become point-and-click operations rather than command-line archaeology.</p>
</li>
<li>
<p><strong>Source tracking</strong>: <strong>Quarto Wizard</strong> automatically adds source metadata to installed extensions, enabling future updates and proper version management.</p>
</li>
<li>
<p><strong>Stable installations</strong>: <strong>Quarto Wizard</strong> installs extensions from GitHub releases/tags by default instead of the potentially unstable default branch, ensuring more reliable installations and more replicable environments.</p>
</li>
</ul>
<h2 id="perfect-for-diverse-use-cases">Perfect for diverse use cases
</h2>
<p>The extension shines across multiple scenarios:</p>
<ul>
<li>
<p><strong>Academic researchers</strong> can quickly install citation management tools, bibliography extensions like 






<a href="https://github.com/pandoc-ext/multibib" target="_blank" rel="noopener"><strong>Multibib</strong></a>
 or 






<a href="https://github.com/pandoc-ext/section-bibliographies" target="_blank" rel="noopener"><strong>Section Bibliographies</strong></a>
, and journal-specific formatting, whether they&rsquo;re using VS Code or Positron for their analysis work.</p>
</li>
<li>
<p><strong>Data scientists</strong> gain easy access to computational extensions like 






<a href="https://github.com/coatless/quarto-webr" target="_blank" rel="noopener"><strong>WebR</strong></a>
, visualisation tools, and interactive notebook capabilities.
This is particularly powerful in Positron, which is designed specifically for data science workflows.</p>
</li>
<li>
<p><strong>Technical writers</strong> can browse and install extensions for enhanced typography with for example the 






<a href="https://github.com/mcanouil/quarto-highlight-text" target="_blank" rel="noopener"><strong>Highlight Text</strong></a>
 extension for multi-format text highlighting, code highlighting, and advanced formatting options of their Reveal.js presentations with 






<a href="https://github.com/EmilHvitfeldt/quarto-revealjs-editable" target="_blank" rel="noopener"><strong>Reveal.js Editable</strong></a>
 extension in their preferred IDE.</p>
</li>
<li>
<p><strong>Workshop instructors</strong> can ensure all participants have consistent extension setups through guided installation processes, regardless of whether attendees prefer VS Code or Positron.</p>
</li>
</ul>
<h2 id="future-ready-architecture">Future-ready architecture
</h2>
<p>I&rsquo;ve built <strong>Quarto Wizard</strong> on robust foundations that ensure long-term reliability.
The extension integrates with GitHub&rsquo;s API through the 






<a href="https://m.canouil.dev/quarto-extensions/" target="_blank" rel="noopener">Quarto Extensions directory</a>
 for real-time metadata, includes attestation verification for security, and maintains full compatibility with both VS Code and Positron environments.</p>
<p>The modular architecture allows for future enhancements whilst maintaining backwards compatibility.
As the Quarto ecosystem continues expanding and as Positron evolves alongside VS Code, <strong>Quarto Wizard</strong> will support new extension types and project management workflows in both environments.</p>
<h2 id="getting-started-today">Getting started today
</h2>
<p>Begin your <strong>Quarto Wizard</strong> journey by installing the extension from the VS Code marketplace, the Open VSX Registry, or directly through your IDE&rsquo;s Extensions view.
Once installed, the <strong>Quarto Wizard</strong> icon appears in your Activity Bar, providing immediate access to extension management and project tools.</p>
<p>You have multiple paths to explore Quarto extensions:</p>
<ol>
<li><strong>Through Quarto Wizard</strong>: Click the <strong>Quarto Wizard</strong> icon in your IDE&rsquo;s Activity Bar and browse the integrated catalogue.</li>
<li><strong>Via the web directory</strong>: Visit 






<a href="https://m.canouil.dev/quarto-extensions/" target="_blank" rel="noopener">m.canouil.dev/quarto-extensions</a>
 where you can browse all extensions and install them directly from the website.
Each extension offers installation buttons for <strong>Quarto Wizard</strong>, VS Code, Positron, VSCodium, or traditional terminal commands.</li>
<li><strong>Traditional command-line</strong>: Use the familiar <code>quarto add</code> commands if you prefer.</li>
</ol>
<p>Try installing your first extension: perhaps 






<a href="https://github.com/mcanouil/quarto-iconify" target="_blank" rel="noopener"><strong>Iconify</strong></a>
 extension for comprehensive icon support or 






<a href="https://github.com/mcanouil/quarto-github" target="_blank" rel="noopener"><strong>GitHub</strong></a>
 for seamless GitHub linking.
Whether you click &ldquo;Install&rdquo; on the website, use <strong>Quarto Wizard</strong>&rsquo;s interface, or type commands in the terminal, the choice is yours.
The difference in experience compared to traditional command-line installation is immediately apparent, especially when browsing the visual catalogue with its filtering options, popularity indicators, and rich metadata.</p>
<p>I believe <strong>Quarto Wizard</strong> represents a significant step forward in making Quarto&rsquo;s powerful publishing capabilities accessible to users regardless of their comfort level with command-line tools or their choice of IDE.
By providing intuitive visual interfaces for complex operations, <strong>Quarto Wizard</strong> democratises access to the rich Quarto ecosystem whilst maintaining the flexibility and power that makes Quarto exceptional.</p>
<p>The future of reproducible publishing is here, and it&rsquo;s more accessible than ever in whichever modern development environment you prefer.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Quarto CLI does not natively track installation sources as of version 1.8.24 (






<a href="https://github.com/quarto-dev/quarto-cli/issues/11468" target="_blank" rel="noopener">quarto-dev/quarto-cli#11468</a>
).&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://opensource.posit.co/blog/2025-10-20_quarto-wizard-1-0-0/featured.png" length="2512689" type="image/png" />
    </item>
  </channel>
</rss>
