<?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://posit-open-source.netlify.app/languages/python/</link>
    <description>Recent content in Python on Posit Open Source</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Thu, 12 Mar 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://posit-open-source.netlify.app/languages/python/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Recreating Septa Transit Timetables in Python</title>
      <link>https://posit-open-source.netlify.app/blog/great-tables/septa-timetables/</link>
      <pubDate>Thu, 12 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/great-tables/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://posit-open-source.netlify.app/blog/great-tables/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://posit-open-source.netlify.app/blog/great-tables/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="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></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="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><p>Here&rsquo;s a quick preview of stops.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">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><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="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><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="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><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="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 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://posit-open-source.netlify.app/blog/great-tables/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://posit-open-source.netlify.app/blog/great-tables/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://posit-open-source.netlify.app/blog/great-tables/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>PDF Accessibility and Standards</title>
      <link>https://posit-open-source.netlify.app/blog/quarto/2026-03-05-pdf-accessibility-and-standards/</link>
      <pubDate>Thu, 05 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/quarto/2026-03-05-pdf-accessibility-and-standards/</guid>
      <dc:creator>Gordon Woodhull</dc:creator><description><![CDATA[<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Pre-release Feature</span>
</div>
<div class="callout-body">
<p>This feature is new in the upcoming Quarto 1.9 release. To use the feature now, you&rsquo;ll need to <a href="https://quarto.org/docs/download/prerelease.html" target="_blank" rel="noopener">download and install</a>
 the Quarto pre-release.</p>
</div>
</div>
<p>2025 was a big year for PDF accessibility. LaTeX and Typst both released support for PDF tagging and accessibility standards, just in time for new regulations in the <a href="https://en.wikipedia.org/wiki/European_Accessibility_Act" target="_blank" rel="noopener">EU</a>
 (June 2025) and <a href="https://accessible.org/ada-title-ii-web-accessibility/" target="_blank" rel="noopener">US</a>
 (April 2026).</p>
<p>Quarto 1.9 brings this support to you as a Quarto user.</p>
<h2 id="what-pdf-standards-do">What PDF Standards Do
</h2>
<p>Currently LaTeX supports the newer UA-2 standard, and Typst supports the older UA-1 standard. Typst is likely to have UA-2 support later in 2026.</p>
<p>Both standards instruct the PDF renderer to provide screen readers:</p>
<ul>
<li>The semantic structure of the text (title, heading, paragraph, figure, etc)</li>
<li>The natural reading order</li>
<li>Spatial coordinates for highlighting and assistive navigation</li>
<li>Required metadata such as title and language</li>
</ul>
<h2 id="how-to-enable-a-pdf-standard-in-quarto">How to enable a PDF Standard in Quarto
</h2>
<p>In Quarto 1.9, specify a PDF standard for your document or project with <code>pdf-standard</code></p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr>
<td style="text-align: left;"><div width="50.0%" data-layout-align="left">
<p><strong>PDF (LaTeX)</strong></p>
<div class="sourceCode" id="cb1"><pre class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">format</span><span class="kw">:</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">pdf</span><span class="kw">:</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">pdf-standard</span><span class="kw">:</span><span class="at"> ua-2</span></span></code></pre></div>
</div></td>
<td style="text-align: left;"><div width="50.0%" data-layout-align="left">
<p><strong>Typst</strong></p>
<div class="sourceCode" id="cb2"><pre class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="fu">format</span><span class="kw">:</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">typst</span><span class="kw">:</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">pdf-standard</span><span class="kw">:</span><span class="at"> ua-1</span></span></code></pre></div>
</div></td>
</tr>
</tbody>
</table>
<p><code>pdf-standard</code> takes a single standard name or list of standard names. PDF version is used if provided in the list, but otherwise inferred from the standard.</p>
<p>If you specify a PDF standard, Quarto first instructs LaTeX or Typst to use the standard when producing the PDF, and then validates the output PDF against the standard using veraPDF, an open-source PDF validation tool. If veraPDF is not installed, you&rsquo;ll get a warning but still receive a PDF &ndash; it just won&rsquo;t be validated.</p>
<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Installing veraPDF</span>
</div>
<div class="callout-body">
<p>To install veraPDF, you&rsquo;ll first need Java, then run:</p>
<p><strong>Terminal</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">quarto install verapdf
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>When a document passes validation, you&rsquo;ll see output like:</p>
<pre><code>[verapdf]: Validating my-document.pdf against PDF/UA-2... PASSED
</code></pre>
<h2 id="creating-accessible-pdfs">Creating accessible PDFs
</h2>
<p>Quarto&rsquo;s Markdown-based workflow handles many accessibility requirements automatically:</p>
<ul>
<li>Document metadata (title, author, date, language) flows into the PDF&rsquo;s built-in metadata fields.</li>
<li>The semantic structure of Markdown satisfies PDF tagging requirements. For Typst this is always enabled; for LaTeX it is enabled when you specify a standard that requires it.</li>
<li>Alt text for images is carried through to the PDF for screen readers.</li>
</ul>
<p>But you do need to make sure your document has:</p>
<ul>
<li>A <strong>title</strong> in the YAML front matter.</li>
<li><strong>Alt text for every image</strong>, specified with <code>fig-alt</code>. See <a href="https://quarto.org/docs/authoring/figures.html#alt-text" target="_blank" rel="noopener">Figures</a>
 for details.</li>
</ul>
<p>See the <a href="https://quarto.org/docs/output-formats/pdf-basics.html#accessibility-requirements" target="_blank" rel="noopener">LaTeX</a>
 and <a href="https://quarto.org/docs/output-formats/typst.html#accessibility-requirements" target="_blank" rel="noopener">Typst</a>
 documentation for more details.</p>
<h2 id="if-your-document-fails-validation">If your document fails validation
</h2>
<p>LaTeX does not perform validation during PDF generation, so if veraPDF validation fails, that&rsquo;s a warning, and you still get a partially-accessible PDF as long as you use <code>pdf-standard: ua-2</code>.</p>
<p>Typst fails and does not produce a PDF if its built-in validation fails during PDF generation. However, in Typst all accessibility features are on by default, so you can generate a partially-accessible PDF by rendering without <code>pdf-standard</code>.</p>
<h2 id="current-limitations">Current limitations
</h2>
<p>We ran our test suite &ndash; 188 LaTeX examples and 317 Typst examples &ndash; to find where Quarto PDFs do not yet pass UA-1 or UA-2, and where users will need to change their documents.</p>
<h3 id="latex">LaTeX
</h3>
<p>Margin content is the biggest structural blocker. If you use <code>.column-margin</code> divs, <code>cap-location: margin</code>, <code>reference-location: margin</code>, or <code>citation-location: margin</code>, the resulting PDF will not pass UA-2. The underlying <code>sidenotes</code> and <code>marginnote</code> LaTeX packages <a href="https://github.com/quarto-dev/quarto-cli/issues/14103" target="_blank" rel="noopener">do not cooperate with PDF tagging</a>
.</p>
<p>(Margin content does work with Typst and passes UA-1 &ndash; see <a href="https://quarto.org/docs/output-formats/typst.html#article-layout" target="_blank" rel="noopener">Typst Article Layout</a>
.)</p>
<p>There are smaller upstream issues in Pandoc, LaTeX, and LaTeX packages, <a href="https://github.com/quarto-dev/quarto-cli/pull/14097#issuecomment-3947653207" target="_blank" rel="noopener">documented here</a>
.</p>
<h3 id="typst">Typst
</h3>
<p>In our tests, Typst catches every UA-1 violation, and fails to generate the PDF. veraPDF did not detect any violation that Typst did not.</p>
<p>Typst also seems to do a very good job of generating UA-1 compliant output by default &ndash; almost all errors were due to missing titles or missing alt text.</p>
<p>However, we did discover that <a href="https://quarto.org/docs/books/book-output.html#typst-output" target="_blank" rel="noopener">Typst books</a>
 are not yet compliant. There is a <a href="https://github.com/flavio20002/typst-orange-template/issues/38" target="_blank" rel="noopener">structural problem with the Typst orange-book package</a>
 and we&rsquo;ll work with the maintainers to correct it.</p>
<h2 id="conclusion">Conclusion
</h2>
<p>Although Typst currently targets an the earlier UA-1 standard, today it seems to offer better PDF accessibility than LaTeX.</p>
<p>We expect PDF accessibility support to improve through the LaTeX ecosystem throughout 2026 as awareness of UA-2 and the new regulations spreads.</p>
<p>If you run into accessibility issues with PDF output, please search the <a href="https://github.com/orgs/quarto-dev/discussions" target="_blank" rel="noopener">Quarto discussions</a>
 and open a new one with the <code>accessibility</code> label for any issues you discover.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/quarto/2026-03-05-pdf-accessibility-and-standards/thumbnail.png" length="41719" type="image/png" />
    </item>
    <item>
      <title>Where Questions Become Queries: Meet querychat</title>
      <link>https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/</link>
      <pubDate>Thu, 22 Jan 2026 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/</guid>
      <dc:creator>Veerle Eeftink - Van Leemput</dc:creator><description><![CDATA[<p>You love data. And you love building dashboards with it, especially with your favourite tool Shiny. But even with a bullet-proof design, dozens of user stories, feedback loops, and adjustments, you also know that there are always questions that your dashboard leaves unanswered.</p>
<p>Let&rsquo;s say you developed a dashboard to display women&rsquo;s international soccer matches. You are proud of what you have built and you eagerly show it to a colleague:</p>
<p><strong>Colleague</strong>: &ldquo;Amazing! Can you show me the soccer matches for the FIFA World Cup only?&rdquo;</p>
<p><strong>You</strong>: &ldquo;Of course, let me filter it down for you and select the FIFA World Cup tournaments&rdquo;</p>
<p><strong>Colleague</strong>: &ldquo;Interesting, can you show me all the matches in which The Netherlands have played?&rdquo;</p>
<p><strong>You</strong>: &ldquo;Eh&hellip; Well, I could, but I just have to include a country filter in my dashboard then!&rdquo;</p>
<p>Right. How are we supposed to filter down to a specific country if there is no input for it? And what about getting summary statistics for countries or players? By now it becomes painfully clear that our soccer dashboard has its limits.</p>
<p>This is exactly the moment when <a href="https://posit-dev.github.io/querychat" target="_blank" rel="noopener"><code>querychat</code></a>
 becomes interesting. It is a multilingual package that allows you to chat with your data using natural language queries. No more clicking, no more limited filters, just you and your questions. And in this article, you&rsquo;re going to learn everything about it!</p>
<p>To bring <code>querychat</code> to life, we will keep returning to two examples:</p>
<ul>
<li>The classic diamonds dataset. After all, diamonds are a girl&rsquo;s best friend, and a data scientist&rsquo;s too! The familiar dataset offers a mix of variables such as cut, colour, clarity and price, which makes it ideal for all sorts of natural language questions. You might wonder about average prices for particular cuts, or you want to compare colours, look at how clarity affects value, or explore simple patterns in the data. In other words, it is a perfect playground for testing how well natural language queries behave on structured data.</li>
<li>SheScores, the soccer dashboard that you were so proud of earlier. This app originates from the shiny::conf(2024) workshop <a href="https://github.com/hypebright/shinyconf2024-shiny101" target="_blank" rel="noopener">&ldquo;Shiny 101: The Modular App Blueprint&rdquo;</a>
, although it has been tweaked to make it a bit more interesting and updated with matches through to November 2025.</li>
</ul>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-1" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-1-1">Python</a></li>
<li><a href="#tabset-1-2">R</a></li>
</ul>
<div id="tabset-1-1">
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/shescores-original-py.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
<div id="tabset-1-2">
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/shescores-original-r.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
</div>
<p>Both datasets set the stage nicely, so let&rsquo;s roll the ball and see how <code>querychat</code> plays. We&rsquo;re talking about soccer after all!</p>
<blockquote>
<p><strong>Full code available on GitHub</strong></p>
<p>Instead of copy-pasting the content of this blog into your favourite IDE, you can also <a href="https://github.com/hypebright/shescores-dashboard" target="_blank" rel="noopener">pull the project from GitHub</a>
 and follow along. All the code is available in both Python and R.</p>
</blockquote>
<blockquote>
<p><strong>Short on time?</strong></p>
<p>Jump straight to the <a href="#adding-querychat-to-your-existing-shiny-app">SheScores app with querychat</a>
 or visit the <a href="https://posit-dev.github.io/querychat" target="_blank" rel="noopener">querychat</a>
 website</p>
</blockquote>
<h1 id="hello-querychat">Hello, querychat
</h1>
<p>In short, <code>querychat</code> makes it easy to query data using natural language. It offers a drop-in component for Shiny, a console interface, and other programmatic building-blocks. You ask questions, <code>querychat</code> translates it to a SQL query, executes it, and returns the results. The results are available as a reactive data frame, which makes it easy to display or further process the data.</p>
<p><code>querychat</code> would solve the problem we encountered earlier. We can ask it any question we can imagine without constantly adding filters or other analysis. No country filter? No problem. And yes, that sounds as cool as it is!</p>
<p>So, what do we need?</p>
<p><code>querychat</code> is powered by a Large Language Model (LLM), so you need access to a model. You first need to register at an LLM provider that provides those models. You can choose any model you like, with two little &ldquo;restrictions&rdquo;: <a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener"><code>chatlas</code></a>
 (Python) or <a href="https://ellmer.tidyverse.org" target="_blank" rel="noopener"><code>ellmer</code></a>
 (R) supports it (which shouldn&rsquo;t be hard, because all the major models are) and the model has the ability to do tool calls.</p>
<blockquote>
<p><strong>Recommended models</strong></p>
<p>In this blog we&rsquo;ll use Claude Sonnet 4.5 from Anthropic. Other good choices would be GPT-4.1 (the current default for <code>querychat</code>) and Google Gemini 3.0 (as of November 2025).</p>
</blockquote>
<p>Once you&rsquo;ve made your choice and registered, you can get an API key. You need this key to authenticate with the LLM provider. One important note: never, ever hardcode the key directly into your script. You&rsquo;ll be amazed how many keys are publicly available on GitHub repos. Don&rsquo;t be that developer. As always with secrets, store it as an environment variable. Just note that the exact name of the key depends on the provider. For example, Anthropic expects <code>ANTHROPIC_API_KEY=yourkey</code>, while OpenAI uses <code>OPENAI_API_KEY=yourkey</code>.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-2" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-2-1">Python</a></li>
<li><a href="#tabset-2-2">R</a></li>
</ul>
<div id="tabset-2-1">
<p>In Python, the recommended approach is to create a <code>.env</code> file in your project folder:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">ANTHROPIC_API_KEY</span><span class="o">=</span><span class="n">yourkey</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>It&rsquo;s recommended to use the <code>dotenv</code> package to load the <code>.env</code> file into your environment:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">dotenv</span> <span class="kn">import</span> <span class="n">load_dotenv</span>
</span></span><span class="line"><span class="cl"><span class="n">load_dotenv</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>To keep the demo code concise we&rsquo;ll omit these lines from subsequent code examples.</p>
</div>
<div id="tabset-2-2">
<p>In R, environment variables are typically stored in a <code>.Renviron</code> file. You can create this file in your project root or in your home directory (<code>~/.Renviron</code>). Or, if you want to make it yourself really easy: you can also open/edit the relevant file with <code>usethis::edit_r_environ()</code>.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">ANTHROPIC_API_KEY</span><span class="o">=</span><span class="n">yourkey</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>Of course we can&rsquo;t use <code>querychat</code> without installing it, so that&rsquo;s the next step:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-3" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-3-1">Python</a></li>
<li><a href="#tabset-3-2">R</a></li>
</ul>
<div id="tabset-3-1">
<p>For Python, <code>querychat</code> is available on PyPI, so you can install it easily with <code>pip</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install querychat
</span></span></code></pre></td></tr></table>
</div>
</div><p>Or, if you&rsquo;re using <code>uv</code>, add it like so:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">uv add querychat
</span></span></code></pre></td></tr></table>
</div>
</div><p>Once installed, import it like this:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">querychat</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-3-2">
<p>You can get <code>querychat</code> from CRAN using:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;querychat&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Alternatively, if you want the latest development version, you can install <code>querychat</code> from GitHub using:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">pak</span><span class="o">::</span><span class="nf">pak</span><span class="p">(</span><span class="s">&#34;posit-dev/querychat/pkg-r&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Once installed, load the package as usual:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">querychat</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>Wouldn&rsquo;t it be great if you can use <code>querychat</code> straight away without much code? Just to see what it&rsquo;s all about? Luckily you can with the &ldquo;quick launch&rdquo; Shiny app! You can simply call <code>app()</code> which spins up an app with <code>querychat</code> chat interface. Let&rsquo;s try it out for our diamonds dataset:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-4" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-4-1">Python</a></li>
<li><a href="#tabset-4-2">R</a></li>
</ul>
<div id="tabset-4-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">seaborn</span> <span class="kn">import</span> <span class="n">load_dataset</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">querychat</span> <span class="kn">import</span> <span class="n">QueryChat</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">diamonds</span> <span class="o">=</span> <span class="n">load_dataset</span><span class="p">(</span><span class="s2">&#34;diamonds&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="n">diamonds</span><span class="p">,</span> <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span> <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">qc</span><span class="o">.</span><span class="n">app</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>qc = QueryChat(...)</code> creates an instance of the <code>QueryChat</code> class. You pass in the dataset, give it a name and specify the model client (powered by <a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener"><code>chatlas</code></a>
). <code>qc.app()</code> returns the web app that lets you explore the diamonds data using natural language questions.</p>
<p>Want to try this with a different provider and/or model? No problem, just change the <code>client</code> argument accordingly. For example, to use GPT-4.1 from OpenAI, you would write: <code>client=&quot;openai/gpt-4.1&quot;</code>. You can learn more about the different options in the <a href="https://posit-dev.github.io/querychat/py/models.html" target="_blank" rel="noopener"><code>querychat</code> documentation</a>
.</p>
<p>To run this app, you need to save the code above in a file (and call it <a href="http://diamonds-app.py" target="_blank" rel="noopener"><code>diamonds-app.py</code></a>
 for example) and run it like so:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">shiny</span> <span class="n">run</span> <span class="o">--</span><span class="n">reload</span> <span class="n">diamonds</span><span class="o">-</span><span class="n">app</span><span class="o">.</span><span class="n">py</span> 
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-4-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ellmer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ggplot2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">querychat_app</span><span class="p">(</span><span class="n">diamonds</span><span class="p">,</span> <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Or, alternatively, you could write:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ellmer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ggplot2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span><span class="o">$</span><span class="nf">app</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Both result in the same outcome, the first one is just a simplified version.</p>
<p><code>QueryChat$new()</code> creates the R6 object, taking the dataset, a table name and the model client (which will be passed to <code>ellmer::chat()</code>). Calling <code>qc$app()</code> then launches the Shiny app so you can query the diamonds dataset in plain English.</p>
<p>Want to change the provider and/or model? No problem, just change the <code>client</code> argument accordingly. For example, to use GPT-4.1 from OpenAI, you would write: <code>client = &quot;openai/gpt-4.1&quot;</code>. You can learn more about the different options in the <a href="https://posit-dev.github.io/querychat/r/index.html#use-a-different-llm-provider" target="_blank" rel="noopener"><code>querychat</code> documentation</a>
.</p>
</div>
</div>
<p>The result: a Shiny app that allows users to interact with a data source using natural language queries.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-5" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-5-1">Python</a></li>
<li><a href="#tabset-5-2">R</a></li>
</ul>
<div id="tabset-5-1">
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/diamonds-py.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
<div id="tabset-5-2">
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/diamonds-r.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<blockquote>
<p><strong>Custom branding</strong></p>
<p>Do you notice the nice green touches and custom font in this demo app? That&rsquo;s because the project we&rsquo;ll be using in this article uses <a href="https://posit-dev.github.io/brand-yml/" target="_blank" rel="noopener">brand.yml</a>
: a simple, portable YAML file that codifies brand guidelines into a format that can be used by Quarto, Python and R. And in this case, it works beautifully for Shiny. Curious to see what such a <code>_brand.yml</code> file looks like? You can check it out <a href="https://github.com/hypebright/shescores-dashboard/blob/0cd4e3f3ae52bcf4a39f7d63fb26e555de9a6b5e/_brand.yml" target="_blank" rel="noopener">here</a>
.</p>
</blockquote>
</div>
</div>
<p>You can ask the diamonds dataset some surprisingly rich questions, and <code>querychat</code> handles them with ease. A simple place to begin is something like &ldquo;show the 10 most expensive diamonds&rdquo;. It produces straightforward SQL and updates the table in the app instantly.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">diamonds</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">price</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>The question may be simple, but it already highlights the convenience of <code>querychat</code>. Without it, users would need to sort the table manually or rely on a picker, slider or some other input that filters the data for this very specific request.</p>
<p>Things get more interesting when we introduce a calculation. Asking &ldquo;can you show the 20 biggest diamonds, based on volume?&rdquo; still results in simple SQL, but the output now includes an extra column, volume, which appears neatly in the app.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Calculate volume (x * y * z) and sort by largest volume
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="p">,</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="n">x</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">z</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">volume</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">diamonds</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">volume</span><span class="w"> </span><span class="k">DESC</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="k">LIMIT</span><span class="w"> </span><span class="mi">20</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>From there, we can try some grouping and window functions. &ldquo;Within each cut, what is the most expensive diamond?&rdquo; works perfectly, showing the grouped results along with the necessary window function behind the scenes.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Find the most expensive diamond for each cut type
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">cut</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">MAX</span><span class="p">(</span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">max_price</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">-- Get the details of the diamond with the max price for each cut
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ARG_MAX</span><span class="p">(</span><span class="n">carat</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">carat</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ARG_MAX</span><span class="p">(</span><span class="n">color</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">color</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ARG_MAX</span><span class="p">(</span><span class="n">clarity</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">clarity</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ARG_MAX</span><span class="p">(</span><span class="n">depth</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">depth</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ARG_MAX</span><span class="p">(</span><span class="s2">&#34;table&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">table_pct</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">diamonds</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">cut</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">max_price</span><span class="w"> </span><span class="k">DESC</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">        cut max_price carat color clarity depth table_pct
</span></span><span class="line"><span class="cl">1   Premium     18823  2.29     I     VS2  60.8        60
</span></span><span class="line"><span class="cl">2 Very Good     18818  2.00     G     SI1  63.5        56
</span></span><span class="line"><span class="cl">3     Ideal     18806  1.51     G      IF  61.7        55
</span></span><span class="line"><span class="cl">4      Good     18788  2.80     G     SI2  63.8        58
</span></span><span class="line"><span class="cl">5      Fair     18574  2.01     G     SI1  70.6        64
</span></span></code></pre></td></tr></table>
</div>
</div><p>Now, for a grand finale, we can throw in a more analytical question: &ldquo;are larger physical dimensions always associated with higher price?&rdquo; This one generates a slightly more complex SQL query, but it also comes with a clear and helpful explanation.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Calculate correlation between physical dimensions and price
</span></span></span><span class="line"><span class="cl"><span class="c1">-- Also look for outliers where small diamonds cost more than large ones
</span></span></span><span class="line"><span class="cl"><span class="k">WITH</span><span class="w"> </span><span class="n">dimension_stats</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">SELECT</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">carat</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">x</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="p">,</span><span class="w"> </span><span class="n">z</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">-- Calculate volume as a proxy for overall size
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">x</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">z</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">volume</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">price</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c1">-- Rank by volume and price separately
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">RANK</span><span class="p">()</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">z</span><span class="w"> </span><span class="k">DESC</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">volume_rank</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">RANK</span><span class="p">()</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">price</span><span class="w"> </span><span class="k">DESC</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">price_rank</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">FROM</span><span class="w"> </span><span class="n">diamonds</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">WHERE</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">z</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w">  </span><span class="c1">-- Exclude malformed records
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> 
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">CORR</span><span class="p">(</span><span class="n">x</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">x_price_correlation</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">CORR</span><span class="p">(</span><span class="n">y</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">y_price_correlation</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">CORR</span><span class="p">(</span><span class="n">z</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">z_price_correlation</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="n">CORR</span><span class="p">(</span><span class="n">volume</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">volume_price_correlation</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c1">-- Count cases where rank differences are significant (outliers)
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">COUNT</span><span class="p">(</span><span class="k">CASE</span><span class="w"> </span><span class="k">WHEN</span><span class="w"> </span><span class="n">volume_rank</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">price_rank</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">1000</span><span class="w"> </span><span class="k">THEN</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">END</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">large_cheap_diamonds</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">COUNT</span><span class="p">(</span><span class="k">CASE</span><span class="w"> </span><span class="k">WHEN</span><span class="w"> </span><span class="n">price_rank</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">volume_rank</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">1000</span><span class="w"> </span><span class="k">THEN</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="k">END</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">small_expensive_diamonds</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">dimension_stats</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">x_price_correlation        : 0.8872314
</span></span><span class="line"><span class="cl">y_price_correlation        : 0.8678642
</span></span><span class="line"><span class="cl">z_price_correlation        : 0.8682064
</span></span><span class="line"><span class="cl">volume_price_correlation   : 0.9042547
</span></span><span class="line"><span class="cl">large_cheap_diamonds       : 21082
</span></span><span class="line"><span class="cl">small_expensive_diamonds   : 21065
</span></span></code></pre></td></tr></table>
</div>
</div><p>The conclusion?</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">Answer: No, larger physical dimensions are NOT always associated with higher price
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">While there&#39;s a strong positive correlation (~0.90 for overall volume), quality factors can override size.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Correlation Analysis
</span></span><span class="line"><span class="cl">Volume ↔ Price: 0.90 correlation
</span></span><span class="line"><span class="cl">X dimension ↔ Price: 0.89 correlation
</span></span><span class="line"><span class="cl">Y dimension ↔ Price: 0.87 correlation
</span></span><span class="line"><span class="cl">Z dimension ↔ Price: 0.87 correlation
</span></span><span class="line"><span class="cl">This means physical size explains about 80-90% of price variation, but not all of it.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The Key Finding
</span></span><span class="line"><span class="cl">I found over 21,000 cases where small diamonds are more expensive than much larger ones, and vice versa. This happens because quality factors (cut, color, clarity) matter significantly.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Concrete Example
</span></span><span class="line"><span class="cl">Here&#39;s a smaller diamond that costs more per unit volume than typical large diamonds:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Small, premium diamond: 74mm³ volume, 1.41 carat, $9,752 (Ideal cut, H color, VS1 clarity)
</span></span><span class="line"><span class="cl">This costs $131 per mm³ of volume
</span></span><span class="line"><span class="cl">Compare this to larger diamonds with lower quality that sell for much less per unit volume due to poor cut, color, or clarity grades.
</span></span></code></pre></td></tr></table>
</div>
</div><p>To encourage further exploration, <code>querychat</code> presents suggestions such as comparing how cut quality affects the price to carat ratio, finding diamonds where clarity has the biggest impact on price, or checking the price difference between the best and worst colour grades for similar sized stones. Yeah, that&rsquo;s right, you don&rsquo;t even have to come up with questions yourself.</p>
<p>Some questions result in a filtered table, others result in an explanation with results in the chat window. <code>querychat</code> figures out, based on your question, whether you want an answer straight away, or want to inspect the filtered data yourself. Pretty cool that this only took a few lines of code.</p>
<blockquote>
<p><strong>LLMs can make mistakes</strong></p>
<p>Note that it&rsquo;s still an LLM that generates these queries. LLMs can make mistakes. The nice thing about <code>querychat</code> though, is that you can inspect the SQL query yourself.</p>
<p>Most issues fall into two categories:</p>
<ul>
<li>Query errors: the SQL may fail to run or may not fully reflect what you intended. When it fails, the model will often try again. In this case, giving more context about the data can help.</li>
<li>Result errors: even when the query is correct, the model may misunderstand or oversimplify the results, especially if that result is large or complex. The result might be that key insights are missed or misinterpreted.</li>
</ul>
</blockquote>
<h1 id="why-this-matters-reliability-transparency-reproducibility">Why this matters: reliability, transparency, reproducibility
</h1>
<p>What makes the &ldquo;quick launch&rdquo; app so powerful is that it is far more than a chat window sitting on top of a dataset. Think back to the questions we explored earlier. We filtered, sorted, computed new columns, grouped data and used window functions. We also looked at analytical relationships without writing a single line of code. And that is only the beginning. If you want to go further, you can hunt for anomalies, create categories, build benchmarks or explore almost any analysis you can imagine. The key is that you never have to think about <em>how</em> to do it. You just ask.</p>
<p>And yes, you could ask all those questions in a typical LLM chat tool. But <code>querychat</code> is different. You are not relying on the model to <em>invent</em> answers or reason about the data internally. Instead, every single question is translated into SQL, executed on the actual dataset and returned exactly as the data dictates. And crucially, the SQL is always shown, so you can see precisely what is being run.</p>
<p>This brings four important benefits:</p>
<ul>
<li><strong>Reliability:</strong> the LLM does not analyse or transform the raw data itself. It only generates SQL text. <code>querychat</code> handles the execution of that SQL via tool calling so all results come from the real data engine, not from the model&rsquo;s internal guesswork.</li>
<li><strong>Transparency:</strong> every query reveals the full SQL statement. Nothing is hidden, nothing is adjusted, and you always know how the answer was produced.</li>
<li><strong>Reproducibility:</strong> since every SQL query is visible, analyses can be reused, shared, and audited.</li>
<li><strong>Safety</strong>: <code>querychat</code>&rsquo;s tools are designed with read-only actions in mind, meaning the LLM is essentially unable to perform destructive actions. However, to fully guarantee no destructive actions on your production database, make sure <code>querychat</code>&rsquo;s database permissions are read-only!</li>
</ul>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-6" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-6-1">Python</a></li>
<li><a href="#tabset-6-2">R</a></li>
</ul>
<div id="tabset-6-1">
<img src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/diamonds-drop-py.png" style="width:50.0%" data-fig-align="center" />
</div>
<div id="tabset-6-2">
<img src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/diamonds-drop-r.png" style="width:50.0%" data-fig-align="center" />
</div>
</div>
<h1 id="how-it-works-tool-calling">How it works: tool calling
</h1>
<p>If you read <a href="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/../../../blog/shiny/shiny-side-of-llms-part-2/#when-llms-guess-tools-know">The Shiny Side of LLMs</a>
 blog series, you already know a bit about tool calling. In that series we explored how LLMs can call external tools instead of trying to do everything themselves, and <code>querychat</code> is a very practical example of this idea in action.</p>
<p>Tool calling is essentially a bridge between an LLM and your Python or R session. The model does not execute code. Instead, it requests your Python or R session execute a certain function with certain inputs (e.g., a SQL statement). Once Python or R performs the execution, the result is then passed back to the model for interpretation.</p>
<p>So how does tool calling help us here? Well, LLMs have their strengths and weaknesses. They are not great at counting things, creating data summaries or doing basic calculations. But they <em>are</em> excellent at taking natural language and turning it into structured code. SQL that is. This SQL is then executed through a tool call: a function that executes the (read only) SQL. In both Python and R this means the LLM can express your question as a request to call a function with precise arguments, and the host language performs the real work. This makes it all reliable, reproducible, and safe (read only SQL).</p>
<p>Given that, generally speaking, LLMs are very good at writing SQL, it makes perfect sense to ask one to translate your natural language questions into SQL queries. In order to generate SQL that can be executed, the LLM does need to know something about your data: which columns are there, what do they mean, and what type are they? This <strong>schema information</strong> is shared with the model, but not the raw data. With this information, it produces an SQL query as a tool call. Now, to run SQL you need a database engine. <code>querychat</code>&rsquo;s weapon of choice: <a href="https://duckdb.org" target="_blank" rel="noopener">DuckDB</a>
. Basically, our diamonds dataset gets turned into a DuckDB database, and generated SQL queries are executed on this database. Then the results are passed back to the LLM so it can say some interesting things about it.</p>
<p>To summarise:</p>
<p>prompt → SQL query → tool call → execute SQL query → return results<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>Tool calling is worth emphasising because it gives us a controlled and predictable interface between LLMs and real code execution. Instead of writing and maintaining your own custom tools, you can turn to <code>querychat</code>. It already provides the functions needed to turn natural language into reliable SQL that Python or R can execute with confidence.</p>
<h1 id="customising-querychat-from-chat-to-toolkit">Customising <code>querychat</code>: from chat to toolkit
</h1>
<p>Alright, enough talking. You now know what <code>querychat</code> can do, and how it does it (high-level). You might even have brilliant ideas for your next app&hellip; In that case it would be nice to know how to build your own app with <code>querychat</code>. The Diamonds &ldquo;quick launch&rdquo; app from earlier, that you run with <code>qc.app()</code> (Python) or <code>qc$app()</code> (R), consists of a handful of methods that you can find in <code>querychat</code>, and we&rsquo;re going to use them directly.</p>
<p>The main component is the <code>QueryChat</code> object, which has different arguments and methods.</p>
<h2 id="querychat-object">QueryChat object
</h2>
<p>You call <code>QueryChat</code> to initialise a <code>QueryChat</code> object (often called <code>qc</code>), like so:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-7" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-7-1">Python</a></li>
<li><a href="#tabset-7-2">R</a></li>
</ul>
<div id="tabset-7-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-7-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span><span class="kc">...</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>You can pass <code>QueryChat</code> several arguments:</p>
<ul>
<li>
<p><code>data_source</code> and <code>table_name</code></p>
<p>These are the two most important arguments: they specify your data source and the name of your table that can be used for the SQL queries. The data source can be your data frame, a tibble, a table or any other Python or R data object, and the table name is usually the variable name of your data frame. In our example our <code>data_source</code> was <code>diamonds</code>, which we also stored in a variable called <code>diamonds</code>.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-8" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-8-1">Python</a></li>
<li><a href="#tabset-8-2">R</a></li>
</ul>
<div id="tabset-8-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="n">diamonds</span><span class="p">,</span> <span class="s2">&#34;diamonds&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-8-2">
<p>Generally, in R, the table name isn&rsquo;t required as it can be inferred from the variable name. However, it is required when you use a database connection, which we&rsquo;ll use later.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>You&rsquo;re not limited to data objects: you can also pass a database connection to <code>data_source</code>. We&rsquo;ll come back to that later.</p>
</li>
<li>
<p><code>client</code></p>
<p>We used the <code>client</code> argument before: we use it to tell <code>querychat</code> that we want to use Claude Sonnet 4.5 (or any other model). This gets us back at the starting point of our Diamonds &ldquo;quick launch&rdquo; app.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-9" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-9-1">Python</a></li>
<li><a href="#tabset-9-2">R</a></li>
</ul>
<div id="tabset-9-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="n">diamonds</span><span class="p">,</span> <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span> <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Alternatively, you can set the client in options the <code>QUERYCHAT_CLIENT</code> environment variable.</p>
</div>
<div id="tabset-9-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Alternatively, you can set the client in options with <code>options(querychat.client = &quot;claude/claude-sonnet-4-5&quot;)</code>.</p>
</div>
</div>
</li>
<li>
<p><code>id</code></p>
<p>This is an optional argument, and if it&rsquo;s not given it&rsquo;s derived from the <code>table_name</code>. When to use it? If you want to work with <a href="https://posit-dev.github.io/querychat/py/build.html#multiple-datasets" target="_blank" rel="noopener">multiple QueryChat instances</a>
, for example.</p>
</li>
<li>
<p><code>greeting</code></p>
<p>A nice greeting message to display to your users. It&rsquo;s the first thing your users see, so you better make it good! If not provided, one is generated at the start. While this one looks fine on first sight, it&rsquo;s rather slow and wasteful (it costs extra tokens because it&rsquo;s generated every single time). Also, because it&rsquo;s generated on the fly, it&rsquo;s far from consistent. Earlier, when we ran the &ldquo;quick launch&rdquo; app, you already might have noticed that it generated a warning message:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-10" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-10-1">Python</a></li>
<li><a href="#tabset-10-2">R</a></li>
</ul>
<div id="tabset-10-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">Warning: No greeting provided; the LLM will be invoked at conversation start to generate one. For faster startup, lower cost, and determinism, please save a greeting and pass it to init(). You can also use `querychat.greeting()` to help generate a greeting.
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-10-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">Warning message:
</span></span><span class="line"><span class="cl">No greeting provided; the LLM will be invoked at conversation start to generate one.
</span></span><span class="line"><span class="cl">• For faster startup, lower cost, and determinism, please save a greeting and pass it to QueryChat$new().
</span></span><span class="line"><span class="cl">ℹ You can generate a greeting with $generate_greeting(). 
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>So yes, we need a greeting! You can add your own greeting by providing a string in Markdown format.</p>
<p>Some inspiration on what you can put in there: basic instructions, suggestions for filtering, sorting or analysing the data, addressing data privacy concerns, or letting people know where they can get support if something goes wrong.</p>
<p>And if you don&rsquo;t feel like writing your own greeting, or if you feel uninspired, you can let <code>querychat</code> handle it! Simply use <code>generate_greeting()</code>:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-11" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-11-1">Python</a></li>
<li><a href="#tabset-11-2">R</a></li>
</ul>
<div id="tabset-11-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="n">diamonds</span><span class="p">,</span> <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span> <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Generate a greeting with help from the LLM</span>
</span></span><span class="line"><span class="cl"><span class="n">greeting_text</span> <span class="o">=</span> <span class="n">qc</span><span class="o">.</span><span class="n">generate_greeting</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save it</span>
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;diamonds_greeting.md&#34;</span><span class="p">,</span> <span class="s2">&#34;w&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">greeting_text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Then use the saved greeting in your app</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span><span class="n">diamonds</span><span class="p">,</span> <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span> <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">,</span> <span class="n">greeting</span><span class="o">=</span><span class="n">Path</span><span class="p">(</span><span class="s2">&#34;diamonds_greeting.md&#34;</span><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Which give us this nice greeting:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gh"># Welcome! 👋
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">I&#39;m here to help you explore and understand your diamonds dataset. 
</span></span><span class="line"><span class="cl">I can filter and sort the data, answer questions with SQL queries, 
</span></span><span class="line"><span class="cl">and help you discover insights.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Here are some ideas to get started:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Explore the data
</span></span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Show me the most expensive diamonds&lt;/span&gt;
</span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;What&#39;s the average price of diamonds by cut quality?&lt;/span&gt;
</span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;How many diamonds are in each clarity category?&lt;/span&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Filter and analyze
</span></span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Show only ideal cut diamonds over 2 carats&lt;/span&gt;
</span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Filter to diamonds with the best color grades (D, E, F)&lt;/span&gt;
</span></span><span class="line"><span class="cl"><span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Which cut has the highest average price per carat?&lt;/span&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">What would you like to explore first?
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-11-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Generate a greeting with help from the LLM</span>
</span></span><span class="line"><span class="cl"><span class="n">greeting_text</span> <span class="o">&lt;-</span> <span class="n">qc</span><span class="o">$</span><span class="nf">generate_greeting</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Save it</span>
</span></span><span class="line"><span class="cl"><span class="nf">writeLines</span><span class="p">(</span><span class="n">greeting_text</span><span class="p">,</span> <span class="s">&#34;diamonds_greeting.md&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Then use the saved greeting in your app</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">greeting</span> <span class="o">=</span> <span class="s">&#34;diamonds_greeting.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Which give us this nice greeting:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gh"># Welcome to the Diamond Dashboard! 💎
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">I&#39;m here to help you explore and analyze this dataset of diamond characteristics and prices. 
</span></span><span class="line"><span class="cl">I can filter and sort the data, answer questions, and help you discover interesting patterns.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Here are some ideas to get started:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Explore the Data**</span>
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;What&#39;s the average price of diamonds in this dataset?&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;How many diamonds are there in each clarity category?&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Which diamond has the highest price?&lt;/span&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Filter and Sort**</span>
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Show me only Ideal cut diamonds&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Filter to diamonds over 2 carats and sort by price&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Show me the most expensive diamonds with VS1 clarity&lt;/span&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">What would you like to explore?
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>You can see that the generated greeting contains a span HTML tag: <code>&lt;span class=&quot;suggestion&quot;&gt;…&lt;/span&gt;</code>. If you make your own greeting, you can use this tag to automatically populate the chatbox when it&rsquo;s being clicked.</p>
</li>
<li>
<p><code>data_description</code></p>
<p><code>querychat</code> automatically helps the LLM by providing things like the column names and datatypes of your data (the <strong>schema information</strong>), but results can be even more accurate when you provide additional context in the data description. There&rsquo;s no specific format needed, and you can add whatever information you like. To give some inspiration, this is what we could say about the diamonds dataset:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gh"># Diamonds Dataset Description
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">A structured dataset describing physical and quality attributes of individual diamonds,
</span></span><span class="line"><span class="cl">commonly used to model or predict price.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Fields
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> carat (float) — Diamond weight  
</span></span><span class="line"><span class="cl"><span class="k">-</span> cut (category) — Cut quality: Fair, Good, Very Good, Premium, Ideal  
</span></span><span class="line"><span class="cl"><span class="k">-</span> color (category) — Color grade from D (best) to J (worst)  
</span></span><span class="line"><span class="cl"><span class="k">-</span> clarity (category) — Clarity grades: I1, SI2, SI1, VS2, VS1, VVS2, VVS1, IF  
</span></span><span class="line"><span class="cl"><span class="k">-</span> depth (float) — Total depth percentage  
</span></span><span class="line"><span class="cl"><span class="k">-</span> table (float) — Table width percentage  
</span></span><span class="line"><span class="cl"><span class="k">-</span> price (int) — Price in USD  
</span></span><span class="line"><span class="cl"><span class="k">-</span> x (float) — Length in mm  
</span></span><span class="line"><span class="cl"><span class="k">-</span> y (float) — Width in mm  
</span></span><span class="line"><span class="cl"><span class="k">-</span> z (float) — Depth in mm
</span></span></code></pre></td></tr></table>
</div>
</div><p>We can save this in a Markdown file and pass it on to <code>querychat</code>:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-12" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-12-1">Python</a></li>
<li><a href="#tabset-12-2">R</a></li>
</ul>
<div id="tabset-12-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">greeting</span><span class="o">=</span><span class="n">Path</span><span class="p">(</span><span class="s2">&#34;diamonds_greeting.md&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_description</span><span class="o">=</span><span class="n">Path</span><span class="p">(</span><span class="s2">&#34;diamonds_data_description.md&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-12-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">greeting</span> <span class="o">=</span> <span class="s">&#34;diamonds_greeting.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">data_description</span> <span class="o">=</span> <span class="s">&#34;diamonds_data_description.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
</li>
<li>
<p><code>extra_instructions</code></p>
<p>For further tweaking the LLMs behaviour you can use <code>extra_instructions</code>. You can go nuts here: make it talk like a pirate, use an emoji in every sentence, or use an annoying amount of diamond-related phrases. You can also use this section for more practical guidance like notes on preferred spelling, tone, or handling of sensitive terms. For example:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="k">-</span> Assume the user doesn&#39;t know much about diamonds: 
</span></span><span class="line"><span class="cl">  keep explanations simple and accessible.
</span></span><span class="line"><span class="cl"><span class="k">-</span> When describing diamond attributes, default to plain English. 
</span></span><span class="line"><span class="cl">  If a term is highly technical, include a short clarification.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Maintain consistent spelling in British English.
</span></span></code></pre></td></tr></table>
</div>
</div><div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-13" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-13-1">Python</a></li>
<li><a href="#tabset-13-2">R</a></li>
</ul>
<div id="tabset-13-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">greeting</span><span class="o">=</span><span class="n">Path</span><span class="p">(</span><span class="s2">&#34;diamonds_greeting.md&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_description</span><span class="o">=</span><span class="n">Path</span><span class="p">(</span><span class="s2">&#34;diamonds_data_description.md&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">extra_instructions</span><span class="o">=</span><span class="n">Path</span><span class="p">(</span><span class="s2">&#34;diamonds_extra_instructions.md&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-13-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">greeting</span> <span class="o">=</span> <span class="s">&#34;diamonds_greeting.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">data_description</span> <span class="o">=</span> <span class="s">&#34;diamonds_data_description.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">extra_instructions</span> <span class="o">=</span> <span class="s">&#34;diamonds_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
</li>
<li>
<p><code>categorical_threshold</code></p>
<p>This threshold applies to text columns, and sets the maximum number of unique values to consider it as a categorical variable. The default is 20.</p>
</li>
<li>
<p><code>prompt_template</code></p>
<p>The <code>prompt_template</code> is a more advanced parameter to provide a custom prompt template. If you don&rsquo;t provide it, <code>querychat</code> will use the built-in prompt, which we&rsquo;ll inspect a little bit closer later.</p>
</li>
</ul>
<p>Besides arguments, you can also call methods on the <code>QueryChat</code> object. One of them is <code>cleanup()</code>, which releases any resources (e.g. database connections) associated with the data source. You should call this when you are done using the <code>QueryChat</code> object to avoid resource leaks:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-14" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-14-1">Python</a></li>
<li><a href="#tabset-14-2">R</a></li>
</ul>
<div id="tabset-14-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">qc</span><span class="o">.</span><span class="n">cleanup</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-14-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">qc</span><span class="o">$</span><span class="nf">cleanup</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>That&rsquo;s&hellip; A lot! And all you need to chat safely with your data. As you&rsquo;ve seen in our earlier examples, you don&rsquo;t need a lot to get started (<code>data_source</code> and <code>table_name</code> are enough, and in R you can even omit the <code>table_name</code>). But knowing the possibilities makes it easier to customise <code>querychat</code> to your liking.</p>
<h1 id="beyond-chat-bespoke-interfaces">Beyond chat: bespoke interfaces
</h1>
<p>Now you know everything there is to know about the <code>QueryChat</code> object. You know how to add a greeting, additional context, and your favourite LLM. However, it&rsquo;s time to dream bigger and time to get building! Because chatting with your data safely is one thing, but if you truly want to amaze your users you can build an entire dashboard around it. Plots, maps, tables, and value boxes that all update based on the user&rsquo;s questions. Your own bespoke interface. Before we dive into that, let&rsquo;s first take a step back and see if we can reconstruct the &ldquo;quick launch&rdquo; app.</p>
<p>You need two things if you want to build a Shiny app with <code>querychat</code>:</p>
<ul>
<li>The UI component (the chat window)</li>
<li>A server method that deals with the results</li>
</ul>
<p>For the UI component, there are two choices: <code>sidebar()</code> or <code>ui()</code>. The difference? <code>ui</code> creates a basic chat interface, while <code>sidebar</code> wraps the chat interface in a (<code>bslib</code>, for the R lovers) sidebar component designed to be used as the <code>sidebar</code> argument to <code>page_sidebar</code>.</p>
<p>If we want to do something with the results that get returned by <code>querychat</code>, we need to make use of the <code>server()</code> method. The server method returns:</p>
<ul>
<li><code>sql</code>: a reactive that returns the current SQL query. And, if you want to run your own queries, you can also call the <code>$sql()</code> method on the <code>QueryChat</code> object to run queries.</li>
<li><code>title</code>: a reactive that returns the current title.</li>
<li><code>df</code>: a reactive that returns the data frame, filtered and sorted by the current SQL query.</li>
</ul>
<p>Let&rsquo;s take a look at a minimal example that rebuilds the &ldquo;quick launch&rdquo; app:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-15" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-15-1">Python</a></li>
<li><a href="#tabset-15-2">R</a></li>
</ul>
<div id="tabset-15-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span><span class="lnt">55
</span><span class="lnt">56
</span><span class="lnt">57
</span><span class="lnt">58
</span><span class="lnt">59
</span><span class="lnt">60
</span><span class="lnt">61
</span><span class="lnt">62
</span><span class="lnt">63
</span><span class="lnt">64
</span><span class="lnt">65
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny</span> <span class="kn">import</span> <span class="n">App</span><span class="p">,</span> <span class="n">render</span><span class="p">,</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">seaborn</span> <span class="kn">import</span> <span class="n">load_dataset</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">querychat</span> <span class="kn">import</span> <span class="n">QueryChat</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Initialize QueryChat with custom files</span>
</span></span><span class="line"><span class="cl"><span class="n">diamonds</span> <span class="o">=</span> <span class="n">load_dataset</span><span class="p">(</span><span class="s2">&#34;diamonds&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">diamonds_greeting</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;diamonds_greeting.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">diamonds_data_description</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;diamonds_data_description.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">diamonds_extra_instructions</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;diamonds_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">greeting</span><span class="o">=</span><span class="n">diamonds_greeting</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_description</span><span class="o">=</span><span class="n">diamonds_data_description</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">extra_instructions</span><span class="o">=</span><span class="n">diamonds_extra_instructions</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># UI</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">app_ui</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">page_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 2. QueryChat sidebar UI component</span>
</span></span><span class="line"><span class="cl">    <span class="n">qc</span><span class="o">.</span><span class="n">sidebar</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">card</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">card_header</span><span class="p">(</span><span class="s2">&#34;SQL Query&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">output_text_verbatim</span><span class="p">(</span><span class="s2">&#34;sql_output&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">fill</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">card</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">card_header</span><span class="p">(</span><span class="n">ui</span><span class="o">.</span><span class="n">output_text</span><span class="p">(</span><span class="s2">&#34;title&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">output_data_frame</span><span class="p">(</span><span class="s2">&#34;data_table&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">fill</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">fillable</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">theme</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">Theme</span><span class="o">.</span><span class="n">from_brand</span><span class="p">(</span><span class="vm">__file__</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">server</span><span class="p">(</span><span class="nb">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 3. QueryChat server component</span>
</span></span><span class="line"><span class="cl">    <span class="n">vals</span> <span class="o">=</span> <span class="n">qc</span><span class="o">.</span><span class="n">server</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 4. Use the filtered/sorted data frame reactively</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@render.data_frame</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">data_table</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">vals</span><span class="o">.</span><span class="n">df</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@render.text</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">title</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">vals</span><span class="o">.</span><span class="n">title</span><span class="p">()</span> <span class="ow">or</span> <span class="s2">&#34;Diamonds&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 5. Display the generated SQL query</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@render.text</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">sql_output</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">vals</span><span class="o">.</span><span class="n">sql</span><span class="p">()</span> <span class="ow">or</span> <span class="s2">&#34;SELECT * FROM diamonds;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>To keep things simple, we opted for a simple verbatim text output, but we also could&rsquo;ve chosen for this combination, which is from the <a href="https://posit-dev.github.io/shinychat/py/" target="_blank" rel="noopener"><code>shinychat</code></a>
 package:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">ui</span><span class="o">.</span><span class="n">output_ui</span><span class="p">(</span><span class="s2">&#34;sql_output&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@render.ui</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">sql_output</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">sql_value</span> <span class="o">=</span> <span class="n">vals</span><span class="o">.</span><span class="n">sql</span><span class="p">()</span> <span class="ow">or</span> <span class="sa">f</span><span class="s2">&#34;SELECT * FROM </span><span class="si">{</span><span class="n">table_name</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">sql_code</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;```sql</span><span class="se">\n</span><span class="si">{</span><span class="n">sql_value</span><span class="si">}</span><span class="se">\n</span><span class="s2">```&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">output_markdown_stream</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;sql_code&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">content</span><span class="o">=</span><span class="n">sql_code</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">auto_scroll</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">width</span><span class="o">=</span><span class="s2">&#34;100%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This actually happens in the source code for the quick launch app. It would give us the nice &ldquo;copy to clipboard&rdquo; feature and nice formatting. Another alternative would be the native <a href="https://shiny.posit.co/py/api/core/ui.output_markdown_stream.html" target="_blank" rel="noopener">markdown stream component in Shiny</a>
.</p>
<blockquote>
<p><strong>brand.yml</strong></p>
<p>If you want to make use of brand.yml, you need to add a theme argument: <code>theme=ui.Theme.from_brand(**file**)</code>. Make sure you have installed the latest version of shiny with the <code>theme</code> extra! You can simply add it with: <code>uv add &quot;shiny[theme]&quot;</code> (if using <code>uv</code>), or <code>pip install &quot;shiny[theme]&quot;</code></p>
</blockquote>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/diamonds-bespoke-py.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
<div id="tabset-15-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span><span class="lnt">55
</span><span class="lnt">56
</span><span class="lnt">57
</span><span class="lnt">58
</span><span class="lnt">59
</span><span class="lnt">60
</span><span class="lnt">61
</span><span class="lnt">62
</span><span class="lnt">63
</span><span class="lnt">64
</span><span class="lnt">65
</span><span class="lnt">66
</span><span class="lnt">67
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shiny</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">bslib</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">DT</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">querychat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ellmer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ggplot2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Initialize QueryChat with custom files</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">diamonds</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;diamonds&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">greeting</span> <span class="o">=</span> <span class="s">&#34;diamonds_greeting.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">data_description</span> <span class="o">=</span> <span class="s">&#34;diamonds_data_description.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">extra_instructions</span> <span class="o">=</span> <span class="s">&#34;diamonds_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># UI</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="nf">page_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;Diamonds Explorer&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># 2. QueryChat sidebar UI component</span>
</span></span><span class="line"><span class="cl">  <span class="n">sidebar</span> <span class="o">=</span> <span class="n">qc</span><span class="o">$</span><span class="nf">sidebar</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">  <span class="nf">card</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">card_header</span><span class="p">(</span><span class="s">&#34;SQL Query&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="nf">verbatimTextOutput</span><span class="p">(</span><span class="s">&#34;sql_query&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="nf">card</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">card_header</span><span class="p">(</span><span class="nf">textOutput</span><span class="p">(</span><span class="s">&#34;title&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">    <span class="n">DT</span><span class="o">::</span><span class="nf">DTOutput</span><span class="p">(</span><span class="s">&#34;data_table&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># 3. QueryChat server component</span>
</span></span><span class="line"><span class="cl">  <span class="n">vals</span> <span class="o">&lt;-</span> <span class="n">qc</span><span class="o">$</span><span class="nf">server</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 3. Display generated SQL query</span>
</span></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">sql_query</span> <span class="o">&lt;-</span> <span class="nf">renderText</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="kr">if</span> <span class="p">(</span><span class="nf">is.null</span><span class="p">(</span><span class="n">vals</span><span class="o">$</span><span class="nf">sql</span><span class="p">()))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="kr">return</span><span class="p">(</span><span class="s">&#34;SELECT * FROM diamonds;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">vals</span><span class="o">$</span><span class="nf">sql</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 4. Display data table based on user query</span>
</span></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">data_table</span> <span class="o">&lt;-</span> <span class="n">DT</span><span class="o">::</span><span class="nf">renderDT</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="n">vals</span><span class="o">$</span><span class="nf">df</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># 5. Dynamic title based on user query</span>
</span></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">title</span> <span class="o">&lt;-</span> <span class="nf">renderText</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="kr">if</span> <span class="p">(</span><span class="nf">is.null</span><span class="p">(</span><span class="n">vals</span><span class="o">$</span><span class="nf">title</span><span class="p">()))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="kr">return</span><span class="p">(</span><span class="s">&#34;Diamonds Data&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">vals</span><span class="o">$</span><span class="nf">title</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/diamonds-bespoke-r.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
</div>
<p>Looks pretty similar to the quick launch app, right?! So that&rsquo;s how it was build. Note that there a few aesthetic differences though. The quick launch app has a few extra sparks here and there, and our app makes use of custom theming with <code>brand.yml</code>.</p>
<p>So far in our diamonds adventure we have only looked at a simple table, but we can extent this idea much further and build an entire dashboard around it: value boxes, graphs, tables, maps, you name it! This is also what <a href="https://shiny.posit.co/py/templates/sidebot/" target="_blank" rel="noopener">sidebot</a>
 does, and this template is available to get you started quickly. A nice touch is the inclusion of the ✨ icon, which sends a screenshot of the visuals to the LLM for an explanation. How cool is that!</p>
<h1 id="adding-querychat-to-your-existing-shiny-app">Adding querychat to your existing Shiny app
</h1>
<p>The idea of <a href="https://shiny.posit.co/py/templates/sidebot/" target="_blank" rel="noopener">sidebot</a>
 is certainly interesting: why build a dashboard with all kind of filters when you can just add a chat window with access to a smart LLM. You ask it questions, <code>querychat</code> returns some SQL and reactive filtered data, and you make sure you update the entire dashboard. Unlimited filter possibilities. And it doesn&rsquo;t have to be complicated to achieve that.</p>
<p>To demonstrate how easy it is, we are going to use an existing dashboard (SheScores), that currently has a number of filters in it: a slider for the year(s), a dropdown for the continent where the matches took place, the tournaments that took place on those continents, and a switch that filters the data to include only data with known scorers, or not.</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/shescores-original-py.gif" alt="Python version of SheScores" />
<figcaption aria-hidden="true">Python version of SheScores</figcaption>
</figure>
<p>So what does SheScores look like behind the scenes? We&rsquo;re not going into the nitty gritty details of the SheScores dashboard, and we don&rsquo;t have to if we want to add <code>querychat</code> to it. The most important bit of logic is stored in a reactive that contains the filtered data. It reacts to changes in any of the inputs (year, continent, tournament, scorer only or not).</p>
<p>The reactive, <code>filtered_data()</code>, forms the basis for all the elements in the dashboard: the value boxes, the map, the graph, and the table.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-16" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-16-1">Python</a></li>
<li><a href="#tabset-16-2">R</a></li>
</ul>
<div id="tabset-16-1">
<blockquote>
<p><strong>Tip</strong></p>
<p>See <a href="https://github.com/hypebright/shescores-dashboard/blob/68f34785f3217d005497f4719b1f5c64af00ac4d/Python/shescores-app.py" target="_blank" rel="noopener">GitHub</a>
 for the full source code.</p>
</blockquote>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span><span class="lnt">55
</span><span class="lnt">56
</span><span class="lnt">57
</span><span class="lnt">58
</span><span class="lnt">59
</span><span class="lnt">60
</span><span class="lnt">61
</span><span class="lnt">62
</span><span class="lnt">63
</span><span class="lnt">64
</span><span class="lnt">65
</span><span class="lnt">66
</span><span class="lnt">67
</span><span class="lnt">68
</span><span class="lnt">69
</span><span class="lnt">70
</span><span class="lnt">71
</span><span class="lnt">72
</span><span class="lnt">73
</span><span class="lnt">74
</span><span class="lnt">75
</span><span class="lnt">76
</span><span class="lnt">77
</span><span class="lnt">78
</span><span class="lnt">79
</span><span class="lnt">80
</span><span class="lnt">81
</span><span class="lnt">82
</span><span class="lnt">83
</span><span class="lnt">84
</span><span class="lnt">85
</span><span class="lnt">86
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;data/results_with_scorers.csv&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">to_datetime</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">=</span> <span class="n">results_with_scorers</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;tournament&#34;</span><span class="p">]</span> <span class="o">!=</span> <span class="s2">&#34;Friendly&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">&amp;</span> <span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="s2">&#34;2000-01-01&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># UI</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">app_ui</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">page_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">h4</span><span class="p">(</span><span class="s2">&#34;Filters&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">input_slider</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;year_filter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Select year range:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nb">min</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dt</span><span class="o">.</span><span class="n">year</span><span class="o">.</span><span class="n">min</span><span class="p">()),</span>
</span></span><span class="line"><span class="cl">            <span class="nb">max</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dt</span><span class="o">.</span><span class="n">year</span><span class="o">.</span><span class="n">max</span><span class="p">()),</span>
</span></span><span class="line"><span class="cl">            <span class="n">value</span><span class="o">=</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">                <span class="nb">int</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dt</span><span class="o">.</span><span class="n">year</span><span class="o">.</span><span class="n">min</span><span class="p">()),</span>
</span></span><span class="line"><span class="cl">                <span class="nb">int</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dt</span><span class="o">.</span><span class="n">year</span><span class="o">.</span><span class="n">max</span><span class="p">()),</span>
</span></span><span class="line"><span class="cl">            <span class="p">],</span>
</span></span><span class="line"><span class="cl">            <span class="n">sep</span><span class="o">=</span><span class="s2">&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">input_selectize</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;continent_filter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Select continents:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">choices</span><span class="o">=</span><span class="nb">sorted</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;continent&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dropna</span><span class="p">()</span><span class="o">.</span><span class="n">unique</span><span class="p">()</span><span class="o">.</span><span class="n">tolist</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">selected</span><span class="o">=</span><span class="s2">&#34;Europe&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">multiple</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">input_select</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;tournament_filter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Select tournaments:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">choices</span><span class="o">=</span><span class="p">[],</span>
</span></span><span class="line"><span class="cl">            <span class="n">selected</span><span class="o">=</span><span class="p">[],</span>
</span></span><span class="line"><span class="cl">            <span class="n">multiple</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">input_switch</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;scorer_only&#34;</span><span class="p">,</span> <span class="s2">&#34;Show matches with scorer data only&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="kc">False</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">width</span><span class="o">=</span><span class="s2">&#34;30%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Other UI content</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span><span class="o">=</span><span class="s2">&#34;She Scores ⚽️: Women&#39;s International Soccer Matches&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">fillable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">theme</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">Theme</span><span class="o">.</span><span class="n">from_brand</span><span class="p">(</span><span class="vm">__file__</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">server</span><span class="p">(</span><span class="nb">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Reactive filtered data based on inputs</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@reactive.calc</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">filtered_data</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">req</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">continent_filter</span><span class="p">())</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">req</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">tournament_filter</span><span class="p">())</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">data</span> <span class="o">=</span> <span class="n">results_with_scorers</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">data</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">            <span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dt</span><span class="o">.</span><span class="n">year</span> <span class="o">&gt;=</span> <span class="nb">input</span><span class="o">.</span><span class="n">year_filter</span><span class="p">()[</span><span class="mi">0</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">            <span class="o">&amp;</span> <span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">dt</span><span class="o">.</span><span class="n">year</span> <span class="o">&lt;=</span> <span class="nb">input</span><span class="o">.</span><span class="n">year_filter</span><span class="p">()[</span><span class="mi">1</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">            <span class="o">&amp;</span> <span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;continent&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">isin</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">continent_filter</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">            <span class="o">&amp;</span> <span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;tournament&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">isin</span><span class="p">(</span><span class="nb">input</span><span class="o">.</span><span class="n">tournament_filter</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">        <span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">input</span><span class="o">.</span><span class="n">scorer_only</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">            <span class="n">data</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;scorer&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">notna</span><span class="p">()]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">data</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># Other server logic</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-16-2">
<blockquote>
<p><strong>Tip</strong></p>
<p>Check out the full source code on <a href="https://github.com/hypebright/shescores-dashboard/blob/9c8b20d64adfb67566272c587e158dbf2a5052d8/R/shescores-app.R" target="_blank" rel="noopener">GitHub</a>
.</p>
</blockquote>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span><span class="lnt">55
</span><span class="lnt">56
</span><span class="lnt">57
</span><span class="lnt">58
</span><span class="lnt">59
</span><span class="lnt">60
</span><span class="lnt">61
</span><span class="lnt">62
</span><span class="lnt">63
</span><span class="lnt">64
</span><span class="lnt">65
</span><span class="lnt">66
</span><span class="lnt">67
</span><span class="lnt">68
</span><span class="lnt">69
</span><span class="lnt">70
</span><span class="lnt">71
</span><span class="lnt">72
</span><span class="lnt">73
</span><span class="lnt">74
</span><span class="lnt">75
</span><span class="lnt">76
</span><span class="lnt">77
</span><span class="lnt">78
</span><span class="lnt">79
</span><span class="lnt">80
</span><span class="lnt">81
</span><span class="lnt">82
</span><span class="lnt">83
</span><span class="lnt">84
</span><span class="lnt">85
</span><span class="lnt">86
</span><span class="lnt">87
</span><span class="lnt">88
</span><span class="lnt">89
</span><span class="lnt">90
</span><span class="lnt">91
</span><span class="lnt">92
</span><span class="lnt">93
</span><span class="lnt">94
</span><span class="lnt">95
</span><span class="lnt">96
</span><span class="lnt">97
</span><span class="lnt">98
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">&lt;-</span> <span class="nf">read.csv</span><span class="p">(</span><span class="s">&#34;../data/results_with_scorers.csv&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">filter</span><span class="p">(</span><span class="n">tournament</span> <span class="o">!=</span> <span class="s">&#34;Friendly&#34;</span><span class="p">,</span> <span class="n">date</span> <span class="o">&gt;=</span> <span class="s">&#34;2000-01-01&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Other setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># UI</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="nf">page_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">fillable</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;She Scores ⚽️: Women&#39;s International Soccer Matches&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">sidebar</span> <span class="o">=</span> <span class="nf">sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;Filters&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">width</span> <span class="o">=</span> <span class="s">&#34;30%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Year filter</span>
</span></span><span class="line"><span class="cl">    <span class="nf">sliderInput</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">inputId</span> <span class="o">=</span> <span class="s">&#34;year_filter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">label</span> <span class="o">=</span> <span class="s">&#34;Select year range:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">min</span> <span class="o">=</span> <span class="nf">year</span><span class="p">(</span><span class="nf">min</span><span class="p">(</span><span class="nf">as.Date</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="o">$</span><span class="n">date</span><span class="p">))),</span>
</span></span><span class="line"><span class="cl">      <span class="n">max</span> <span class="o">=</span> <span class="nf">year</span><span class="p">(</span><span class="nf">max</span><span class="p">(</span><span class="nf">as.Date</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="o">$</span><span class="n">date</span><span class="p">))),</span>
</span></span><span class="line"><span class="cl">      <span class="n">value</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="nf">year</span><span class="p">(</span><span class="nf">min</span><span class="p">(</span><span class="nf">as.Date</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="o">$</span><span class="n">date</span><span class="p">))),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">year</span><span class="p">(</span><span class="nf">max</span><span class="p">(</span><span class="nf">as.Date</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="o">$</span><span class="n">date</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="n">sep</span> <span class="o">=</span> <span class="s">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Continent filter (dropdown)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">pickerInput</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">inputId</span> <span class="o">=</span> <span class="s">&#34;continent_filter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">label</span> <span class="o">=</span> <span class="s">&#34;Select continents:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">choices</span> <span class="o">=</span> <span class="nf">sort</span><span class="p">(</span><span class="nf">unique</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="o">$</span><span class="n">continent</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">      <span class="n">selected</span> <span class="o">=</span> <span class="nf">sort</span><span class="p">(</span><span class="nf">unique</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="o">$</span><span class="n">continent</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">      <span class="n">options</span> <span class="o">=</span> <span class="nf">pickerOptions</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">actionsBox</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">selectedTextFormat</span> <span class="o">=</span> <span class="s">&#34;count &gt; 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">countSelectedText</span> <span class="o">=</span> <span class="s">&#34;{0} continents selected&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="n">multiple</span> <span class="o">=</span> <span class="kc">TRUE</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Tournament filter (dropdown)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">pickerInput</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">inputId</span> <span class="o">=</span> <span class="s">&#34;tournament_filter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">label</span> <span class="o">=</span> <span class="s">&#34;Select tournaments:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">choices</span> <span class="o">=</span> <span class="kc">NULL</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">selected</span> <span class="o">=</span> <span class="kc">NULL</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">options</span> <span class="o">=</span> <span class="nf">pickerOptions</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">actionsBox</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">liveSearch</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">liveSearchPlaceholder</span> <span class="o">=</span> <span class="s">&#34;Search for a tournament&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">selectedTextFormat</span> <span class="o">=</span> <span class="s">&#34;count &gt; 1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">countSelectedText</span> <span class="o">=</span> <span class="s">&#34;{0} tournaments selected&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="n">multiple</span> <span class="o">=</span> <span class="kc">TRUE</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Switch to show data with scorers only</span>
</span></span><span class="line"><span class="cl">    <span class="nf">input_switch</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">id</span> <span class="o">=</span> <span class="s">&#34;scorer_only&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">label</span> <span class="o">=</span> <span class="s">&#34;Show matches with scorer data only&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">value</span> <span class="o">=</span> <span class="kc">FALSE</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Other UI content</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Reactive filtered data based on inputs</span>
</span></span><span class="line"><span class="cl">  <span class="n">filtered_data</span> <span class="o">&lt;-</span> <span class="nf">reactive</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nf">req</span><span class="p">(</span><span class="nf">length</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">continent_filter</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">req</span><span class="p">(</span><span class="nf">length</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">tournament_filter</span><span class="p">)</span> <span class="o">&gt;</span> <span class="m">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">results_with_scorers</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">mutate</span><span class="p">(</span><span class="n">date</span> <span class="o">=</span> <span class="nf">as.Date</span><span class="p">(</span><span class="n">date</span><span class="p">))</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">filter</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="nf">year</span><span class="p">(</span><span class="n">date</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">input</span><span class="o">$</span><span class="n">year_filter[1]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nf">year</span><span class="p">(</span><span class="n">date</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="n">input</span><span class="o">$</span><span class="n">year_filter[2]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">continent</span> <span class="o">%in%</span> <span class="n">input</span><span class="o">$</span><span class="n">continent_filter</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">tournament</span> <span class="o">%in%</span> <span class="n">input</span><span class="o">$</span><span class="n">tournament_filter</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="kr">if</span> <span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">scorer_only</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="o">!</span><span class="nf">is.na</span><span class="p">(</span><span class="n">scorer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span> <span class="kr">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="kc">TRUE</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Other server logic</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>Now we want to get rid of all those filters. We want a chat window instead. What do we need to change in order to use <code>querychat</code>? Spoiler alert: not much.</p>
<p>Of course we need to initialise our <code>QueryChat</code> object. And since we&rsquo;re not talking about diamonds, we need to make sure to provide a proper soccer-themed greeting, a data description, and extra instructions:</p>
<p><code>shescores_greeting.md</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gh"># Welcome to SheScores! ⚽
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">I&#39;m here to help you explore international women&#39;s soccer match data. 
</span></span><span class="line"><span class="cl">You can ask me to filter and sort the dashboard, answer questions about the data, 
</span></span><span class="line"><span class="cl">or provide insights about teams, players, tournaments, and more.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Here are some ideas to get started:
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Explore match data:**</span>
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Show me the highest-scoring matches in World Cup tournaments&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Which teams have played the most matches against each other?&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Filter to matches from the 2025 UEFA Euro&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Which team has the best win rate in the Canada vs United States rivalry?&lt;/span&gt;
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl"><span class="gs">**Analyze player performance:**</span>
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Who are the top scorers in World Cup history?&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Which players have scored the most penalty goals?&lt;/span&gt;
</span></span><span class="line"><span class="cl">  <span class="k">*</span> &lt;span class=&#34;suggestion&#34;&gt;Which matches had the most own goals?&lt;/span&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">What would you like to explore?
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>shescores_data_description.md</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gh"># Dataset description
</span></span></span><span class="line"><span class="cl">This dataset contains international women’s football match results. 
</span></span><span class="line"><span class="cl">It includes match metadata (date, location, teams), outcomes (scores), 
</span></span><span class="line"><span class="cl">plus optional event-level information such as individual scorers. 
</span></span><span class="line"><span class="cl">Not all friendly matches are represented; 
</span></span><span class="line"><span class="cl">major tournaments are mostly complete.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># Column descriptions
</span></span></span><span class="line"><span class="cl"><span class="k">-</span> date (string, YYYY-MM-DD): The calendar date on which the match was played.
</span></span><span class="line"><span class="cl"><span class="k">-</span> home_team (string): Name of the home team.
</span></span><span class="line"><span class="cl"><span class="k">-</span> date (string, YYYY-MM-DD): The calendar date on which the match was played.
</span></span><span class="line"><span class="cl"><span class="k">-</span> home_team (string): Name of the home team.
</span></span><span class="line"><span class="cl"><span class="k">-</span> away_team (string): Name of the away team.
</span></span><span class="line"><span class="cl"><span class="k">-</span> home_score (integer): Goals scored by the home team at full time (extra time included, - penalty shoot-outs excluded).
</span></span><span class="line"><span class="cl"><span class="k">-</span> away_score (integer): Goals scored by the away team at full time (extra time included, - penalty shoot-outs excluded).
</span></span><span class="line"><span class="cl"><span class="k">-</span> tournament (string): Name of the competition or event.
</span></span><span class="line"><span class="cl"><span class="k">-</span> city (string): City or administrative area where the match was played.
</span></span><span class="line"><span class="cl"><span class="k">-</span> country (string): Country where the match was played.
</span></span><span class="line"><span class="cl"><span class="k">-</span> neutral (boolean): Indicates whether the match took place at a neutral venue.
</span></span><span class="line"><span class="cl"><span class="k">-</span> team (string, optional): Team associated with a recorded scoring event.
</span></span><span class="line"><span class="cl"><span class="k">-</span> scorer (string, optional): Player who scored the goal.
</span></span><span class="line"><span class="cl"><span class="k">-</span> minute (integer, optional): Match minute in which the goal occurred.
</span></span><span class="line"><span class="cl"><span class="k">-</span> own_goal (boolean, optional): Indicates whether the goal was an own goal.
</span></span><span class="line"><span class="cl"><span class="k">-</span> penalty (boolean, optional): Indicates whether the goal was scored from a penalty kick.
</span></span><span class="line"><span class="cl"><span class="k">-</span> country_flag_home (string): Emoji or symbol representing the home country.
</span></span><span class="line"><span class="cl"><span class="k">-</span> country_flag_away (string): Emoji or symbol representing the away country.
</span></span><span class="line"><span class="cl"><span class="k">-</span> continent (string): Continent associated with the home country.
</span></span><span class="line"><span class="cl"><span class="k">-</span> country_code (string): Country code associated with the home team (e.g., ISO-like).
</span></span><span class="line"><span class="cl"><span class="k">-</span> latitude (float): Latitude of the match location.
</span></span><span class="line"><span class="cl"><span class="k">-</span> longitude (float): Longitude of the match location.
</span></span><span class="line"><span class="cl"><span class="k">-</span> match_id (string): Unique identifier for the match, typically based on date and team names.
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>shescores_extra_instrucions.md</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="k">-</span> Maintain consistent spelling in British English.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Don&#39;t add any extra columns to the dataset. You may use them internally 
</span></span><span class="line"><span class="cl">  for calculations, but the final output should only include the original 
</span></span><span class="line"><span class="cl">  columns with the original column names.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Soccer terminology should be used throughout the analysis 
</span></span><span class="line"><span class="cl">  (e.g., &#34;goal&#34; instead of &#34;point&#34;).
</span></span></code></pre></td></tr></table>
</div>
</div><p>Now, adding <code>querychat</code> into the mix is as simple as replacing our inputs in the sidebar with the <code>querychat</code> sidebar component (<code>sidebar()</code>), and our reactive with the results of <code>server()</code>.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-17" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-17-1">Python</a></li>
<li><a href="#tabset-17-2">R</a></li>
</ul>
<div id="tabset-17-1">
<blockquote>
<p><strong>Tip</strong></p>
<p>See <a href="https://github.com/hypebright/shescores-dashboard/blob/963d2b72c600ee9f30ce04da170b05a01c1dc31c/Python/shescores-querychat-app.py" target="_blank" rel="noopener">GitHub</a>
 for the full source code</p>
</blockquote>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span><span class="lnt">48
</span><span class="lnt">49
</span><span class="lnt">50
</span><span class="lnt">51
</span><span class="lnt">52
</span><span class="lnt">53
</span><span class="lnt">54
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;data/results_with_scorers.csv&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">to_datetime</span><span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">=</span> <span class="n">results_with_scorers</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;tournament&#34;</span><span class="p">]</span> <span class="o">!=</span> <span class="s2">&#34;Friendly&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">&amp;</span> <span class="p">(</span><span class="n">results_with_scorers</span><span class="p">[</span><span class="s2">&#34;date&#34;</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="s2">&#34;2000-01-01&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Initialize QueryChat with custom files</span>
</span></span><span class="line"><span class="cl"><span class="n">shescores_greeting</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;shescores_greeting.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">shescores_data_description</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;shescores_data_description.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">shescores_extra_instructions</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;shescores_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">results_with_scorers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">greeting</span><span class="o">=</span><span class="n">shescores_greeting</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_description</span><span class="o">=</span><span class="n">shescores_data_description</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">extra_instructions</span><span class="o">=</span><span class="n">shescores_extra_instructions</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Other setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># UI</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">app_ui</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">page_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">qc</span><span class="o">.</span><span class="n">sidebar</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Other UI components</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span><span class="o">=</span><span class="s2">&#34;She Scores ⚽️: Women&#39;s International Soccer Matches&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">fillable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">theme</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">Theme</span><span class="o">.</span><span class="n">from_brand</span><span class="p">(</span><span class="vm">__file__</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">server</span><span class="p">(</span><span class="nb">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Reactive filtered data based on query</span>
</span></span><span class="line"><span class="cl">    <span class="n">filtered_data</span> <span class="o">=</span> <span class="n">qc</span><span class="o">.</span><span class="n">server</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># Other server logic</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-17-2">
<blockquote>
<p><strong>Tip</strong></p>
<p>Check out the full code on <a href="https://github.com/hypebright/shescores-dashboard/blob/9c8b20d64adfb67566272c587e158dbf2a5052d8/R/shescores-querychat-app.R" target="_blank" rel="noopener">GitHub</a>
.</p>
</blockquote>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">&lt;-</span> <span class="nf">read.csv</span><span class="p">(</span><span class="s">&#34;../data/results_with_scorers.csv&#34;</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="nf">filter</span><span class="p">(</span><span class="n">tournament</span> <span class="o">!=</span> <span class="s">&#34;Friendly&#34;</span><span class="p">,</span> <span class="n">date</span> <span class="o">&gt;=</span> <span class="s">&#34;2000-01-01&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 1. Initialize QueryChat with custom files</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">results_with_scorers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">greeting</span> <span class="o">=</span> <span class="s">&#34;shescores_greeting.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">data_description</span> <span class="o">=</span> <span class="s">&#34;shescores_data_description.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">extra_instructions</span> <span class="o">=</span> <span class="s">&#34;shescores_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Other setup</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># UI</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="nf">page_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">fillable</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;She Scores ⚽️: Women&#39;s International Soccer Matches&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">sidebar</span> <span class="o">=</span> <span class="n">qc</span><span class="o">$</span><span class="nf">sidebar</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Other UI components</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Server</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ===============================</span>
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Reactive filtered data based on query</span>
</span></span><span class="line"><span class="cl">  <span class="n">filtered_data</span> <span class="o">&lt;-</span> <span class="n">qc</span><span class="o">$</span><span class="nf">server</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Other server logic</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>It results in a lot less code and logic too. Win-win. Thanks <code>querychat</code> !</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/shescores-querychat-py.gif" alt="Python version of SheScores with querychat" />
<figcaption aria-hidden="true">Python version of SheScores with querychat</figcaption>
</figure>
<blockquote>
<p><strong>Note</strong></p>
<p>While we don&rsquo;t have a reset button in the app, <code>querychat</code> knows very well what to do when you ask it to reset the dashboard. In this case, it will display the unfiltered data, just like we started when we launched the app.</p>
</blockquote>
<h1 id="database-options">Database options
</h1>
<p>So far we&rsquo;ve only worked with simple datasets: the <code>diamonds</code> dataset that ships with a package, and our soccer data loaded from a <code>.csv</code>. But here&rsquo;s how it works under the hood: even in those examples, you weren&rsquo;t really querying a data frame directly. <code>querychat</code> hands everything off to DuckDB, which becomes the engine that executes all generated SQL. And DuckDB does so quickly and efficiently. Your data frame or <code>.csv</code> is effectively registered inside DuckDB, and every answer comes from real SQL running on that engine.</p>
<p>But what if you don&rsquo;t want to work with in-memory tables at all? What if you already have a database you want to query directly? Maybe a DuckDB file, a SQLite database, Postgres, or even BigQuery? That&rsquo;s exactly what the <code>data_source</code> argument is for. Earlier we used it with plain data frames, but it also accepts database connections. In Python, that means any <a href="https://www.sqlalchemy.org" target="_blank" rel="noopener">SQLAlchemy-supported database</a>
; in R, anything that <a href="https://dbi.r-dbi.org" target="_blank" rel="noopener"><code>DBI</code></a>
 can handle. <code>querychat</code> will inspect the schema of whatever you connect, and from that moment on the workflow is identical as before, only now you&rsquo;re interacting with your own database.</p>
<p>Let&rsquo;s take a look at how to set up <code>querychat</code> with another backend (SQLite) using the <code>data_source</code> argument.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-18" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-18-1">Python</a></li>
<li><a href="#tabset-18-2">R</a></li>
</ul>
<div id="tabset-18-1">
<p>For demonstration purposes, we&rsquo;ll create a SQLite database from the SheScores data (<code>results_with_scorers.csv</code>).</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy</span> <span class="kn">import</span> <span class="n">create_engine</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># From results_with_scorers.csv, create a SQLite database named shescores.db</span>
</span></span><span class="line"><span class="cl"><span class="n">df_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;data/results_with_scorers.csv&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">read_csv</span><span class="p">(</span><span class="n">df_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create the SQLite database and store the DataFrame in it</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Save database in top-level /data directory</span>
</span></span><span class="line"><span class="cl"><span class="n">df</span><span class="o">.</span><span class="n">to_sql</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">con</span><span class="o">=</span><span class="n">create_engine</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;sqlite:///&#34;</span> <span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;data/shescores.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">if_exists</span><span class="o">=</span><span class="s2">&#34;replace&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">index</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>We can then use this database in our <code>QueryChat</code> instance like so:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy</span> <span class="kn">import</span> <span class="n">create_engine</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">querychat</span> <span class="kn">import</span> <span class="n">QueryChat</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">dotenv</span> <span class="kn">import</span> <span class="n">load_dotenv</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">load_dotenv</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Custom files for SheScores</span>
</span></span><span class="line"><span class="cl"><span class="n">shescores_greeting</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;shescores_greeting.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">shescores_data_description</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;shescores_data_description.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">shescores_extra_instructions</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;shescores_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Now create a QueryChat instance to interact with the database</span>
</span></span><span class="line"><span class="cl"><span class="n">db_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="s2">&#34;data/shescores.db&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">engine</span> <span class="o">=</span> <span class="n">create_engine</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;sqlite:///</span><span class="si">{</span><span class="n">db_path</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">=</span> <span class="n">QueryChat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">engine</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">client</span><span class="o">=</span><span class="s2">&#34;anthropic/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">greeting</span><span class="o">=</span><span class="n">shescores_greeting</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_description</span><span class="o">=</span><span class="n">shescores_data_description</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">extra_instructions</span><span class="o">=</span><span class="n">shescores_extra_instructions</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">qc</span><span class="o">.</span><span class="n">app</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>You can also create a DuckDB database from a CSV file or a pandas DataFrame, which is definitely nice for larger datasets. For more examples you can check out the package documentation on <a href="https://posit-dev.github.io/querychat/py/data-sources.html" target="_blank" rel="noopener">data sources</a>
.</p>
<p>Even if you have a database that isn&rsquo;t supported by SQLAlchemy or isn&rsquo;t suited for DuckDB, you can still let <code>querychat</code> access it. In that case, you need to implement the <a href="https://posit-dev.github.io/querychat/py/reference/types.DataSource.html" target="_blank" rel="noopener">DataSource</a>
 interface/protocol.</p>
</div>
<div id="tabset-18-2">
<p>For demonstration purposes, we&rsquo;ll create a SQLite database from the SheScores data (<code>results_with_scorers.csv</code>). To create a new SQLite database, you simply supply the filename to <a href="https://dbi.r-dbi.org/reference/dbConnect.html" target="_blank" rel="noopener"><code>dbConnect()</code></a>
. And with <code>dbWriteTable(</code>), you can easily copy an R dataframe into that newly generated SQLite database:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">DBI</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># From results_with_scorers.csv, create a SQLite database named shescores.db</span>
</span></span><span class="line"><span class="cl"><span class="n">results_with_scorers</span> <span class="o">&lt;-</span> <span class="nf">read.csv</span><span class="p">(</span><span class="s">&#34;data/results_with_scorers.csv&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create a connection to a new SQLite database</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Save database in top-level /data directory</span>
</span></span><span class="line"><span class="cl"><span class="n">conn</span> <span class="o">&lt;-</span> <span class="nf">dbConnect</span><span class="p">(</span><span class="n">RSQLite</span><span class="o">::</span><span class="nf">SQLite</span><span class="p">(),</span> <span class="s">&#34;data/shescores.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Write the data frame to a table named results_with_scorers</span>
</span></span><span class="line"><span class="cl"><span class="nf">dbWriteTable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">conn</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">results_with_scorers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">overwrite</span> <span class="o">=</span> <span class="kc">TRUE</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">dbDisconnect</span><span class="p">(</span><span class="n">conn</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>If you have a SQLite database, connecting to it works in the same manner:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">querychat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">DBI</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create a connection to a SQLite database</span>
</span></span><span class="line"><span class="cl"><span class="n">conn</span> <span class="o">&lt;-</span> <span class="nf">dbConnect</span><span class="p">(</span><span class="n">RSQLite</span><span class="o">::</span><span class="nf">SQLite</span><span class="p">(),</span> <span class="s">&#34;data/shescores.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Write the data frame to a table named results_with_scorers</span>
</span></span><span class="line"><span class="cl"><span class="nf">dbWriteTable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">conn</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">results_with_scorers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">overwrite</span> <span class="o">=</span> <span class="kc">TRUE</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Now create a QueryChat instance to interact with the database</span>
</span></span><span class="line"><span class="cl"><span class="n">qc</span> <span class="o">&lt;-</span> <span class="n">QueryChat</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">conn</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;results_with_scorers&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">=</span> <span class="s">&#34;claude/claude-sonnet-4-5&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">greeting</span> <span class="o">=</span> <span class="s">&#34;shescores_greeting.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">data_description</span> <span class="o">=</span> <span class="s">&#34;shescores_data_description.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">extra_instructions</span> <span class="o">=</span> <span class="s">&#34;shescores_extra_instructions.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">qc</span><span class="o">$</span><span class="nf">app</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Looking for more examples? Check out these <a href="https://github.com/posit-dev/querychat/tree/main/pkg-r/inst/examples-shiny/sqlite" target="_blank" rel="noopener">database setup examples for querychat</a>
.</p>
</div>
</div>
<p><code>querychat</code> knows how to deal with databases, and it has some convenient features for it too, especially when things go wrong: it validates whether tables actually exist and handles any issues gracefully (without cryptic error messages).</p>
<p>One thing to keep in mind when you move from in-memory data to real databases, especially inside Shiny apps, is proper connection management. Whenever your app opens a database connection, it also needs to close it. In Python that usually means calling <code>engine.dispose()</code> when the app shuts down. In R you would use <code>dbDisconnect(conn)</code>, or rely on a connection pool. SQLAlchemy already provides pooling on the Python side, but in R you&rsquo;ll want the <code>pool</code> package to handle this in a nice manner.</p>
<h1 id="for-the-curious-how-does-querychat-know-what-to-do">For the curious: how does querychat know what to do?
</h1>
<p>You&rsquo;ve seen what <code>querychat</code> can do, and you know a bit how it works conceptually. But behind all those concepts is of course some real code. So, for the curious amongst us, here&rsquo;s a little peek into the <code>querychat</code> code!</p>
<p>To talk with an LLM you need a good prompt: prompt design is crucial for a good outcome. A prompt contains context and instructions that an LLM will use to come up with its answer. <code>querychat</code> has a set of instructions for the LLM too, the system prompt, which is stored in a Markdown file (<code>prompt.md</code>).</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-19" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-19-1">Python</a></li>
<li><a href="#tabset-19-2">R</a></li>
</ul>
<div id="tabset-19-1">
<p>You can check out the <code>prompt.md</code> file <a href="https://github.com/posit-dev/querychat/blob/fea52e4e2b56a2cc0a042140dbe5ce194aca8ac6/pkg-py/src/querychat/prompts/prompt.md" target="_blank" rel="noopener">here</a>
, or you can simply print it out:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">qc</span><span class="o">.</span><span class="n">system_prompt</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-19-2">
<p>You can check out the <code>prompt.md</code> file <a href="https://github.com/posit-dev/querychat/blob/main/pkg-r/inst/prompts/prompt.md" target="_blank" rel="noopener">here</a>
, or you can simply print it out:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">print</span><span class="p">(</span><span class="n">qc</span><span class="o">$</span><span class="nf">system_prompt</span><span class="p">())</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>So, what&rsquo;s in this prompt? Let&rsquo;s highlight a few bits:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">You have access to a {{db_type}} SQL database with the following schema:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;database_schema&gt;
</span></span><span class="line"><span class="cl">{{schema}}
</span></span><span class="line"><span class="cl">&lt;/database_schema&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">{{<span class="ni">#data_description</span>}}
</span></span><span class="line"><span class="cl">Here is additional information about the data:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&lt;data_description&gt;
</span></span><span class="line"><span class="cl">{{data_description}}
</span></span><span class="line"><span class="cl">&lt;/data_description&gt;
</span></span><span class="line"><span class="cl">{{/data_description}}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">For security reasons, you may only query this specific table.
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">{{<span class="ni">#extra_instructions</span>}}
</span></span><span class="line"><span class="cl"><span class="gu">## Additional Instructions
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">{{extra_instructions}}
</span></span><span class="line"><span class="cl">{{/extra_instructions}}
</span></span></code></pre></td></tr></table>
</div>
</div><p>The prompt is a <a href="https://mustache.github.io" target="_blank" rel="noopener">Mustache</a>
 template. It&rsquo;s a fill-in-the-blanks template: the <code>{name}</code> parts get replaced with real values, and the <code>{#something} ... {{/something}}</code> blocks only appear if that &ldquo;something&rdquo; actually exists. When you call QueryChat with corresponding arguments, everything gets filled in.</p>
<p>We talked about tool calling earlier, and there was a little note that said that there&rsquo;s not just one tool. There are multiple, for different tasks. You can see that back clearly in the prompt, where we instruct the LLM to call a certain tool (e.g. <code>querychat_update_dashboard</code>) when it receives a request:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">You can handle three types of requests:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### 1. Filtering and Sorting Data
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Call <span class="sb">`querychat_update_dashboard`</span> with the query and a descriptive title
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The user may ask to &#34;reset&#34; or &#34;start over&#34;; that means clearing the filter and title. Do this by calling querychat_reset_dashboard().
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### 2. Answering Questions About Data
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Use the <span class="sb">`querychat_query`</span> tool to run SQL queries
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### 3. Providing Suggestions for Next Steps
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...
</span></span></code></pre></td></tr></table>
</div>
</div><p>There are three tools in <code>querychat</code>:</p>
<ul>
<li><code>querychat_query</code>: used whenever the user asks a question that requires data analysis, aggregation, or calculations.</li>
<li><code>querychat_update_dashboard</code>: used whenever the user requests filtering, sorting, or data manipulation on the dashboard with questions like &ldquo;Show me&hellip;&rdquo; or &ldquo;Which records have&hellip;&rdquo;. Basically any request that involves showing a subset of the data or reordering it.</li>
<li><code>querychat_reset_dashboard</code>: if the user asks to reset the dashboard</li>
</ul>
<p>All the tools are written as <code>chatlas</code> or <code>ellmer</code> tools. As a user, you don&rsquo;t have to worry about this though. The LLM makes sure to use the rights tools, which will make sure the SQL gets executed and the data gets filtered accordingly. But hey, this section was for the curious amongst us!</p>
<h1 id="safety-control-and-confidence">Safety, control, and confidence
</h1>
<p>At some point, everyone asks the same question: is this safe? And it&rsquo;s a fair one. Luckily, <code>querychat</code> is designed entirely around control. The LLM never executes anything itself, never touches your data(base) and never sees raw data. Its only job is to propose <em>read-only</em> SQL.</p>
<p>Remember the moment we asked it to drop a table? It refused. Not because it&rsquo;s polite, but because it&rsquo;s instructed to do so. Combine that with an underlying database (the built-in DuckDB temporary database or your own) that only provides read-only access, and your data will always be left untouched. </p>
<p>It&rsquo;s not a black box either: every generated query can be logged, inspected or audited at any time. In Shiny v1.12.0 this becomes even easier thanks to built in OpenTelemetry support via <code>otel</code>. If you&rsquo;re curious about what that looks like in practice, you can read more in this <a href="https://shiny.posit.co/r/articles/improve/opentelemetry/" target="_blank" rel="noopener">article</a>
.</p>
<p>The safety, control, and (hopefully) the confidence you&rsquo;ve gained by now, make it also suitable for enterprise and regulated environments. If you need to use private or managed LLMs, you&rsquo;re covered: Azure, AWS Bedrock and Google Vertex AI all provide versions of popular models that support tool calling and can work with <code>querychat</code>.</p>
<h1 id="other-querychat-apps-in-the-wild">Other querychat apps in the wild
</h1>
<p>It&rsquo;s always nice to see what others have done with <code>querychat</code>. So here are few sources of inspiration:</p>
<ul>
<li>Do you like trail running? This <a href="https://posit.co/blog/race-stats-dashboard-querychat/" target="_blank" rel="noopener">Race Stats dashboard</a>
 is for you!</li>
<li>Is the American football league more your thing? This <a href="https://www.infoworld.com/article/4040535/chat-with-your-data-the-easy-way-in-r-or-python.html" target="_blank" rel="noopener">Shiny for Python app</a>
 shows you a lot of stats.</li>
<li>Joe Cheng and Garrick Aden-Buie hosted a workshop at posit::conf(2025) called &ldquo;Programming with LLMs&rdquo; that also contains some <a href="https://github.com/posit-conf-2025/llm" target="_blank" rel="noopener">examples</a>
.</li>
<li>And one that we mentioned before: <a href="https://www.infoworld.com/article/4040535/chat-with-your-data-the-easy-way-in-r-or-python.html" target="_blank" rel="noopener">sidebot</a>
, a dashboard analysing restaurant tipping, which is a template you can use very easily.</li>
</ul>
<p>Whether you&rsquo;re playing with a small example dataset or building something much bigger, <code>querychat</code> can be the companion in your app that you&rsquo;re users will love. Build a whole dashboard around the chatbot, or add a touch of LLM magic for those extra side questions. With all this knowledge under your belt, you can build it all!</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>While it seems like there is only one tool call, there&rsquo;s not. In <code>querychat</code> there are different tools for, surprise, different tasks. For the curious there&rsquo;s a deep dive into <code>querychat</code>&rsquo;s source code later in this article.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/shiny/querychat-python-r/querychat-python-r-header.png" length="871482" type="image/png" />
    </item>
    <item>
      <title>Shiny Talks from posit::conf(2025)</title>
      <link>https://posit-open-source.netlify.app/blog/shiny/conf-2025-shinytalks/</link>
      <pubDate>Tue, 02 Dec 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/shiny/conf-2025-shinytalks/</guid><description><![CDATA[<p>Videos from posit::conf(2025) are now available. To make it easier to explore how people are building with Shiny across R and Python, we&rsquo;ve put together a curated playlist of the Shiny-focused talks from the conference. These sessions highlight practical apps, new tooling, design patterns, AI workflows, and real-world deployments built with Shiny.</p>
<h2 id="shiny-talks-playlist">Shiny Talks Playlist:
</h2>
<iframe width="560" height="315" src="https://www.youtube.com/embed/Kh8xDym8sTg?si=trOED7T3iArnY7Kj" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen>
</iframe>
<p>Talks included in the playlist, broken up into a few categories for easier browsing, are as follows:</p>
<h2 id="shiny--ai--intelligent-automation">Shiny + AI &amp; Intelligent Automation
</h2>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Speakers</th>
          <th style="text-align: center">Title</th>
          <th style="text-align: left">Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right"><strong>Winston Chang (Shiny Team)</strong></td>
          <td style="text-align: center">Web applications with Shiny and React (and AI)</td>
          <td style="text-align: left"><a href="https://youtu.be/Kh8xDym8sTg" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/Kh8xDym8sTg/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Karan Gathani (Shiny Team)</strong></td>
          <td style="text-align: center">Old Apps, New Tricks: How AI can write Automated Tests for Shiny</td>
          <td style="text-align: left"><a href="https://youtu.be/gxcAadq0Bmk" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/gxcAadq0Bmk/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Regis A. James</strong></td>
          <td style="text-align: center">AskRADS: An AI Recommendation Agent for Maximizing Shiny Development</td>
          <td style="text-align: left"><a href="https://youtu.be/v3CCoq7j9Tk" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/v3CCoq7j9Tk/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h2 id="shiny-architecture-performance--data">Shiny Architecture, Performance &amp; Data
</h2>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Speakers</th>
          <th style="text-align: center">Title</th>
          <th style="text-align: left">Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right"><strong>Alex Chisholm</strong></td>
          <td style="text-align: center">Keeping Data Alive: Persistent Storage Options for Shiny</td>
          <td style="text-align: left"><a href="https://youtu.be/Pe-XTCKUzV8" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/Pe-XTCKUzV8/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Melissa Albino Hegeman</strong></td>
          <td style="text-align: center">Get your ducks in a row&hellip; faster Shiny apps with DuckDB</td>
          <td style="text-align: left"><a href="https://www.youtube.com/watch?v=2sLJSosz1OY" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://i.ytimg.com/vi/2sLJSosz1OY/hqdefault.jpg?sqp=-oaymwEjCNACELwBSFryq4qpAxUIARUAAAAAGAElAADIQj0AgKJDeAE=&amp;rs=AOn4CLBZKR5FG40MaA-0o4SMJg3cKepLdQ"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Barret Schloerke (Shiny Team)</strong></td>
          <td style="text-align: center">Observability at scale: Monitoring Shiny Applications</td>
          <td style="text-align: left"><a href="https://youtu.be/zwxhFKRlQLs" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/zwxhFKRlQLs/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h2 id="shiny-ux-ui-design--accessibility">Shiny UX, UI Design &amp; Accessibility
</h2>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Speakers</th>
          <th style="text-align: center">Title</th>
          <th style="text-align: left">Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right"><strong>Cameron Race; Sarah Wong-Brown</strong></td>
          <td style="text-align: center">shinyGovStyle &ndash; accessible government design in Shiny</td>
          <td style="text-align: left"><a href="https://youtu.be/33doZkPSUqY" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/33doZkPSUqY/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Casey Aguilar-Gervase; Maya Gans</strong></td>
          <td style="text-align: center">Design of Everyday Shiny Apps</td>
          <td style="text-align: left"><a href="https://youtu.be/RQod46DSkiA" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/RQod46DSkiA/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Jeremy Winget, PhD</strong></td>
          <td style="text-align: center">Death by Dropdown? Engineer Insightful Shiny Apps</td>
          <td style="text-align: left"><a href="https://youtu.be/QvMnUZLQ-oo" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/QvMnUZLQ-oo/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Kim Schouten</strong></td>
          <td style="text-align: center">Modular, layout-as-code approach for customizable Shiny dashboards</td>
          <td style="text-align: left"><a href="https://youtu.be/HJs2HVltpcs" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/HJs2HVltpcs/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h2 id="shiny-extensions-packages--developer-tooling">Shiny Extensions, Packages &amp; Developer Tooling
</h2>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Speakers</th>
          <th style="text-align: center">Title</th>
          <th style="text-align: left">Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right"><strong>Eric Nantz</strong></td>
          <td style="text-align: center">shinystate: Launching collaboration and session state</td>
          <td style="text-align: left"><a href="https://youtu.be/69bnkXD3e6w" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/69bnkXD3e6w/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>James Wade</strong></td>
          <td style="text-align: center">shinyEventLogger &mdash; logging events in Shiny apps</td>
          <td style="text-align: left"><a href="https://youtu.be/smnrmTtoiOM" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/smnrmTtoiOM/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h2 id="applied-shiny-case-studies">Applied Shiny Case Studies
</h2>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Speakers</th>
          <th style="text-align: center">Title</th>
          <th style="text-align: left">Link</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right"><strong>Marcus Beck</strong></td>
          <td style="text-align: center">Shiny for ecological data workflows</td>
          <td style="text-align: left"><a href="https://youtu.be/Cnk3770AzuA" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/Cnk3770AzuA/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>Hugo Fitipaldi</strong></td>
          <td style="text-align: center">Building a Real-Time COVID-19 Surveillance System with Shiny</td>
          <td style="text-align: left"><a href="https://youtu.be/3-UsetFXFlk" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/3-UsetFXFlk/maxresdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h2 id="shiny-workshops-from-positconf2025">Shiny Workshops from posit::conf(2025)
</h2>
<p>Full workshop materials are now available, including slides, exercises, and code for all Shiny workshops.</p>
<h3 class="me-2">
LLMs + Shiny Workshop
</h3>
/
<p class="ms-2">
Joe Cheng & Garrick Aden-Buie
</p>
<p><a href="https://posit-conf-2025.github.io/llm/" class="me-3">LLM Course Materials</a><a href="https://github.com/posit-conf-2025/llm"><img src="https://posit-open-source.netlify.app/images/github.svg" style="width:16px;height:auto;display:inline;"></a></p>
<h3 class="me-2">
Shiny for Python Workshop
</h3>
/
<p class="ms-2">
Daniel Chen
</p>
<p><a href="https://posit-conf-2025.github.io/shiny-py/" class="me-3">Shiny for R Materials</a><a href="https://github.com/posit-conf-2025/shiny-py"><img src="https://posit-open-source.netlify.app/images/github.svg" style="width:16px;height:auto;display:inline;"></a></p>
<h3 class="me-2">
Shiny for R Workshop
</h3>
/
<p class="ms-2">
Colin Rundel
</p>
<p><a href="https://posit-conf-2025.github.io/shiny-r/" class="me-3">Shiny for Python Materials</a><a href="https://github.com/posit-conf-2025/shiny-r/"><img src="https://posit-open-source.netlify.app/images/github.svg" style="width:16px;height:auto;display:inline;"></a></p>
<h2 id="closing">Closing
</h2>
<p>We hope you enjoy this roundup of Shiny talks from posit::conf(2025). The Shiny community continues to explore new patterns&mdash;AI-assisted development, real-time analytics, performance tooling, design systems, and more.
We hope to see you next year at posit::conf&mdash;and maybe even see <em>your</em> Shiny work up on stage.</p>
<style>
  td, th {text-align: left !important;}
  /*tr:last-of-type td {border-bottom: 0px !important;}
  table {border-bottom: 1px solid transparent !important;}*/
</style>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/shiny/conf-2025-shinytalks/conf-recordings-banner.png" length="243314" type="image/png" />
    </item>
    <item>
      <title>posit::conf(2025) Quarto talks</title>
      <link>https://posit-open-source.netlify.app/blog/quarto/2025-11-24-conf-talk-videos/</link>
      <pubDate>Mon, 24 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/quarto/2025-11-24-conf-talk-videos/</guid>
      <dc:creator>Andrew Holz</dc:creator><description><![CDATA[<p>The posit::conf(2025) session videos are now live! We&rsquo;ve created a curated playlist highlighting all the talks that showcase Quarto&mdash;how it is evolving, how people are using it, and how they&rsquo;re building on top of it.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/videoseries?si=RnKN-0bM7C0bwXfQ&amp;list=PLitrm9UndxcvQgAigiiOofTEPAWpROiiK" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen>
</iframe>
<p>Talks included in the playlist, broken up into a few categories for easier browsing, are as follows:</p>
<h1 id="quarto-extensions--advanced-features">Quarto Extensions &amp; Advanced Features
</h1>
<table>
  <thead>
      <tr>
          <th>Speakers</th>
          <th>Title</th>
          <th>Thumbnail</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Carlos Scheidegger (Quarto Team)</strong></td>
          <td>What we&rsquo;re doing to make Quarto fast(er)</td>
          <td><a href="https://youtu.be/OBHppBRztO4" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/OBHppBRztO4/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Christophe Dervieux (Quarto Team)</strong></td>
          <td>Beyond the Basics: Expanding Quarto&rsquo;s Capabilities</td>
          <td><a href="https://youtu.be/u9ev3mvC-p0" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/u9ev3mvC-p0/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Garrick Aden-Buie</strong></td>
          <td>Theming Made Easy: Introducing brand.yml</td>
          <td><a href="https://youtu.be/DPaoNM8Ux04" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/DPaoNM8Ux04/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Gordon Woodhull (Quarto Team)</strong></td>
          <td>Brand YML and Dark Mode in Quarto</td>
          <td><a href="https://youtu.be/WNwsgS-klMA" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/WNwsgS-klMA/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>JooYoung Seo</strong></td>
          <td>maidr: Empowering Accessible, Multimodal Data Science with Quarto</td>
          <td><a href="https://youtu.be/QR7mdgM8Hf0" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/QR7mdgM8Hf0/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h1 id="workflow-automation--reporting">Workflow Automation &amp; Reporting
</h1>
<table>
  <thead>
      <tr>
          <th>Speakers</th>
          <th>Title</th>
          <th>Thumbnail</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Becca Krouse</strong></td>
          <td>Instant Impact: Developing {docorator} to Simplify Document Production</td>
          <td><a href="https://youtu.be/SWt-lcnYlNM" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/SWt-lcnYlNM/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>John Paul Helveston</strong></td>
          <td>surveydown: A Markdown-Based Platform for Interactive Surveys</td>
          <td><a href="https://youtu.be/VwoeFKNvN5k" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/VwoeFKNvN5k/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Keaton Wilson</strong></td>
          <td>Using Quarto to Improve Formatting and Automation</td>
          <td><a href="https://youtu.be/vHrI17AeYGs" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/vHrI17AeYGs/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h1 id="teaching--education">Teaching &amp; Education
</h1>
<table>
  <thead>
      <tr>
          <th>Speakers</th>
          <th>Title</th>
          <th>Thumbnail</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Claus Wilke</strong></td>
          <td>Teaching data visualization with R entirely in Quarto</td>
          <td><a href="https://youtu.be/Q7y0YqCuvHc" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/Q7y0YqCuvHc/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Mine Çetinkaya-Rundel (Quarto Team)</strong></td>
          <td>Leveraging LLMs for student feedback in introductory data science courses</td>
          <td><a href="https://youtu.be/5gS7AUGwZPs" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/5gS7AUGwZPs/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Ted Laderas</strong></td>
          <td>Empowering Learners with WebR, Pyodide, and Quarto</td>
          <td><a href="https://youtu.be/EQ9_MP2PYL8" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/EQ9_MP2PYL8/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<h1 id="business-collaboration--publishing">Business, Collaboration &amp; Publishing
</h1>
<table>
  <thead>
      <tr>
          <th>Speakers</th>
          <th>Title</th>
          <th>Thumbnail</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Andrew Heiss</strong> and <strong>Gabe Osterhout</strong></td>
          <td>Election Night Reporting Using R &amp; Quarto</td>
          <td><a href="https://youtu.be/UCloM4GcfVY" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/UCloM4GcfVY/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Bill Pikounis</strong></td>
          <td>Quarto for Business Collaboration and Technical Documentation in Word docx format</td>
          <td><a href="https://youtu.be/4-dQ2Q985A0" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/4-dQ2Q985A0/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
      <tr>
          <td><strong>Timothy Keyes</strong></td>
          <td>Trust, but Verify: Lessons from Deploying LLMs</td>
          <td><a href="https://youtu.be/HYQaZTLb2Co" target="_blank" rel="noopener"><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://img.youtube.com/vi/HYQaZTLb2Co/hqdefault.jpg"
      alt="thumb" 
      loading="lazy"
    >
  </figure></div>
</a>
</td>
      </tr>
  </tbody>
</table>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">Tip</span>
</div>
<div class="callout-body">
<p><strong>Workshop materials now available:</strong><br>
The Quarto team has published full materials from the two workshops at posit::conf 2025: &ldquo;Branded Websites, Presentations, Dashboards, and PDFs with Quarto&rdquo; and &ldquo;Extending Quarto&rdquo;.<br>
You can access the workshop websites, exercise source code, and full slide decks under a CC BY-SA 4.0 license from the <a href="https://quarto.org/docs/blog/posts/2025-10-27-conf-workshops-materials/" target="_blank" rel="noopener">Quarto blog</a>
.</p>
</div>
</div>
<p>We hope you enjoyed this look back at the Quarto sessions from posit::conf(2025). Every year the community brings new ideas, new tools, and new ways of working &mdash; and we&rsquo;d love to see <strong>your</strong> voice added to the mix. We hope to see you next year, and maybe even see <em>you</em> up on stage sharing your own work at posit::conf!</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/quarto/2025-11-24-conf-talk-videos/thumbnail.png" length="221625" type="image/png" />
    </item>
    <item>
      <title>Tool Calling UI in shinychat</title>
      <link>https://posit-open-source.netlify.app/blog/shiny/shinychat-tool-ui/</link>
      <pubDate>Thu, 20 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/shiny/shinychat-tool-ui/</guid>
      <dc:creator>Garrick Aden-Buie</dc:creator>
      <dc:creator>Carson Sievert</dc:creator>
      <dc:creator>Barret Schloerke</dc:creator><description><![CDATA[<link href="index_files/libs/shinychat-0.3.0/chat/chat.css" rel="stylesheet" />
<link href="index_files/libs/shinychat-0.3.0/markdown-stream/markdown-stream.css" rel="stylesheet" />
<script src="https://posit-open-source.netlify.app/blog/shiny/shinychat-tool-ui/index_files/libs/shinychat-0.3.0/chat/chat.js" type="module"></script>
<script src="https://posit-open-source.netlify.app/blog/shiny/shinychat-tool-ui/index_files/libs/shinychat-0.3.0/markdown-stream/markdown-stream.js" type="module"></script>
<style>
.highlight-line {
  font-weight: bold;
}
body:not(.modal-open) div.sourceCode pre code.has-line-highlights> span:not(.highlight-line) {
  opacity: 0.6;
}
body:not(.modal-open) div.sourceCode:hover pre code.has-line-highlights> span:not(.highlight-line) {
  opacity: 0.8;
}
.card-header {
  --bs-card-cap-bg: transparent;
}
.shiny-tool-card {
  margin-bottom: 1rem !important;
}
.code-copy-button> .bi::after {
  display: none;
}
</style>
<p>We&rsquo;re jazzed to announce that <a href="https://posit-dev.github.io/shinychat" target="_blank" rel="noopener">shinychat</a>
 now includes rich UI for tool calls!
shinychat makes it easy to build LLM-powered chat interfaces in Shiny apps, and with tool calling UI, your users can see which tools are being executed and their outcomes.
This feature is available in <a href="https://posit-dev.github.io/shinychat/r" target="_blank" rel="noopener">shinychat for R</a>
 (v0.3.0) and <a href="https://posit-dev.github.io/shinychat/py/" target="_blank" rel="noopener">shinychat for Python</a>
 (v0.2.0 or later).</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-1" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-1-1">R</a></li>
<li><a href="#tabset-1-2">Python</a></li>
</ul>
<div id="tabset-1-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;shinychat&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-1-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install shinychat
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>This release brings tool call displays that work with <a href="https://ellmer.tidyverse.org" target="_blank" rel="noopener">ellmer</a>
 (R) and <a href="https://github.com/posit-dev/chatlas" target="_blank" rel="noopener">chatlas</a>
 (Python).
When the LLM calls a tool, shinychat automatically displays the request and result in a collapsible card interface.</p>
<p>In this post we&rsquo;ll cover the new <a href="#tool-calling-ui">Tool calling UI</a>
 features, how to set them up in your apps, and ways to customize the display.
We&rsquo;ll also highlight some <a href="#bookmarking-support">chat bookmarking support</a>
 and <a href="#other-improvements-in-r-v0.3.0">other improvements in shinychat for R v0.3.0</a>
.
As always, you can find the full list of changes in the <a href="https://posit-dev.github.io/shinychat/r/news/index.html#shinychat-030" target="_blank" rel="noopener">R release notes</a>
 and <a href="https://github.com/posit-dev/shinychat/blob/main/pkg-py/CHANGELOG.md" target="_blank" rel="noopener">Python release notes</a>
.</p>
<h2 id="tool-calling-ui">Tool calling UI
</h2>
<p>Tool calling lets you extend an LLM&rsquo;s capabilities by giving it access to functions you define.
When you provide a tool to the LLM, you&rsquo;re telling it &ldquo;here&rsquo;s a function you can call if you need it.&rdquo;
The key thing to understand is that the tool runs on <em>your machine</em> (or wherever your Shiny app is running) &mdash; the LLM doesn&rsquo;t directly run the tool itself.
Instead, it asks <em>you</em> to run the function and return the result.</p>
<p>Both ellmer and chatlas make it easy to define tools and register them with your chat client<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, and they also handle the back-and-forth of tool calls by receiving requests from the LLM, executing the tool, and sending the results back.
This means you can focus on what you do best: writing code to solve problems.</p>
<p>Any problem you can solve with a function can become a tool for an LLM!
You can give the LLM access to live data, APIs, databases, or any other resources your app can reach.</p>
<div class="callout callout-tip" role="note" aria-label="Tip">
<div class="callout-header">
<span class="callout-title">btw: A complete toolkit for R</span>
</div>
<div class="callout-body">
<p>If you&rsquo;re working in R, <a href="https://posit-dev.github.io/btw" target="_blank" rel="noopener">btw</a>
 is a complete toolkit to help LLMs work better with R.
Whether you&rsquo;re copy-pasting to ChatGPT, chatting with an AI assistant in your IDE, or building LLM-powered apps with shinychat, btw makes it easy to give LLMs the context they need.</p>
<p>And, most importantly, btw provides a full suite of tools for gathering context from R sessions, including tools to: read help pages and vignettes, describe data frames, search for packages on CRAN, read web pages, and more.</p>
<p>Learn more at <a href="https://posit-dev.github.io/btw" target="_blank" rel="noopener">posit-dev.github.io/btw</a>
!</p>
</div>
</div>
<p>When the LLM decides to call a tool, shinychat displays the request and result in the chat interface.
Users can see which tools are being invoked, what arguments are being passed, and what data is being returned.
The tool display is designed to be customizable, so shinychat developers can customize the appearance and display of tool calls to best serve their users.</p>
<h3 id="basic-tool-display">Basic tool display
</h3>
<p>Let&rsquo;s start by creating a simple weather forecasting tool that fetches a weather data (in the United States) for a given latitude and longitude.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-2" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-2-1">R</a></li>
<li><a href="#tabset-2-2">Python</a></li>
</ul>
<div id="tabset-2-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shinychat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ellmer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">weathR</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">get_weather_forecast</span> <span class="o">&lt;-</span> <span class="nf">tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">point_tomorrow</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">,</span> <span class="n">short</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">name</span> <span class="o">=</span> <span class="s">&#34;get_weather_forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Get the weather forecast for a location.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">arguments</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">lat</span> <span class="o">=</span> <span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Latitude&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">lon</span> <span class="o">=</span> <span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Longitude&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Register the tool with your chat client</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="n">ellmer</span><span class="o">::</span><span class="nf">chat</span><span class="p">(</span><span class="s">&#34;openai/gpt-4.1-nano&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">$</span><span class="nf">register_tool</span><span class="p">(</span><span class="n">get_weather_forecast</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-2-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatOpenAI</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">requests</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_weather_forecast</span><span class="p">(</span><span class="n">lat</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">lon</span><span class="p">:</span> <span class="nb">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Get the weather forecast for a location.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">lat_lng</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;latitude=</span><span class="si">{</span><span class="n">lat</span><span class="si">}</span><span class="s2">&amp;longitude=</span><span class="si">{</span><span class="n">lon</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;https://api.open-meteo.com/v1/forecast?</span><span class="si">{</span><span class="n">lat_lng</span><span class="si">}</span><span class="s2">&amp;current=temperature_2m,wind_speed_10m&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()[</span><span class="s2">&#34;current&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Register the tool with your chat client</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">ChatOpenAI</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s2">&#34;gpt-4.1-nano&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">register_tool</span><span class="p">(</span><span class="n">get_weather_forecast</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>With this tool registered, when you ask a weather-related question, the LLM might decide to call the <code>get_weather_forecast()</code> tool to get the latest weather.</p>
<p>In a chat conversation in your R console with ellmer, this might look like the following.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">chat</span><span class="o">$</span><span class="nf">chat</span><span class="p">(</span><span class="s">&#34;Will I need an umbrella for my walk to the T?&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ◯ [tool call] get_weather_forecast(lat = 42.3515, lon = -71.0552)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ● #&gt; [{&#34;time&#34;:&#34;2025-11-20 16:00:00 EST&#34;,&#34;temp&#34;:42,&#34;dewpoint&#34;:0,&#34;humidity&#34;:67,&#34;p_rain&#34;:1,&#34;wi…</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Based on the weather forecast, there is a chance of rain around 4 to 5 PM,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; with mostly cloudy to partly sunny skies. It seems there might be some rain</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; during this time, so carrying an umbrella could be a good idea if you plan</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; to go out around that time. Otherwise, the weather looks relatively clear</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; in the evening.</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Notice that I didn&rsquo;t provide many context clues, but the model correctly guessed that I&rsquo;m walking to the MBTA in Boston, MA and picked <a href="https://www.openstreetmap.org/?mlat=42.35150&amp;mlon=-71.05520#map=16/42.35150/-71.05520&amp;layers=P" target="_blank" rel="noopener">the latitude and longitude for Boston&rsquo;s South Station</a>
.</p>
<p>In shinychat, when the LLM calls the tool, shinychat automatically displays the tool request in a collapsed card:</p>
<pre><code>OpenTelemetry error: there is no package called 'otelsdk'
</code></pre>
<p><shiny-tool-request request-id="tool_call_001" tool-name="get_weather_forecast" arguments="{&quot;lat&quot;:42.3515,&quot;lon&quot;:-71.0552}"></shiny-tool-request></p>
<p>Expanding the card shows the arguments passed to the tool.
When the tool completes, shinychat replaces the request with a card containing the result:</p>
<p><shiny-tool-result request-id="tool_call_002" tool-name="get_weather_forecast" request-call="get_weather_forecast(lat = 42.3515, lon = -71.0552)" status="success" show-request value="[&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 18:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 13,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 19:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 12,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 20:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 10,&#10;    &quot;wind_speed&quot;: 10,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 21:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 10,&#10;    &quot;wind_speed&quot;: 9,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 22:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 0.5556,&#10;    &quot;humidity&quot;: 82,&#10;    &quot;p_rain&quot;: 11,&#10;    &quot;wind_speed&quot;: 8,&#10;    &quot;wind_dir&quot;: &quot;E&quot;,&#10;    &quot;skies&quot;: &quot;Cloudy&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 23:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 0.5556,&#10;    &quot;humidity&quot;: 82,&#10;    &quot;p_rain&quot;: 11,&#10;    &quot;wind_speed&quot;: 7,&#10;    &quot;wind_dir&quot;: &quot;E&quot;,&#10;    &quot;skies&quot;: &quot;Cloudy&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  }&#10;]" value-type="code"></shiny-tool-result></p>
<p>If the tool throws an error, the error is captured and the error message is shown to the LLM.
Typically this happens when the model makes a mistake in calling the tool and often the error message is instructive.</p>
<p>shinychat updates the card to show the error message:</p>
<p><shiny-tool-result request-id="tool_call_001c" tool-name="get_weather_forecast" request-call="get_weather_forecast(lat = 42.3515, lon = -71.0552)" status="error" show-request value="object of type &#39;closure&#39; is not subsettable" value-type="code"></shiny-tool-result></p>
<h3 id="setting-up-streaming">Setting up streaming
</h3>
<p>To enable tool UI in your apps, you need to ensure that tool requests and results are streamed to shinychat:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-3" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-3-1">R</a></li>
<li><a href="#tabset-3-2">Python</a></li>
</ul>
<div id="tabset-3-1">
<p>You don&rsquo;t need to do anything if you&rsquo;re using <code>chat_app()</code> or the chat module via <code>chat_mod_ui()</code> and <code>chat_mod_server()</code>; tool UI is enabled automatically.</p>
<p>If you&rsquo;re using <code>chat_ui()</code> with <code>chat_append()</code>, set <code>stream = &quot;content&quot;</code> when calling <code>$stream_async()</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span> <span class="o">&lt;-</span> <span class="n">ellmer</span><span class="o">::</span><span class="nf">chat</span><span class="p">(</span><span class="s">&#34;openai/gpt-4.1-nano&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">client</span><span class="o">$</span><span class="nf">register_tool</span><span class="p">(</span><span class="n">get_weather_forecast</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">chat_user_input</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">stream</span> <span class="o">&lt;-</span> <span class="n">client</span><span class="o">$</span><span class="nf">stream_async</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">chat_user_input</span><span class="p">,</span> <span class="n">stream</span> <span class="o">=</span> <span class="s">&#34;content&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">chat_append</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">stream</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-3-2">
<p>In Python with Shiny Express, use <code>content=&quot;all&quot;</code> when calling <code>stream_async()</code>:</p>
<p><strong>app.py</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatOpenAI</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.express</span> <span class="kn">import</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shinychat.express</span> <span class="kn">import</span> <span class="n">Chat</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">ChatOpenAI</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s2">&#34;gpt-4.1-nano&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">client</span><span class="o">.</span><span class="n">register_tool</span><span class="p">(</span><span class="n">get_weather_forecast</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">Chat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">ui</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@chat.on_user_submit</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">handle_user_input</span><span class="p">(</span><span class="n">user_input</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="k">await</span> <span class="n">client</span><span class="o">.</span><span class="n">stream_async</span><span class="p">(</span><span class="n">user_input</span><span class="p">,</span> <span class="n">content</span><span class="o">=</span><span class="s2">&#34;all&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">chat</span><span class="o">.</span><span class="n">append_message_stream</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>For Shiny Core mode:</p>
<p><strong>app.py</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatOpenAI</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny</span> <span class="kn">import</span> <span class="n">App</span><span class="p">,</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shinychat</span> <span class="kn">import</span> <span class="n">Chat</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">ChatOpenAI</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s2">&#34;gpt-4.1-nano&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">client</span><span class="o">.</span><span class="n">register_tool</span><span class="p">(</span><span class="n">get_weather_forecast</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app_ui</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">page_fluid</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">Chat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">ui</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">server</span><span class="p">(</span><span class="nb">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat</span> <span class="o">=</span> <span class="n">Chat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@chat.on_user_submit</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">handle_user_input</span><span class="p">(</span><span class="n">user_input</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">response</span> <span class="o">=</span> <span class="k">await</span> <span class="n">client</span><span class="o">.</span><span class="n">stream_async</span><span class="p">(</span><span class="n">user_input</span><span class="p">,</span> <span class="n">content</span><span class="o">=</span><span class="s2">&#34;all&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="n">chat</span><span class="o">.</span><span class="n">append_message_stream</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<h3 id="customizing-tool-title-and-icon">Customizing tool title and icon
</h3>
<p>You can enhance the visual presentation of tool requests and results by adding custom titles and icons to your tools.
This helps users quickly identify which tools are being called.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-4" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-4-1">R</a></li>
<li><a href="#tabset-4-2">Python</a></li>
</ul>
<div id="tabset-4-1">
<p>Use <code>tool_annotations()</code> to add a title and icon:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">get_weather_forecast</span> <span class="o">&lt;-</span> <span class="nf">tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">point_tomorrow</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">,</span> <span class="n">short</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">name</span> <span class="o">=</span> <span class="s">&#34;get_weather_forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Get the weather forecast for a location.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">arguments</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">lat</span> <span class="o">=</span> <span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Latitude&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">lon</span> <span class="o">=</span> <span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Longitude&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">annotations</span> <span class="o">=</span> <span class="nf">tool_annotations</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;Weather Forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">icon</span> <span class="o">=</span> <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;cloud-sun&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-4-2">
<p>With chatlas, you can customize the tool display in two ways:</p>
<ol>
<li>
<p>Use the <code>._display</code> attribute to customize the tool display:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">faicons</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_weather_forecast</span><span class="p">(</span><span class="n">lat</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">lon</span><span class="p">:</span> <span class="nb">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Get the weather forecast for a location.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ... implementation ...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">get_weather_forecast</span><span class="o">.</span><span class="n">_display</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;title&#34;</span><span class="p">:</span> <span class="s2">&#34;Weather Forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;icon&#34;</span><span class="p">:</span> <span class="n">faicons</span><span class="o">.</span><span class="n">icon_svg</span><span class="p">(</span><span class="s2">&#34;cloud-sun&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This approach sets the title and icon for all calls to this tool, so it&rsquo;s ideal for predefined tools or tools that are bundled in a Python module or package.</p>
</li>
<li>
<p>Set the tool annotations at registration time:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">register_tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">get_weather_forecast</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">annotations</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;title&#34;</span><span class="p">:</span> <span class="s2">&#34;Weather Forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;icon&#34;</span><span class="p">:</span> <span class="n">faicons</span><span class="o">.</span><span class="n">icon_svg</span><span class="p">(</span><span class="s2">&#34;cloud-sun&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This approach allows you to customize the display for a specific chat client or application without modifying the tool function itself.</p>
</li>
</ol>
</div>
</div>
<p>Now the tool card shows your custom title and icon:</p>
<p><shiny-tool-result request-id="tool_call_004" tool-name="get_weather_forecast" request-call="get_weather_forecast(lat = 42.3515, lon = -71.0552)" status="success" tool-title="Weather Forecast" icon="&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 16 16&quot; class=&quot;bi bi-cloud-sun &quot; style=&quot;height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot; &gt;&lt;path d=&quot;M7 8a3.5 3.5 0 0 1 3.5 3.555.5.5 0 0 0 .624.492A1.503 1.503 0 0 1 13 13.5a1.5 1.5 0 0 1-1.5 1.5H3a2 2 0 1 1 .1-3.998.5.5 0 0 0 .51-.375A3.502 3.502 0 0 1 7 8zm4.473 3a4.5 4.5 0 0 0-8.72-.99A3 3 0 0 0 3 16h8.5a2.5 2.5 0 0 0 0-5h-.027z&quot;&gt;&lt;/path&gt;&#10;&lt;path d=&quot;M10.5 1.5a.5.5 0 0 0-1 0v1a.5.5 0 0 0 1 0v-1zm3.743 1.964a.5.5 0 1 0-.707-.707l-.708.707a.5.5 0 0 0 .708.708l.707-.708zm-7.779-.707a.5.5 0 0 0-.707.707l.707.708a.5.5 0 1 0 .708-.708l-.708-.707zm1.734 3.374a2 2 0 1 1 3.296 2.198c.199.281.372.582.516.898a3 3 0 1 0-4.84-3.225c.352.011.696.055 1.028.129zm4.484 4.074c.6.215 1.125.59 1.522 1.072a.5.5 0 0 0 .039-.742l-.707-.707a.5.5 0 0 0-.854.377zM14.5 6.5a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1h-1z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;" show-request value="[&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 18:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 13,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 19:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 12,&#10;    &quot;wind_speed&quot;: 12,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 20:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 10,&#10;    &quot;wind_speed&quot;: 10,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 21:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 1.1111,&#10;    &quot;humidity&quot;: 85,&#10;    &quot;p_rain&quot;: 10,&#10;    &quot;wind_speed&quot;: 9,&#10;    &quot;wind_dir&quot;: &quot;NE&quot;,&#10;    &quot;skies&quot;: &quot;Patchy Fog&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 22:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 0.5556,&#10;    &quot;humidity&quot;: 82,&#10;    &quot;p_rain&quot;: 11,&#10;    &quot;wind_speed&quot;: 8,&#10;    &quot;wind_dir&quot;: &quot;E&quot;,&#10;    &quot;skies&quot;: &quot;Cloudy&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  },&#10;  {&#10;    &quot;time&quot;: &quot;2026-04-02 23:00:00 EDT&quot;,&#10;    &quot;temp&quot;: 38,&#10;    &quot;dewpoint&quot;: 0.5556,&#10;    &quot;humidity&quot;: 82,&#10;    &quot;p_rain&quot;: 11,&#10;    &quot;wind_speed&quot;: 7,&#10;    &quot;wind_dir&quot;: &quot;E&quot;,&#10;    &quot;skies&quot;: &quot;Cloudy&quot;,&#10;    &quot;geometry&quot;: {&#10;      &quot;type&quot;: &quot;Point&quot;,&#10;      &quot;coordinates&quot;: [-71.0589, 42.3601]&#10;    }&#10;  }&#10;]" value-type="code"></shiny-tool-result></p>
<h3 id="custom-display-content">Custom display content
</h3>
<p>By default, shinychat shows the raw tool result value as a code block.
But often you&rsquo;ll want to present data to users in a more polished format&mdash;like a formatted table or a summary.</p>
<p>You can customize the display by returning alternative content:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-5" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-5-1">R</a></li>
<li><a href="#tabset-5-2">Python</a></li>
</ul>
<div id="tabset-5-1">
<p>Return a <code>ContentToolResult</code> with <code>extra$display</code> containing alternative content:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">get_weather_forecast</span> <span class="o">&lt;-</span> <span class="nf">tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">,</span> <span class="n">location_name</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">forecast_data</span> <span class="o">&lt;-</span> <span class="nf">point_tomorrow</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">,</span> <span class="n">short</span> <span class="o">=</span> <span class="kc">FALSE</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">forecast_table</span> <span class="o">&lt;-</span> <span class="n">gt</span><span class="o">::</span><span class="nf">as_raw_html</span><span class="p">(</span><span class="n">gt</span><span class="o">::</span><span class="nf">gt</span><span class="p">(</span><span class="n">forecast_data</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">ContentToolResult</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">forecast_data</span><span class="p">,</span>  <span class="c1"># This is what the LLM sees</span>
</span></span><span class="line"><span class="cl">      <span class="n">extra</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">display</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">html</span> <span class="o">=</span> <span class="n">forecast_table</span><span class="p">,</span>  <span class="c1"># This is what users see</span>
</span></span><span class="line"><span class="cl">          <span class="n">title</span> <span class="o">=</span> <span class="nf">paste</span><span class="p">(</span><span class="s">&#34;Weather Forecast for&#34;</span><span class="p">,</span> <span class="n">location_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">name</span> <span class="o">=</span> <span class="s">&#34;get_weather_forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Get the weather forecast for a location.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">arguments</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">lat</span> <span class="o">=</span> <span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Latitude&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">lon</span> <span class="o">=</span> <span class="nf">type_number</span><span class="p">(</span><span class="s">&#34;Longitude&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">location_name</span> <span class="o">=</span> <span class="nf">type_string</span><span class="p">(</span><span class="s">&#34;Name of the location&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">annotations</span> <span class="o">=</span> <span class="nf">tool_annotations</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span> <span class="o">=</span> <span class="s">&#34;Weather Forecast&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">icon</span> <span class="o">=</span> <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;cloud-sun&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-5-2">
<p>Return a <code>ToolResult</code> with display options:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ToolResult</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pandas</span> <span class="k">as</span> <span class="nn">pd</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_weather_forecast</span><span class="p">(</span><span class="n">lat</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">lon</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">location_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Get the weather forecast for a location.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Get forecast data</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">=</span> <span class="n">fetch_weather_data</span><span class="p">(</span><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Create a DataFrame for the LLM</span>
</span></span><span class="line"><span class="cl">    <span class="n">forecast_df</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Create HTML table for users</span>
</span></span><span class="line"><span class="cl">    <span class="n">forecast_table</span> <span class="o">=</span> <span class="n">forecast_df</span><span class="o">.</span><span class="n">to_html</span><span class="p">(</span><span class="n">index</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">ToolResult</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">value</span><span class="o">=</span><span class="n">forecast_df</span><span class="o">.</span><span class="n">to_dict</span><span class="p">(),</span>  <span class="c1"># LLM sees this</span>
</span></span><span class="line"><span class="cl">        <span class="n">display</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;html&#34;</span><span class="p">:</span> <span class="n">forecast_table</span><span class="p">,</span>  <span class="c1"># Users see this</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;title&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;Weather Forecast for </span><span class="si">{</span><span class="n">location_name</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>The <code>display</code> options support three content types (in order of preference):</p>
<ol>
<li><strong><code>html</code></strong>: HTML content from packages like <code>{gt}</code>, <code>{reactable}</code>, or <code>{htmlwidgets}</code> (R), or Pandas/HTML strings (Python)</li>
<li><strong><code>markdown</code></strong>: Markdown text that&rsquo;s automatically rendered</li>
<li><strong><code>text</code></strong>: Plain text without code formatting</li>
</ol>
<p>Here&rsquo;s what a formatted table looks like in the tool result:</p>
<p><shiny-tool-result request-id="tool_call_007" tool-name="get_weather_forecast" request-call="get_weather_forecast(lat = 42.3515, lon = -71.0552, location_name = &quot;South Station in Boston, MA&quot;)" status="success" tool-title="Weather Forecast for South Station in Boston, MA" icon="&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 16 16&quot; class=&quot;bi bi-cloud-sun &quot; style=&quot;height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;&quot; aria-hidden=&quot;true&quot; role=&quot;img&quot; &gt;&lt;path d=&quot;M7 8a3.5 3.5 0 0 1 3.5 3.555.5.5 0 0 0 .624.492A1.503 1.503 0 0 1 13 13.5a1.5 1.5 0 0 1-1.5 1.5H3a2 2 0 1 1 .1-3.998.5.5 0 0 0 .51-.375A3.502 3.502 0 0 1 7 8zm4.473 3a4.5 4.5 0 0 0-8.72-.99A3 3 0 0 0 3 16h8.5a2.5 2.5 0 0 0 0-5h-.027z&quot;&gt;&lt;/path&gt;&#10;&lt;path d=&quot;M10.5 1.5a.5.5 0 0 0-1 0v1a.5.5 0 0 0 1 0v-1zm3.743 1.964a.5.5 0 1 0-.707-.707l-.708.707a.5.5 0 0 0 .708.708l.707-.708zm-7.779-.707a.5.5 0 0 0-.707.707l.707.708a.5.5 0 1 0 .708-.708l-.708-.707zm1.734 3.374a2 2 0 1 1 3.296 2.198c.199.281.372.582.516.898a3 3 0 1 0-4.84-3.225c.352.011.696.055 1.028.129zm4.484 4.074c.6.215 1.125.59 1.522 1.072a.5.5 0 0 0 .039-.742l-.707-.707a.5.5 0 0 0-.854.377zM14.5 6.5a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1h-1z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;" show-request value="&lt;div id=&quot;sxmpupkcrl&quot; style=&quot;padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;&quot;&gt;&#10;  &#10;  &lt;table class=&quot;gt_table&quot; data-quarto-disable-processing=&quot;false&quot; data-quarto-bootstrap=&quot;false&quot; style=&quot;-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-family: system-ui, &#39;Segoe UI&#39;, Roboto, Helvetica, Arial, sans-serif, &#39;Apple Color Emoji&#39;, &#39;Segoe UI Emoji&#39;, &#39;Segoe UI Symbol&#39;, &#39;Noto Color Emoji&#39;; display: table; border-collapse: collapse; line-height: normal; margin-left: auto; margin-right: auto; color: #333333; font-size: 16px; font-weight: normal; font-style: normal; background-color: #FFFFFF; width: auto; border-top-style: solid; border-top-width: 2px; border-top-color: #A8A8A8; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #A8A8A8; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3;&quot; bgcolor=&quot;#FFFFFF&quot;&gt;&#10;  &lt;thead style=&quot;border-style: none;&quot;&gt;&#10;    &lt;tr class=&quot;gt_col_headings&quot; style=&quot;border-style: none; border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3;&quot;&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_left&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;time&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: left;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;left&quot;&gt;time&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_right&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;temp&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;right&quot;&gt;temp&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_right&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;dewpoint&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;right&quot;&gt;dewpoint&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_right&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;humidity&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;right&quot;&gt;humidity&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_right&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;p_rain&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;right&quot;&gt;p_rain&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_right&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;wind_speed&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;right&quot;&gt;wind_speed&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_left&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;wind_dir&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: left;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;left&quot;&gt;wind_dir&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_left&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;skies&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: left;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;left&quot;&gt;skies&lt;/th&gt;&#10;      &lt;th class=&quot;gt_col_heading gt_columns_bottom_border gt_center&quot; rowspan=&quot;1&quot; colspan=&quot;1&quot; scope=&quot;col&quot; id=&quot;geometry&quot; style=&quot;border-style: none; color: #333333; background-color: #FFFFFF; font-size: 100%; font-weight: normal; text-transform: inherit; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: bottom; padding-top: 5px; padding-bottom: 6px; padding-left: 5px; padding-right: 5px; overflow-x: hidden; text-align: center;&quot; bgcolor=&quot;#FFFFFF&quot; valign=&quot;bottom&quot; align=&quot;center&quot;&gt;geometry&lt;/th&gt;&#10;    &lt;/tr&gt;&#10;  &lt;/thead&gt;&#10;  &lt;tbody class=&quot;gt_table_body&quot; style=&quot;border-style: none; border-top-style: solid; border-top-width: 2px; border-top-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3;&quot;&gt;&#10;    &lt;tr style=&quot;border-style: none;&quot;&gt;&lt;td headers=&quot;time&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;2026-04-02 18:00:00 EDT&lt;/td&gt;&#10;&lt;td headers=&quot;temp&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;38&lt;/td&gt;&#10;&lt;td headers=&quot;dewpoint&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;1.666667&lt;/td&gt;&#10;&lt;td headers=&quot;humidity&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;89&lt;/td&gt;&#10;&lt;td headers=&quot;p_rain&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;13&lt;/td&gt;&#10;&lt;td headers=&quot;wind_speed&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;14&lt;/td&gt;&#10;&lt;td headers=&quot;wind_dir&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;NE&lt;/td&gt;&#10;&lt;td headers=&quot;skies&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;Patchy Fog&lt;/td&gt;&#10;&lt;td headers=&quot;geometry&quot; class=&quot;gt_row gt_center&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: center;&quot; valign=&quot;middle&quot; align=&quot;center&quot;&gt;c(-71.0552, 42.3515)&lt;/td&gt;&lt;/tr&gt;&#10;    &lt;tr style=&quot;border-style: none;&quot;&gt;&lt;td headers=&quot;time&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;2026-04-02 19:00:00 EDT&lt;/td&gt;&#10;&lt;td headers=&quot;temp&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;38&lt;/td&gt;&#10;&lt;td headers=&quot;dewpoint&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;1.666667&lt;/td&gt;&#10;&lt;td headers=&quot;humidity&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;89&lt;/td&gt;&#10;&lt;td headers=&quot;p_rain&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;12&lt;/td&gt;&#10;&lt;td headers=&quot;wind_speed&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;13&lt;/td&gt;&#10;&lt;td headers=&quot;wind_dir&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;NE&lt;/td&gt;&#10;&lt;td headers=&quot;skies&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;Patchy Fog&lt;/td&gt;&#10;&lt;td headers=&quot;geometry&quot; class=&quot;gt_row gt_center&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: center;&quot; valign=&quot;middle&quot; align=&quot;center&quot;&gt;c(-71.0552, 42.3515)&lt;/td&gt;&lt;/tr&gt;&#10;    &lt;tr style=&quot;border-style: none;&quot;&gt;&lt;td headers=&quot;time&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;2026-04-02 20:00:00 EDT&lt;/td&gt;&#10;&lt;td headers=&quot;temp&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;38&lt;/td&gt;&#10;&lt;td headers=&quot;dewpoint&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;1.666667&lt;/td&gt;&#10;&lt;td headers=&quot;humidity&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;89&lt;/td&gt;&#10;&lt;td headers=&quot;p_rain&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;10&lt;/td&gt;&#10;&lt;td headers=&quot;wind_speed&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;13&lt;/td&gt;&#10;&lt;td headers=&quot;wind_dir&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;NE&lt;/td&gt;&#10;&lt;td headers=&quot;skies&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;Patchy Fog&lt;/td&gt;&#10;&lt;td headers=&quot;geometry&quot; class=&quot;gt_row gt_center&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: center;&quot; valign=&quot;middle&quot; align=&quot;center&quot;&gt;c(-71.0552, 42.3515)&lt;/td&gt;&lt;/tr&gt;&#10;    &lt;tr style=&quot;border-style: none;&quot;&gt;&lt;td headers=&quot;time&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;2026-04-02 21:00:00 EDT&lt;/td&gt;&#10;&lt;td headers=&quot;temp&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;38&lt;/td&gt;&#10;&lt;td headers=&quot;dewpoint&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;1.666667&lt;/td&gt;&#10;&lt;td headers=&quot;humidity&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;89&lt;/td&gt;&#10;&lt;td headers=&quot;p_rain&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;10&lt;/td&gt;&#10;&lt;td headers=&quot;wind_speed&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;13&lt;/td&gt;&#10;&lt;td headers=&quot;wind_dir&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;E&lt;/td&gt;&#10;&lt;td headers=&quot;skies&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;Patchy Fog&lt;/td&gt;&#10;&lt;td headers=&quot;geometry&quot; class=&quot;gt_row gt_center&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: center;&quot; valign=&quot;middle&quot; align=&quot;center&quot;&gt;c(-71.0552, 42.3515)&lt;/td&gt;&lt;/tr&gt;&#10;    &lt;tr style=&quot;border-style: none;&quot;&gt;&lt;td headers=&quot;time&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;2026-04-02 22:00:00 EDT&lt;/td&gt;&#10;&lt;td headers=&quot;temp&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;38&lt;/td&gt;&#10;&lt;td headers=&quot;dewpoint&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;1.111111&lt;/td&gt;&#10;&lt;td headers=&quot;humidity&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;85&lt;/td&gt;&#10;&lt;td headers=&quot;p_rain&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;11&lt;/td&gt;&#10;&lt;td headers=&quot;wind_speed&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;12&lt;/td&gt;&#10;&lt;td headers=&quot;wind_dir&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;E&lt;/td&gt;&#10;&lt;td headers=&quot;skies&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;Patchy Fog&lt;/td&gt;&#10;&lt;td headers=&quot;geometry&quot; class=&quot;gt_row gt_center&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: center;&quot; valign=&quot;middle&quot; align=&quot;center&quot;&gt;c(-71.0552, 42.3515)&lt;/td&gt;&lt;/tr&gt;&#10;    &lt;tr style=&quot;border-style: none;&quot;&gt;&lt;td headers=&quot;time&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;2026-04-02 23:00:00 EDT&lt;/td&gt;&#10;&lt;td headers=&quot;temp&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;38&lt;/td&gt;&#10;&lt;td headers=&quot;dewpoint&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;1.111111&lt;/td&gt;&#10;&lt;td headers=&quot;humidity&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;85&lt;/td&gt;&#10;&lt;td headers=&quot;p_rain&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;11&lt;/td&gt;&#10;&lt;td headers=&quot;wind_speed&quot; class=&quot;gt_row gt_right&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: right; font-variant-numeric: tabular-nums;&quot; valign=&quot;middle&quot; align=&quot;right&quot;&gt;10&lt;/td&gt;&#10;&lt;td headers=&quot;wind_dir&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;E&lt;/td&gt;&#10;&lt;td headers=&quot;skies&quot; class=&quot;gt_row gt_left&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: left;&quot; valign=&quot;middle&quot; align=&quot;left&quot;&gt;Patchy Fog&lt;/td&gt;&#10;&lt;td headers=&quot;geometry&quot; class=&quot;gt_row gt_center&quot; style=&quot;border-style: none; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: solid; border-top-width: 1px; border-top-color: #D3D3D3; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; vertical-align: middle; overflow-x: hidden; text-align: center;&quot; valign=&quot;middle&quot; align=&quot;center&quot;&gt;c(-71.0552, 42.3515)&lt;/td&gt;&lt;/tr&gt;&#10;  &lt;/tbody&gt;&#10;  &#10;&lt;/table&gt;&#10;&lt;/div&gt;" value-type="html"></shiny-tool-result></p>
<h3 id="additional-display-options">Additional display options
</h3>
<p>You can control how tool results are presented using additional display options:</p>
<ul>
<li><code>show_request = FALSE</code>: Hide the tool call details when they&rsquo;re obvious from the display</li>
<li><code>open = TRUE</code>: Expand the result panel by default (useful for rich content like maps or charts)</li>
<li><code>title</code> and <code>icon</code>: Override the tool&rsquo;s default title and icon for this specific result</li>
</ul>
<p>Another helpful feature is to include an <code>_intent</code> argument in your tool definition.
When present in the tool arguments, shinychat shows the <code>_intent</code> value in the tool card header, helping users understand why the LLM is calling the tool.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-6" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-6-1">R</a></li>
<li><a href="#tabset-6-2">Python</a></li>
</ul>
<div id="tabset-6-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">tool_with_intent</span> <span class="o">&lt;-</span> <span class="nf">tool</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">function</span><span class="p">(</span><span class="n">`_intent`</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">runif</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="n">name</span> <span class="o">=</span> <span class="s">&#34;random_number&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Generate a random number.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">arguments</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">`_intent`</span> <span class="o">=</span> <span class="nf">type_string</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;Explain why you&#39;re generating this number&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-6-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">random_number</span><span class="p">(</span><span class="n">_intent</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Generate a random number.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    Args:
</span></span></span><span class="line"><span class="cl"><span class="s2">        _intent: Explain why you&#39;re generating this number
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">import</span> <span class="nn">random</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">random</span><span class="o">.</span><span class="n">random</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>Notice that the tool function itself doesn&rsquo;t actually use the <code>_intent</code> argument, but its presence allows shinychat to give the user additional context about the tool call.</p>
<h2 id="bookmarking-support">Bookmarking support
</h2>
<p>When a Shiny app reloads, the app returns to its initial state, unless the URL includes <a href="https://shiny.posit.co/r/articles/build/bookmarking-state/" target="_blank" rel="noopener">bookmarked state</a>
.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>
Automatically updating the URL to include a bookmark of the chat state is a great way to help users return to their work if they accidentally refresh the page or unexpectedly lose their connection.</p>
<p>Both shinychat for R and Python provide helper functions that make it easy to restore conversations with bookmarks.
This means users can refresh the page or share a URL and pick up right where they left off.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-7" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-7-1">R</a></li>
<li><a href="#tabset-7-2">Python</a></li>
</ul>
<div id="tabset-7-1">
<p>In R, the <code>chat_restore()</code> function restores the message history from the bookmark when the app starts up <em>and</em> ensures that the chat client state is automatically bookmarked on user input and assistant responses.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shiny</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">shinychat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nf">page_fillable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">chat_ui</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat_client</span> <span class="o">&lt;-</span> <span class="n">ellmer</span><span class="o">::</span><span class="nf">chat_openai</span><span class="p">(</span><span class="n">model</span> <span class="o">=</span> <span class="s">&#34;gpt-4o-mini&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Automatically save chat state on user input and responses</span>
</span></span><span class="line"><span class="cl">  <span class="nf">chat_restore</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">chat_client</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">chat_user_input</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">stream</span> <span class="o">&lt;-</span> <span class="n">chat_client</span><span class="o">$</span><span class="nf">stream_async</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">chat_user_input</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">chat_append</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">stream</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enable URL-based bookmarking</span>
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">,</span> <span class="n">enableBookmarking</span> <span class="o">=</span> <span class="s">&#34;url&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>enableBookmarking = &quot;url&quot;</code> stores the chat state in encoded data in the query string of the app&rsquo;s URL.
Because browsers have native limitations on the size of a URL, you should use <code>enableBookmarking = &quot;server&quot;</code> to store state server-side without URL size limitations for chatbots expected to have large conversation histories.</p>
<p>And if you&rsquo;re using <code>chat_app()</code> for quick prototypes, bookmarking is already enabled automatically.</p>
</div>
<div id="tabset-7-2">
<p>In Python, the <code>.enable_bookmarking()</code> method handles the where, when, and how of bookmarking chat state.</p>
<h3 id="express-mode">Express mode
</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatOllama</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny.express</span> <span class="kn">import</span> <span class="n">ui</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">chat_client</span> <span class="o">=</span> <span class="n">ChatOllama</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s2">&#34;llama3.2&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">Chat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">ui</span><span class="p">(</span><span class="n">messages</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;Welcome!&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">enable_bookmarking</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat_client</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">bookmark_store</span><span class="o">=</span><span class="s2">&#34;url&#34;</span><span class="p">,</span> <span class="c1"># or &#34;server&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">bookmark_on</span><span class="o">=</span><span class="s2">&#34;response&#34;</span><span class="p">,</span> <span class="c1"># or None</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="core-mode">Core mode
</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatOllama</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shiny</span> <span class="kn">import</span> <span class="n">ui</span><span class="p">,</span> <span class="n">App</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app_ui</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">page_fixed</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">chat_ui</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">,</span> <span class="n">messages</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;Welcome!&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">server</span><span class="p">(</span><span class="nb">input</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat_client</span> <span class="o">=</span> <span class="n">ChatOllama</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s2">&#34;llama3.2&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">Chat</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="s2">&#34;chat&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">chat</span><span class="o">.</span><span class="n">enable_bookmarking</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat_client</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">bookmark_on</span><span class="o">=</span><span class="s2">&#34;response&#34;</span><span class="p">,</span> <span class="c1"># or None</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">,</span> <span class="n">bookmark_store</span><span class="o">=</span><span class="s2">&#34;url&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="configuration-options">Configuration options
</h3>
<p>The <code>.enable_bookmarking()</code> method handles three aspects of bookmarking:</p>
<ol>
<li><strong>Where</strong> (<code>bookmark_store</code>)
<ul>
<li><code>&quot;url&quot;</code>: Store the state in the URL.</li>
<li><code>&quot;server&quot;</code>: Store the state on the server. Consider this over <code>&quot;url&quot;</code> if you want to support a large amount of state, or have other bookmark state that can&rsquo;t be serialized to JSON.</li>
</ul>
</li>
<li><strong>When</strong> (<code>bookmark_on</code>)
<ul>
<li><code>&quot;response&quot;</code>: Triggers a bookmark when an <code>&quot;assistant&quot;</code> response is appended.</li>
<li><code>None</code>: Don&rsquo;t trigger a bookmark automatically. This assumes you&rsquo;ll be triggering bookmarks through other means (e.g., a button).</li>
</ul>
</li>
<li><strong>How</strong> is handled automatically by registering the relevant <code>on_bookmark</code> and <code>on_restore</code> callbacks.</li>
</ol>
<p>When <code>.enable_bookmarking()</code> triggers a bookmark for you, it&rsquo;ll also update the URL query string to include the bookmark state.
This way, when the user unexpectedly loses connection, they can load the current URL to restore the chat state, or go back to the original URL to start over.</p>
</div>
</div>
<h2 id="other-improvements-in-shinychat-for-r">Other improvements in shinychat for R
</h2>
<p>Beyond tool calling UI and bookmarking support, shinychat for R v0.3.0 includes several other enhancements.</p>
<h3 id="better-programmatic-control">Better programmatic control
</h3>
<p><code>chat_mod_server()</code> now returns a set of reactive values and functions for controlling the chat interface:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">server</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_mod_server</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">ellmer</span><span class="o">::</span><span class="nf">chat_openai</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># React to user input</span>
</span></span><span class="line"><span class="cl">  <span class="nf">observe</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nf">req</span><span class="p">(</span><span class="n">chat</span><span class="o">$</span><span class="nf">last_input</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="nf">print</span><span class="p">(</span><span class="nf">paste</span><span class="p">(</span><span class="s">&#34;User said:&#34;</span><span class="p">,</span> <span class="n">chat</span><span class="o">$</span><span class="nf">last_input</span><span class="p">()))</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># React to assistant responses</span>
</span></span><span class="line"><span class="cl">  <span class="nf">observe</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nf">req</span><span class="p">(</span><span class="n">chat</span><span class="o">$</span><span class="nf">last_turn</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="nf">print</span><span class="p">(</span><span class="s">&#34;Assistant completed response&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Programmatically control the chat</span>
</span></span><span class="line"><span class="cl">  <span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">suggest_question</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat</span><span class="o">$</span><span class="nf">update_user_input</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">value</span> <span class="o">=</span> <span class="s">&#34;What&#39;s the weather like today?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">submit</span> <span class="o">=</span> <span class="kc">TRUE</span>  <span class="c1"># Automatically submit</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nf">observeEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">reset</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat</span><span class="o">$</span><span class="nf">clear</span><span class="p">()</span>  <span class="c1"># Clear history and UI</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The returned list includes:</p>
<ul>
<li><strong><code>last_input</code></strong> and <strong><code>last_turn</code></strong> reactives for monitoring chat state</li>
<li><strong><code>update_user_input()</code></strong> for programmatically setting or submitting user input&mdash;great for suggested prompts or guided conversations</li>
<li><strong><code>append()</code></strong> for adding messages to the chat UI</li>
<li><strong><code>clear()</code></strong> for resetting the chat, with options to control how the client history is handled</li>
<li><strong><code>client</code></strong> for direct access to the ellmer chat client</li>
</ul>
<p>There&rsquo;s also a standalone <code>update_chat_user_input()</code> function if you&rsquo;re using <code>chat_ui()</code> directly, which supports updating the placeholder text and moving focus to the input.</p>
<h3 id="custom-assistant-icons">Custom assistant icons
</h3>
<p>You can now customize the icon shown next to assistant messages to better match your application&rsquo;s branding or to distinguish between different assistants:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">bsicons</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Set a custom icon for a specific response</span>
</span></span><span class="line"><span class="cl"><span class="nf">chat_append</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;chat&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Here&#39;s some helpful information!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">icon</span> <span class="o">=</span> <span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;lightbulb&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Or set a default icon for all assistant messages</span>
</span></span><span class="line"><span class="cl"><span class="nf">chat_ui</span><span class="p">(</span><span class="s">&#34;chat&#34;</span><span class="p">,</span> <span class="n">icon_assistant</span> <span class="o">=</span> <span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;robot&#34;</span><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This is especially useful when building multi-agent applications where different assistants might have different personalities or roles.</p>
<h3 id="safer-external-links">Safer external links
</h3>
<p>External links in chat messages now open in a new tab with a confirmation dialog.
This prevents users from accidentally navigating away from the chat session and losing their conversation.
This is particularly helpful when LLMs include links in their responses, for example when shinychat is used in combination with Retrieval Augmented Generation via <a href="https://ragnar.tidyverse.org" target="_blank" rel="noopener">ragnar</a>
.</p>
<h2 id="learn-more">Learn more
</h2>
<p>The tool calling UI opens up exciting possibilities for building transparent, user-friendly AI applications.
Whether you&rsquo;re fetching data, running calculations, or integrating with external services, users can now see exactly what&rsquo;s happening.</p>
<p>To dive deeper:</p>
<ul>
<li>Read the <a href="https://posit-dev.github.io/shinychat/r/articles/tool-ui.html" target="_blank" rel="noopener">tool calling UI article</a>
 for comprehensive examples in R</li>
<li>Explore tool calling with <a href="https://ellmer.tidyverse.org/articles/tool-calling.html" target="_blank" rel="noopener">ellmer</a>
 (R) or <a href="https://posit-dev.github.io/chatlas/tool-calling/displays.html" target="_blank" rel="noopener">chatlas</a>
 (Python)</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>A huge thank you to everyone who contributed to this release with bug reports, feature requests, and code contributions:</p>
<p><a href="https://github.com/bianchenhao" target="_blank" rel="noopener">@bianchenhao</a>
, <a href="https://github.com/cboettig" target="_blank" rel="noopener">@cboettig</a>
, <a href="https://github.com/chendaniely" target="_blank" rel="noopener">@chendaniely</a>
, <a href="https://github.com/cpsievert" target="_blank" rel="noopener">@cpsievert</a>
, <a href="https://github.com/DavZim" target="_blank" rel="noopener">@DavZim</a>
, <a href="https://github.com/DeepanshKhurana" target="_blank" rel="noopener">@DeepanshKhurana</a>
, <a href="https://github.com/DivadNojnarg" target="_blank" rel="noopener">@DivadNojnarg</a>
, <a href="https://github.com/gadenbuie" target="_blank" rel="noopener">@gadenbuie</a>
, <a href="https://github.com/iainwallacebms" target="_blank" rel="noopener">@iainwallacebms</a>
, <a href="https://github.com/janlimbeck" target="_blank" rel="noopener">@janlimbeck</a>
, <a href="https://github.com/jcheng5" target="_blank" rel="noopener">@jcheng5</a>
, <a href="https://github.com/jimrothstein" target="_blank" rel="noopener">@jimrothstein</a>
, <a href="https://github.com/karangattu" target="_blank" rel="noopener">@karangattu</a>
, <a href="https://github.com/ManuelSpinola" target="_blank" rel="noopener">@ManuelSpinola</a>
, <a href="https://github.com/MohoWu" target="_blank" rel="noopener">@MohoWu</a>
, <a href="https://github.com/nissinbo" target="_blank" rel="noopener">@nissinbo</a>
, <a href="https://github.com/noamanemobidata" target="_blank" rel="noopener">@noamanemobidata</a>
, <a href="https://github.com/parmsam" target="_blank" rel="noopener">@parmsam</a>
, <a href="https://github.com/PaulC91" target="_blank" rel="noopener">@PaulC91</a>
, <a href="https://github.com/rkennedy01" target="_blank" rel="noopener">@rkennedy01</a>
, <a href="https://github.com/schloerke" target="_blank" rel="noopener">@schloerke</a>
, <a href="https://github.com/selesnow" target="_blank" rel="noopener">@selesnow</a>
, <a href="https://github.com/simonpcouch" target="_blank" rel="noopener">@simonpcouch</a>
, <a href="https://github.com/skaltman" target="_blank" rel="noopener">@skaltman</a>
, <a href="https://github.com/stefanlinner" target="_blank" rel="noopener">@stefanlinner</a>
, <a href="https://github.com/t-kalinowski" target="_blank" rel="noopener">@t-kalinowski</a>
, <a href="https://github.com/thendrix-trlm" target="_blank" rel="noopener">@thendrix-trlm</a>
, <a href="https://github.com/wch" target="_blank" rel="noopener">@wch</a>
, <a href="https://github.com/wlandau" target="_blank" rel="noopener">@wlandau</a>
, and <a href="https://github.com/Yousuf28" target="_blank" rel="noopener">@Yousuf28</a>
.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>See the <a href="https://ellmer.tidyverse.org/articles/tool-calling.html" target="_blank" rel="noopener">ellmer tool calling documentation</a>
 for R and the <a href="https://posit-dev.github.io/chatlas/tool-calling/how-it-works.html" target="_blank" rel="noopener">chatlas tool calling documentation</a>
 for Python for more details on defining and registering tools.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>This can be especially frustrating behavior since hosted apps, by default, will close an idle session after a certain (<a href="https://docs.posit.co/shinyapps.io/guide/applications/#advanced-settings" target="_blank" rel="noopener">configurable</a>
) amount of time.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/shiny/shinychat-tool-ui/feature.png" length="147726" type="image/png" />
    </item>
    <item>
      <title>Python Open-Source Developer</title>
      <link>https://posit-open-source.netlify.app/blog/tidyverse/2025/python-open-source-developer/</link>
      <pubDate>Wed, 12 Nov 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/tidyverse/2025/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://posit-open-source.netlify.app/blog/tidyverse/2025/python-open-source-developer/thumbnail-sq.jpg" length="30429" type="image/jpeg" />
    </item>
    <item>
      <title>posit::conf(2025) Quarto workshop materials</title>
      <link>https://posit-open-source.netlify.app/blog/quarto/2025-10-27-conf-workshops-materials/</link>
      <pubDate>Mon, 27 Oct 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/quarto/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://posit-open-source.netlify.app/blog/quarto/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://posit-open-source.netlify.app/blog/quarto/2025-10-20-quarto-wizard-1-0-0/</link>
      <pubDate>Mon, 20 Oct 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/quarto/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://posit-open-source.netlify.app/blog/quarto/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>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr>
<td style="text-align: left;"><div width="50.0%" data-layout-align="left">
<ul>
<li><p>Via VS Code or Positron Extensions view:</p>
<ul>
<li>Search for “Quarto Wizard”.</li>
<li>Click “Install”.</li>
</ul>
<p>
<img src="https://posit-open-source.netlify.app/blog/quarto/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></td>
<td style="text-align: left;"><div width="50.0%" data-layout-align="left">
<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-with-filename">
<strong>Terminal</strong>
<div class="sourceCode" id="cb1" data-filename="Terminal"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">code</span> <span class="at">--install-extension</span> mcanouil.quarto-wizard</span></code></pre></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 ‘code’ command in PATH</em> from Visual Studio Code’s Command Palette (<code>Cmd-Shift-P</code> (mac), <code>Ctrl-Shift-P</code> (windows), <code>Ctrl-Shift-P</code> (linux)) if you haven’t done so already.</p>
</div>
</div>
</div>
<div id="tabset-1-2">
<div class="code-with-filename">
<strong>Terminal</strong>
<div class="sourceCode" id="cb2" data-filename="Terminal"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">positron</span> <span class="at">--install-extension</span> mcanouil.quarto-wizard</span></code></pre></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 ‘positron’ command in PATH</em> from Positron’s Command Palette (<code>Cmd-Shift-P</code> (mac), <code>Ctrl-Shift-P</code> (windows), <code>Ctrl-Shift-P</code> (linux)) if you haven’t done so already.</p>
</div>
</div>
</div>
</div></li>
</ul>
</div></td>
</tr>
</tbody>
</table>
<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://posit-open-source.netlify.app/blog/quarto/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>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr>
<td style="text-align: left;"><div width="50.0%" data-layout-align="left">
<ol type="1">
<li>Open the Command Palette (<code>Cmd-Shift-P</code> (mac), <code>Ctrl-Shift-P</code> (windows), <code>Ctrl-Shift-P</code> (linux)).</li>
<li>Type <code>Quarto Wizard: Install Extensions</code> and select it.</li>
<li>Browse the list of available Quarto extensions.</li>
<li>Select the Quarto extension(s) you want to install.</li>
<li>Answer the prompts to confirm the installation.</li>
</ol>
</div></td>
<td style="text-align: center;"><div width="50.0%" data-layout-align="center">
<p><img src="https://posit-open-source.netlify.app/blog/quarto/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></td>
</tr>
</tbody>
</table>
<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://posit-open-source.netlify.app/blog/quarto/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>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr>
<td style="text-align: left;"><div width="50.0%" data-layout-align="left">
<ol type="1">
<li>Open the Command Palette (<code>Cmd-Shift-P</code> (mac), <code>Ctrl-Shift-P</code> (windows), <code>Ctrl-Shift-P</code> (linux)).</li>
<li>Type <code>Quarto Wizard: Use Template</code> and select it.</li>
<li>Browse the list of available Quarto templates.</li>
<li>Select the Quarto template(s) you want to use.</li>
<li>Answer the prompts to confirm the selection.</li>
</ol>
</div></td>
<td style="text-align: center;"><div width="50.0%" data-layout-align="center">
<p><img src="https://posit-open-source.netlify.app/blog/quarto/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></td>
</tr>
</tbody>
</table>
<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://posit-open-source.netlify.app/blog/quarto/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://posit-open-source.netlify.app/blog/quarto/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://posit-open-source.netlify.app/blog/quarto/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://posit-open-source.netlify.app/blog/quarto/2025-10-20-quarto-wizard-1-0-0/featured.png" length="2512689" type="image/png" />
    </item>
    <item>
      <title>Quarto 1.8</title>
      <link>https://posit-open-source.netlify.app/blog/quarto/2025-10-13-1.8-release/</link>
      <pubDate>Mon, 13 Oct 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/quarto/2025-10-13-1.8-release/</guid>
      <dc:creator>Charlotte Wickham</dc:creator><description><![CDATA[<p>Quarto 1.8 is available! 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>Quarto 1.8 improves support for light and dark brand colors and logos, brand extensions for sharing brands across Quarto projects, HTML accessibility checks powered by Axe-core, and access to more information about execution context from your code cells.
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.8/" target="_blank" rel="noopener">Release Notes</a>
.</p>
<h2 id="dark-and-light-colors-and-logos-in-brand">Dark and light colors and logos in brand
</h2>
<p>You can now specify <code>light</code> and <code>dark</code> versions of any colors or logo in a brand specification:</p>
<p><strong>_brand.yml</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></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">foreground</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">light</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;#333333&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">dark</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;#EEEEEE&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">background</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">light</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;#EEEEEE&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">dark</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;#333333&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">logos</span><span class="p">:</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></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">light</span><span class="p">:</span><span class="w"> </span><span class="l">logo.png</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">dark</span><span class="p">:</span><span class="w"> </span><span class="l">logo-white.png         </span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>This works in <code>_brand.yml</code> files as well as <code>brand</code> specified directly in document metadata.
You can also present in dark mode by specifying <code>brand-mode: dark</code> in your <code>format: revealjs</code> presentations.</p>
<p>Read more in the updated <a href="https://quarto.org/docs/authoring/brand.html" target="_blank" rel="noopener">Guide &gt; Brand</a>
:</p>
<ul>
<li><a href="https://quarto.org/docs/authoring/brand.html#light-and-dark-colors" target="_blank" rel="noopener">Light and dark colors</a>
</li>
<li><a href="https://quarto.org/docs/authoring/brand.html#light-and-dark-logos" target="_blank" rel="noopener">Light and dark logos</a>
</li>
<li><a href="https://quarto.org/docs/authoring/brand.html#brand-mode" target="_blank" rel="noopener">Brand mode</a>
</li>
</ul>
<h2 id="brand-extensions">Brand extensions
</h2>
<p>Share brand definitions and assets across Quarto projects with a brand extension.</p>
<p>Get started with:</p>
<p><strong>Terminal</strong></p>
<pre tabindex="0"><code class="language-default" data-lang="default">quarto create extension brand
</code></pre><p>Read more in <a href="https://quarto.org/docs/extensions/brand.html" target="_blank" rel="noopener">Extensions &gt; Brand</a>
, and keep an eye out for other ways to reuse and share your brand in future releases.</p>
<h2 id="accessibility-checks-for-html">Accessibility checks for HTML
</h2>
<p>You can add accessibility checks using the <a href="https://github.com/dequelabs/axe-core" target="_blank" rel="noopener">Axe-core engine</a>
 to HTML documents (<code>format</code>: <code>html</code>, <code>revealjs</code> and <code>dashboard</code>) with the new <code>axe</code> option.</p>
<p>For example, you can get a summary of violations right in your document preview:</p>
<figure>
<img src="https://quarto.org/docs/output-formats/images/axe-violation.png" class="border" data-fig-alt="A webpage with a box in the bottom left that warns &#39;Serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds&#39;." alt="A rendered webpage with an accessibility violation warning" />
<figcaption aria-hidden="true">A rendered webpage with an accessibility violation warning</figcaption>
</figure>
<p>Read about your options in <a href="https://quarto.org/docs/output-formats/html-accessibility.html" target="_blank" rel="noopener">HTML Accessibility Checks</a>
</p>
<p>We know accessability is a big concern for many of our users, and more improvements will be coming in future releases.</p>
<h2 id="accessing-execution-information">Accessing execution information
</h2>
<p>Quarto sets the <code>QUARTO_EXECUTE_INFO</code> environment variable, which allows you to access information about execution context from code cells.</p>
<p>Read the JSON file located at <code>QUARTO_EXECUTE_INFO</code> and access properties such as <code>document-path</code>, <code>format</code>, <code>metadata</code> and more:</p>
<div class="panel-tabset">
<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>
<li><a href="#tabset-1-3">Julia</a></li>
</ul>
<div id="tabset-1-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</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">jsonlite</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">execute_info</span> <span class="o">&lt;-</span> <span class="nf">read_json</span><span class="p">(</span><span class="nf">Sys.getenv</span><span class="p">(</span><span class="s">&#34;QUARTO_EXECUTE_INFO&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="n">execute_info</span><span class="o">$</span><span class="n">`document-path`</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-1-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><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">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;QUARTO_EXECUTE_INFO&#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">execute_info</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">load</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">execute_info</span><span class="p">[</span><span class="s2">&#34;document-path&#34;</span><span class="p">]</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-1-3">
<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-julia" data-lang="julia"><span class="line"><span class="cl"><span class="k">using</span> <span class="n">JSON</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">execute_info</span> <span class="o">=</span> <span class="n">JSON</span><span class="o">.</span><span class="n">parsefile</span><span class="p">(</span><span class="nb">ENV</span><span class="p">[</span><span class="s">&#34;QUARTO_EXECUTE_INFO&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="n">execute_info</span><span class="p">[</span><span class="s">&#34;document-path&#34;</span><span class="p">]</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>Read more in <a href="https://quarto.org/docs/advanced/quarto-execute-info.html" target="_blank" rel="noopener">Access execution settings from code cells</a>
.</p>
<h2 id="other-highlights">Other Highlights
</h2>
<ul>
<li>
<p>Access <a href="https://quarto.org/docs/extensions/lua-api.html#metadata-access" target="_blank" rel="noopener">metadata</a>
 and <a href="https://quarto.org/docs/extensions/lua-api.html#variables-access" target="_blank" rel="noopener">variables</a>
 in filters and shortcodes: Use the new <code>quarto.variables.get()</code> and <code>quarto.metadata.get()</code> APIs.</p>
</li>
<li>
<p>The default LaTeX engine is now <code>lualatex</code>.</p>
</li>
</ul>
<p>Dependency updates:</p>
<ul>
<li><code>mermaidjs</code> updated to 11.6.0.</li>
<li>Bootstrap icons updated to v1.13.1</li>
<li><code>QuartoNotebookRunner</code> in <code>julia</code> engine updated to 0.17.3</li>
</ul>
<h2 id="acknowledgements">Acknowledgements
</h2>
<p>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/Aariq" target="_blank" rel="noopener">Aariq</a>
,
<a href="https://github.com/AndreasThinks" target="_blank" rel="noopener">AndreasThinks</a>
,
<a href="https://github.com/ArthurData" target="_blank" rel="noopener">ArthurData</a>
,
<a href="https://github.com/Blake-Madden" target="_blank" rel="noopener">Blake-Madden</a>
,
<a href="https://github.com/ColinFay" target="_blank" rel="noopener">ColinFay</a>
,
<a href="https://github.com/DCEW" target="_blank" rel="noopener">DCEW</a>
,
<a href="https://github.com/DanStuder" target="_blank" rel="noopener">DanStuder</a>
,
<a href="https://github.com/Data-Wise" target="_blank" rel="noopener">Data-Wise</a>
,
<a href="https://github.com/EllaKaye" target="_blank" rel="noopener">EllaKaye</a>
,
<a href="https://github.com/EmilHvitfeldt" target="_blank" rel="noopener">EmilHvitfeldt</a>
,
<a href="https://github.com/FrankwaP" target="_blank" rel="noopener">FrankwaP</a>
,
<a href="https://github.com/GabrielCoffee9" target="_blank" rel="noopener">GabrielCoffee9</a>
,
<a href="https://github.com/GeorgRamer" target="_blank" rel="noopener">GeorgRamer</a>
,
<a href="https://github.com/Gewerd-Strauss" target="_blank" rel="noopener">Gewerd-Strauss</a>
,
<a href="https://github.com/GuillaumeDehaene" target="_blank" rel="noopener">GuillaumeDehaene</a>
,
<a href="https://github.com/HarunCelikOtto" target="_blank" rel="noopener">HarunCelikOtto</a>
,
<a href="https://github.com/HayesJohnD" target="_blank" rel="noopener">HayesJohnD</a>
,
<a href="https://github.com/Joao-O-Santos" target="_blank" rel="noopener">Joao-O-Santos</a>
,
<a href="https://github.com/MateusMolina" target="_blank" rel="noopener">MateusMolina</a>
,
<a href="https://github.com/MichaelHatherly" target="_blank" rel="noopener">MichaelHatherly</a>
,
<a href="https://github.com/PeteArm" target="_blank" rel="noopener">PeteArm</a>
,
<a href="https://github.com/Selbosh" target="_blank" rel="noopener">Selbosh</a>
,
<a href="https://github.com/SergeCroise" target="_blank" rel="noopener">SergeCroise</a>
,
<a href="https://github.com/SrShelo" target="_blank" rel="noopener">SrShelo</a>
,
<a href="https://github.com/VisruthSK" target="_blank" rel="noopener">VisruthSK</a>
,
<a href="https://github.com/Vistales" target="_blank" rel="noopener">Vistales</a>
,
<a href="https://github.com/abhiaagarwal" target="_blank" rel="noopener">abhiaagarwal</a>
,
<a href="https://github.com/aborruso" target="_blank" rel="noopener">aborruso</a>
,
<a href="https://github.com/adamblake" target="_blank" rel="noopener">adamblake</a>
,
<a href="https://github.com/adamiturabi" target="_blank" rel="noopener">adamiturabi</a>
,
<a href="https://github.com/alastairrushworth" target="_blank" rel="noopener">alastairrushworth</a>
,
<a href="https://github.com/albertomercurio" target="_blank" rel="noopener">albertomercurio</a>
,
<a href="https://github.com/alecloudenback" target="_blank" rel="noopener">alecloudenback</a>
,
<a href="https://github.com/alex-r-bigelow" target="_blank" rel="noopener">alex-r-bigelow</a>
,
<a href="https://github.com/allefeld" target="_blank" rel="noopener">allefeld</a>
,
<a href="https://github.com/alyst" target="_blank" rel="noopener">alyst</a>
,
<a href="https://github.com/andrewheiss" target="_blank" rel="noopener">andrewheiss</a>
,
<a href="https://github.com/andrewpbray" target="_blank" rel="noopener">andrewpbray</a>
,
<a href="https://github.com/austin-hoover" target="_blank" rel="noopener">austin-hoover</a>
,
<a href="https://github.com/batpigandme" target="_blank" rel="noopener">batpigandme</a>
,
<a href="https://github.com/bauerj" target="_blank" rel="noopener">bauerj</a>
,
<a href="https://github.com/benkeks" target="_blank" rel="noopener">benkeks</a>
,
<a href="https://github.com/benz0li" target="_blank" rel="noopener">benz0li</a>
,
<a href="https://github.com/bkowshik" target="_blank" rel="noopener">bkowshik</a>
,
<a href="https://github.com/blackerby" target="_blank" rel="noopener">blackerby</a>
,
<a href="https://github.com/boshek" target="_blank" rel="noopener">boshek</a>
,
<a href="https://github.com/brandonmontez" target="_blank" rel="noopener">brandonmontez</a>
,
<a href="https://github.com/bryce-carson" target="_blank" rel="noopener">bryce-carson</a>
,
<a href="https://github.com/carschandler" target="_blank" rel="noopener">carschandler</a>
,
<a href="https://github.com/christopherkenny" target="_blank" rel="noopener">christopherkenny</a>
,
<a href="https://github.com/cl-roberts" target="_blank" rel="noopener">cl-roberts</a>
,
<a href="https://github.com/cmadland" target="_blank" rel="noopener">cmadland</a>
,
<a href="https://github.com/co1emi11er2" target="_blank" rel="noopener">co1emi11er2</a>
,
<a href="https://github.com/coatless" target="_blank" rel="noopener">coatless</a>
,
<a href="https://github.com/cpcloud" target="_blank" rel="noopener">cpcloud</a>
,
<a href="https://github.com/daxkellie" target="_blank" rel="noopener">daxkellie</a>
,
<a href="https://github.com/dixslyf" target="_blank" rel="noopener">dixslyf</a>
,
<a href="https://github.com/dkapitan" target="_blank" rel="noopener">dkapitan</a>
,
<a href="https://github.com/econmaett" target="_blank" rel="noopener">econmaett</a>
,
<a href="https://github.com/edavidaja" target="_blank" rel="noopener">edavidaja</a>
,
<a href="https://github.com/edvinsyk" target="_blank" rel="noopener">edvinsyk</a>
,
<a href="https://github.com/ethanwhite" target="_blank" rel="noopener">ethanwhite</a>
,
<a href="https://github.com/fermarsan" target="_blank" rel="noopener">fermarsan</a>
,
<a href="https://github.com/fredguth" target="_blank" rel="noopener">fredguth</a>
,
<a href="https://github.com/fuhrmanator" target="_blank" rel="noopener">fuhrmanator</a>
,
<a href="https://github.com/gadenbuie" target="_blank" rel="noopener">gadenbuie</a>
,
<a href="https://github.com/georgestagg" target="_blank" rel="noopener">georgestagg</a>
,
<a href="https://github.com/ghisvail" target="_blank" rel="noopener">ghisvail</a>
,
<a href="https://github.com/ghost" target="_blank" rel="noopener">ghost</a>
,
<a href="https://github.com/apps/github-actions" target="_blank" rel="noopener">github-actions[bot]</a>
,
<a href="https://github.com/glin" target="_blank" rel="noopener">glin</a>
,
<a href="https://github.com/gregswinehart" target="_blank" rel="noopener">gregswinehart</a>
,
<a href="https://github.com/gwbrck" target="_blank" rel="noopener">gwbrck</a>
,
<a href="https://github.com/halleysfifthinc" target="_blank" rel="noopener">halleysfifthinc</a>
,
<a href="https://github.com/hansfn" target="_blank" rel="noopener">hansfn</a>
,
<a href="https://github.com/hchulkim" target="_blank" rel="noopener">hchulkim</a>
,
<a href="https://github.com/holtzy" target="_blank" rel="noopener">holtzy</a>
,
<a href="https://github.com/htbunn" target="_blank" rel="noopener">htbunn</a>
,
<a href="https://github.com/hturner" target="_blank" rel="noopener">hturner</a>
,
<a href="https://github.com/hugetim" target="_blank" rel="noopener">hugetim</a>
,
<a href="https://github.com/hutch3232" target="_blank" rel="noopener">hutch3232</a>
,
<a href="https://github.com/iagopinal" target="_blank" rel="noopener">iagopinal</a>
,
<a href="https://github.com/ihrke" target="_blank" rel="noopener">ihrke</a>
,
<a href="https://github.com/jameslairdsmith" target="_blank" rel="noopener">jameslairdsmith</a>
,
<a href="https://github.com/jdfoote" target="_blank" rel="noopener">jdfoote</a>
,
<a href="https://github.com/jeremy9959" target="_blank" rel="noopener">jeremy9959</a>
,
<a href="https://github.com/jfy133" target="_blank" rel="noopener">jfy133</a>
,
<a href="https://github.com/jkrumbiegel" target="_blank" rel="noopener">jkrumbiegel</a>
,
<a href="https://github.com/jmgirard" target="_blank" rel="noopener">jmgirard</a>
,
<a href="https://github.com/jonpeake" target="_blank" rel="noopener">jonpeake</a>
,
<a href="https://github.com/jvcarli" target="_blank" rel="noopener">jvcarli</a>
,
<a href="https://github.com/jxpeng98" target="_blank" rel="noopener">jxpeng98</a>
,
<a href="https://github.com/kandolfp" target="_blank" rel="noopener">kandolfp</a>
,
<a href="https://github.com/kapsner" target="_blank" rel="noopener">kapsner</a>
,
<a href="https://github.com/kathsherratt" target="_blank" rel="noopener">kathsherratt</a>
,
<a href="https://github.com/kazuyanagimoto" target="_blank" rel="noopener">kazuyanagimoto</a>
,
<a href="https://github.com/kevinah95" target="_blank" rel="noopener">kevinah95</a>
,
<a href="https://github.com/kippandrew" target="_blank" rel="noopener">kippandrew</a>
,
<a href="https://github.com/koldle" target="_blank" rel="noopener">koldle</a>
,
<a href="https://github.com/lachlansimpson" target="_blank" rel="noopener">lachlansimpson</a>
,
<a href="https://github.com/lbm364dl" target="_blank" rel="noopener">lbm364dl</a>
,
<a href="https://github.com/leovuong" target="_blank" rel="noopener">leovuong</a>
,
<a href="https://github.com/lostmygithubaccount" target="_blank" rel="noopener">lostmygithubaccount</a>
,
<a href="https://github.com/lu-kas" target="_blank" rel="noopener">lu-kas</a>
,
<a href="https://github.com/lukmanaj" target="_blank" rel="noopener">lukmanaj</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/mahmudstat" target="_blank" rel="noopener">mahmudstat</a>
,
<a href="https://github.com/masud90" target="_blank" rel="noopener">masud90</a>
,
<a href="https://github.com/melaniewalsh" target="_blank" rel="noopener">melaniewalsh</a>
,
<a href="https://github.com/mfisher87" target="_blank" rel="noopener">mfisher87</a>
,
<a href="https://github.com/mipmip" target="_blank" rel="noopener">mipmip</a>
,
<a href="https://github.com/mpr1255" target="_blank" rel="noopener">mpr1255</a>
,
<a href="https://github.com/multimeric" target="_blank" rel="noopener">multimeric</a>
,
<a href="https://github.com/musvaage" target="_blank" rel="noopener">musvaage</a>
,
<a href="https://github.com/mvuorre" target="_blank" rel="noopener">mvuorre</a>
,
<a href="https://github.com/nathanj3" target="_blank" rel="noopener">nathanj3</a>
,
<a href="https://github.com/nessan" target="_blank" rel="noopener">nessan</a>
,
<a href="https://github.com/nichtich" target="_blank" rel="noopener">nichtich</a>
,
<a href="https://github.com/odysseu" target="_blank" rel="noopener">odysseu</a>
,
<a href="https://github.com/ofkoru" target="_blank" rel="noopener">ofkoru</a>
,
<a href="https://github.com/olivroy" target="_blank" rel="noopener">olivroy</a>
,
<a href="https://github.com/oyvindbso" target="_blank" rel="noopener">oyvindbso</a>
,
<a href="https://github.com/pagiraud" target="_blank" rel="noopener">pagiraud</a>
,
<a href="https://github.com/parmsam" target="_blank" rel="noopener">parmsam</a>
,
<a href="https://github.com/peter-gy" target="_blank" rel="noopener">peter-gy</a>
,
<a href="https://github.com/pm-gusmano" target="_blank" rel="noopener">pm-gusmano</a>
,
<a href="https://github.com/produnis" target="_blank" rel="noopener">produnis</a>
,
<a href="https://github.com/rabyj" target="_blank" rel="noopener">rabyj</a>
,
<a href="https://github.com/raffaem" target="_blank" rel="noopener">raffaem</a>
,
<a href="https://github.com/randyzwitch" target="_blank" rel="noopener">randyzwitch</a>
,
<a href="https://github.com/rben01" target="_blank" rel="noopener">rben01</a>
,
<a href="https://github.com/rossbowen" target="_blank" rel="noopener">rossbowen</a>
,
<a href="https://github.com/rundel" target="_blank" rel="noopener">rundel</a>
,
<a href="https://github.com/ryanzomorrodi" target="_blank" rel="noopener">ryanzomorrodi</a>
,
<a href="https://github.com/ryjohnson09" target="_blank" rel="noopener">ryjohnson09</a>
,
<a href="https://github.com/s2t2" target="_blank" rel="noopener">s2t2</a>
,
<a href="https://github.com/salim-b" target="_blank" rel="noopener">salim-b</a>
,
<a href="https://github.com/samcarter" target="_blank" rel="noopener">samcarter</a>
,
<a href="https://github.com/serialc" target="_blank" rel="noopener">serialc</a>
,
<a href="https://github.com/sgelzenleuchter" target="_blank" rel="noopener">sgelzenleuchter</a>
,
<a href="https://github.com/skriptum" target="_blank" rel="noopener">skriptum</a>
,
<a href="https://github.com/spaette" target="_blank" rel="noopener">spaette</a>
,
<a href="https://github.com/stragu" target="_blank" rel="noopener">stragu</a>
,
<a href="https://github.com/sun123zxy" target="_blank" rel="noopener">sun123zxy</a>
,
<a href="https://github.com/sverrirarnors" target="_blank" rel="noopener">sverrirarnors</a>
,
<a href="https://github.com/tecosaur" target="_blank" rel="noopener">tecosaur</a>
,
<a href="https://github.com/temospena" target="_blank" rel="noopener">temospena</a>
,
<a href="https://github.com/thatchermo" target="_blank" rel="noopener">thatchermo</a>
,
<a href="https://github.com/topepo" target="_blank" rel="noopener">topepo</a>
,
<a href="https://github.com/tylere" target="_blank" rel="noopener">tylere</a>
,
<a href="https://github.com/winniehell" target="_blank" rel="noopener">winniehell</a>
,
<a href="https://github.com/wklimowicz" target="_blank" rel="noopener">wklimowicz</a>
,
<a href="https://github.com/yogabonito" target="_blank" rel="noopener">yogabonito</a>
,
<a href="https://github.com/youcc" target="_blank" rel="noopener">youcc</a>
,
<a href="https://github.com/yves-amevoin" target="_blank" rel="noopener">yves-amevoin</a>
,
<a href="https://github.com/yyzeng" target="_blank" rel="noopener">yyzeng</a>
.</p>
<p>The lightbulb 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://posit-open-source.netlify.app/blog/quarto/2025-10-13-1.8-release/thumbnail.png" length="58117" type="image/png" />
    </item>
    <item>
      <title>Build Your First LLM App with Shiny for Python or R</title>
      <link>https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/</link>
      <pubDate>Mon, 15 Sep 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/</guid>
      <dc:creator>Veerle Eeftink - Van Leemput</dc:creator><description><![CDATA[<p>You&rsquo;ve made it to third part of &ldquo;The Shiny Side of LLMs&rdquo; series, where we turn everything we&rsquo;ve learned into something real and interactive! Our weapon of choice: Shiny, of course.</p>
<p>In the first part, <a href="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/../../../blog/shiny/shiny-side-of-llms-part-1/">What LLMs Actually Do (and What They Don&rsquo;t)</a>
, we explored what LLMs actually do. We covered how they generate text, what they&rsquo;re good (and bad) at, and we covered most of the jargon that gets thrown around. Then in part two, <a href="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/../../../blog/shiny/shiny-side-of-llms-part-2/">Talking to LLMs: From Prompt to Response</a>
, we got practical. You learned how to structure prompts, send them to a model via an API using <a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener"><code>chatlas</code></a>
 or <a href="https://ellmer.tidyverse.org" target="_blank" rel="noopener"><code>ellmer</code></a>
, and handle the responses in your code. Now it&rsquo;s time to wrap that logic in an interface your users can love and can actually interact with!</p>
<p>In this post, we&rsquo;ll cover how to:</p>
<ul>
<li>Structure a basic Shiny app (in both Python and R) with a user interface (UI) and reactive server logic</li>
<li>Connect user input to an LLM call and show the result in the UI</li>
<li>Set up a chat interface</li>
<li>Keep your app responsive while doing lengthy tasks or calculations (yes, a quick guide to async programming!)</li>
<li>Add UI polish like pretty looking value boxes, loading indicators, and error messages</li>
<li>Bonus: deploy your app with a single click to Posit Connect (Cloud) or shinyapps.io</li>
</ul>
<p>By the end of this part, you&rsquo;ll have your first real, working, LLM-powered app. And most importantly: the knowledge to build many more.</p>
<blockquote>
<p><strong>What are we going to do in this last part?</strong></p>
<p>This part of &ldquo;The Shiny Side of LLMs&rdquo; series will build an app called &ldquo;DeckCheck&rdquo;: a genius app that gets rid of lengthy unfocused presentations, like a perfect &ldquo;Presentation Rehearsal Buddy&rdquo;. The goal: let users upload their <a href="https://quarto.org" target="_blank" rel="noopener">Quarto</a>
 presentation and provide them with feedback on how to make it better.</p>
</blockquote>
<blockquote>
<p><strong>No time for the walkthrough?</strong></p>
<p>Want to dive straight into the full app? Head over to <a href="#the-end-result">the end result</a>
.</p>
</blockquote>
<h1 id="why-shiny">Why Shiny?
</h1>
<p>We knew pretty early on that &ldquo;DeckCheck&rdquo; was going to be an app. Why? Because an app is just easier. People can click buttons, type things, and get results without ever touching code (which is great, because &ldquo;copy-paste this into your terminal&rdquo; tends to scare off half the audience).</p>
<p>But ok, why Shiny? You&rsquo;re reading this on the Shiny blog, so yes, it seems pretty obvious we love it. Still, we have solid reasons:</p>
<ul>
<li>You can go from idea to something you can actually click in almost no time. Shiny is great for prototyping. You can quickly experiment, gather feedback, and gradually build an app out to a production-ready state.</li>
<li>Shiny is reactive by design, which is a fancy way of saying: it keeps track of what depends on what, and only updates what actually needs to change. You don&rsquo;t need to worry about the logic, which means you can spend more time building your app instead of dealing with state management.</li>
<li>This reactive engine also makes Shiny efficient: outputs only re-render when the stuff they depend on changes.</li>
<li>If your LLM workflow is in Python, there&rsquo;s Shiny for Python. If it&rsquo;s in R, there&rsquo;s Shiny for R. No language wars or conversations about &ldquo;X is better than Y&rdquo;. Just pick whatever you&rsquo;re comfortable with.</li>
<li>You can even drop in your own HTML, CSS, and JavaScript to really make it your own.</li>
</ul>
<p>Alright, enough with the sales pitch. Shiny it is!</p>
<blockquote>
<p><strong>How Shiny Assistant assists you</strong></p>
<p>Fun fact: this article contains side-by-side examples in both Python and R. To showcase how <a href="https://gallery.shinyapps.io/assistant/" target="_blank" rel="noopener">Shiny Assistant</a>
 can support you in either language, it was used to generate some of the conversions. That&rsquo;s a neat way to highlight how an LLM can help you get started with Shiny! The true &ldquo;Shiny Side of LLMs&rdquo;. Of course result were not always 100% spot on, but luckily there was still a human in the loop.</p>
</blockquote>
<h1 id="optimising-conversations-for-shiny">Optimising conversations for Shiny
</h1>
<blockquote>
<p><strong>Getting your API key</strong></p>
<p>Remember you need to <a href="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/../../../blog/shiny/shiny-side-of-llms-part-2/#what-do-you-need">grab an API key for your chosen LLM provider</a>
. You need this key to authenticate. Store this key as an environment variable. For example, to use Claude from Anthropic, <code>ANTHROPIC_API_KEY=yourkey</code> needs to be in <code>.Renviron</code> or <code>.env</code> file.</p>
</blockquote>
<p>Going from a script-like workflow to an app requires a different way of thinking. We simply have other expectations from a web app compared to just a regular Python or R script. We want things to be interactive, and ideally we want to have the result instantly. If we click on something, we expect something to happen, fast. Ever encountered a web page that stayed blank for just 5 seconds? How long did that feel? Like 10 minutes? Or didn&rsquo;t you even stick out the 5 seconds? Yes, you are impatient! You need to see something is happening, and get some visual feedback.</p>
<p>LLMs take your impatience into account, and that&rsquo;s why most models stream tokens progressively. Instead of making you wait forever for a big wall of text, they start showing you the answer bit by bit, almost like they&rsquo;re thinking out loud. This streaming feels faster, keeps you engaged, and makes the whole experience way less frustrating. <code>chatlas</code> and <code>ellmer</code> use this streaming capability too and they print the result on the console as soon as words come in. But&hellip; a Shiny app doesn&rsquo;t have a console! So what to do?</p>
<p>Where the <code>chat()</code> method does not return any results until the entire response is received and only prints the streaming results to the console, the <code>stream()</code> method can process the response as it arrives. It&rsquo;s perfect for something like a Shiny chat window. You simply replace the <code>chat()</code> method with the <code>stream()</code> method, which returns something called a &ldquo;generator&rdquo;. A generator is a function that can pause (<code>yield</code>) and resume later, remembering where it left off. That&rsquo;s handy because it lets your code:</p>
<ol>
<li>Process text as it arrives (aka, by chunk) instead of waiting for the whole thing.</li>
<li>Pause between chunks without blocking other things.</li>
<li>Keep its place so it can pick up right where it left off when the next chunk arrives.</li>
</ol>
<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>In the <code>chatlas</code> documentation you can read more about <a href="https://posit-dev.github.io/chatlas/get-started/stream.html" target="_blank" rel="noopener">streams</a>
.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><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></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">ChatAnthropic</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">ChatAnthropic</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;claude-sonnet-4-20250514&#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;You are a presentation coach for data scientists. You give constructive, focused, and practical feedback on titles, structure, and storytelling.&#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"># Set model parameters (optional)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">set_model_params</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">temperature</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span>  <span class="c1"># default is 1</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">stream</span> <span class="o">=</span> <span class="n">chat</span><span class="o">.</span><span class="n">stream</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;I&#39;m working on a presentation with the title: &#39;The Shiny Side of LLMs&#39;.
</span></span></span><span class="line"><span class="cl"><span class="s2">        Please evaluate the clarity, tone, and relevance of this title for the intended audience.
</span></span></span><span class="line"><span class="cl"><span class="s2">        For context, this is a 10-minute lightning talk at posit::conf(2025).
</span></span></span><span class="line"><span class="cl"><span class="s2">        The audience is Python and R users who are curious about AI and large language models,
</span></span></span><span class="line"><span class="cl"><span class="s2">        but not all of them have a deep technical background.
</span></span></span><span class="line"><span class="cl"><span class="s2">        
</span></span></span><span class="line"><span class="cl"><span class="s2">        The talk uses Shiny as a way to explore and demo LLMs in practice.
</span></span></span><span class="line"><span class="cl"><span class="s2">        
</span></span></span><span class="line"><span class="cl"><span class="s2">        Return your answer as a JSON array of objects, where each object has the following keys:
</span></span></span><span class="line"><span class="cl"><span class="s2">        
</span></span></span><span class="line"><span class="cl"><span class="s2">        - &#39;aspect&#39;: one of &#39;clarity&#39;, &#39;tone&#39;, or &#39;relevance&#39;
</span></span></span><span class="line"><span class="cl"><span class="s2">        - &#39;feedback&#39;: your concise assessment
</span></span></span><span class="line"><span class="cl"><span class="s2">        - &#39;suggestion&#39;: an optional improvement if applicable&#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></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">stream</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="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The object that gets returned by the <a href="https://posit-dev.github.io/chatlas/reference/Chat.html#stream" target="_blank" rel="noopener"><code>stream()</code></a>
 method is a <a href="https://wiki.python.org/moin/Generators" target="_blank" rel="noopener">generator</a>
.</p>
</div>
<div id="tabset-1-2">
<p>In the <code>ellmer</code> documentation you can read more about <a href="https://ellmer.tidyverse.org/articles/streaming-async.html" target="_blank" rel="noopener">streaming</a>
.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><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></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></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="s">&#34;You are a presentation coach for data scientists.
</span></span></span><span class="line"><span class="cl"><span class="s">  You give constructive, focused, and practical feedback on titles, structure, and storytelling.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">params</span> <span class="o">=</span> <span class="nf">params</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">temperature</span> <span class="o">=</span> <span class="m">0.8</span> <span class="c1"># default is 1</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">stream</span> <span class="o">&lt;-</span> <span class="n">chat</span><span class="o">$</span><span class="nf">stream</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;I&#39;m working on a presentation with the title: &#39;The Shiny Side of LLMs&#39;.
</span></span></span><span class="line"><span class="cl"><span class="s">Please evaluate the clarity, tone, and relevance of this title for the intended audience.
</span></span></span><span class="line"><span class="cl"><span class="s">For context, this is a 10-minute lightning talk at posit::conf(2025).
</span></span></span><span class="line"><span class="cl"><span class="s">The audience is Python and R users who are curious about AI and large language models,
</span></span></span><span class="line"><span class="cl"><span class="s">but not all of them have a deep technical background.
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">The talk uses Shiny as a way to explore and demo LLMs in practice.
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">Return your answer as a JSON array of objects, where each object has the following keys:
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">- &#39;aspect&#39;: one of &#39;clarity&#39;, &#39;tone&#39;, or &#39;relevance&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">- &#39;feedback&#39;: your concise assessment
</span></span></span><span class="line"><span class="cl"><span class="s">- &#39;suggestion&#39;: an optional improvement if applicable&#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">coro</span><span class="o">::</span><span class="nf">loop</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="kr">for</span> <span class="p">(</span><span class="n">chunk</span> <span class="kr">in</span> <span class="n">stream</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">cat</span><span class="p">(</span><span class="n">chunk</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The object that gets returned by the <a href="https://ellmer.tidyverse.org/reference/Chat.html#method-stream-" target="_blank" rel="noopener"><code>stream()</code></a>
 method is a <a href="https://coro.r-lib.org/articles/generator.html#generators" target="_blank" rel="noopener">coro generator</a>
.</p>
</div>
</div>
<p>Streaming is great for things like chatbots, live transcription, or anything where seeing text appear in real time feels natural. It&rsquo;s perfect for conversations, or when the answer is long and you don&rsquo;t want to keep users staring at a blank screen.</p>
<p>In apps like &ldquo;DeckCheck&rdquo;, where we&rsquo;re analysing a Quarto presentation behind the scenes and then showing a finished result, streaming doesn&rsquo;t really add much. Users expect a clear, polished answer all at once. Not a JSON, but ready-to-go value boxes, graphs and tables. So in this case, a smooth loading indicator or progress bar will probably satisfy impatient users and we can stick to our <code>chat()</code> method. Don&rsquo;t forget the <code>stream()</code> method though: we&rsquo;ll use that a little bit later in a small demo chatbot.</p>
<p>Whether we&rsquo;re streaming or not, users simply have to wait for an LLM response and you can keep them entertained while they&rsquo;re doing that. But another important thing related to a long waiting time is when your app is used by multiple users at the same time. They might ask things from the model at the same time too, meaning that you&rsquo;re dealing with concurrent chat sessions. If you would just use <code>chat()</code> or <code>stream()</code>, the Shiny app (or technically speaking: the Python or R session running the Shiny app) will be blocked for other users for the duration of each response. And that&rsquo;s not cool. The more users are concurrently using your app, the longer the queue gets, and the longer users have to wait. This is <strong>synchronous</strong> behaviour. We rather deal <strong>asynchronously</strong> (aka <strong>async</strong>) with a model&rsquo;s responses. It means that we can receive responses at the same time, in parallel. To use async chat, we need to call <code>chat_async()</code> / <code>stream_async()</code> instead of <code>chat()</code> / <code>stream()</code>. The <code>_async</code> variants take the same arguments but returns a <strong>coroutine</strong> object, (aka a placeholder for something that will come) instead of the actual response.</p>
<p>So ideally, asking a question to an LLM would look something like this in our Shiny application:</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">
<blockquote>
<p><strong>Note for Positron/Jupyter users</strong></p>
<p>Positron and Jupyter already run their own event loop, so <code>asyncio.run(main())</code> will fail with a runtime error. Instead of wrapping in <code>asyncio.run</code>, you can just do: <code>await main()</code> at the top level.</p>
</blockquote>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span></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">asyncio</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatAnthropic</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">ChatAnthropic</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;claude-sonnet-4-20250514&#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;You are a presentation coach for data scientists. You give constructive, focused, and practical feedback on titles, structure, and storytelling.&#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">async</span> <span class="k">def</span> <span class="nf">main</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">chat</span><span class="o">.</span><span class="n">chat_async</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;&#34;&#34;I&#39;m working on a presentation with the title: &#39;The Shiny Side of LLMs&#39;.
</span></span></span><span class="line"><span class="cl"><span class="s2">        Please evaluate the clarity, tone, and relevance of this title for the intended audience.
</span></span></span><span class="line"><span class="cl"><span class="s2">        For context, this is a 10-minute lightning talk at posit::conf(2025).
</span></span></span><span class="line"><span class="cl"><span class="s2">        The audience is Python and R users who are curious about AI and large language models,
</span></span></span><span class="line"><span class="cl"><span class="s2">        but not all of them have a deep technical background.
</span></span></span><span class="line"><span class="cl"><span class="s2">        
</span></span></span><span class="line"><span class="cl"><span class="s2">        The talk uses Shiny as a way to explore and demo LLMs in practice.
</span></span></span><span class="line"><span class="cl"><span class="s2">        
</span></span></span><span class="line"><span class="cl"><span class="s2">        Return your answer as a JSON array of objects, where each object has the following keys:
</span></span></span><span class="line"><span class="cl"><span class="s2">        
</span></span></span><span class="line"><span class="cl"><span class="s2">        - &#39;aspect&#39;: one of &#39;clarity&#39;, &#39;tone&#39;, or &#39;relevance&#39;
</span></span></span><span class="line"><span class="cl"><span class="s2">        - &#39;feedback&#39;: your concise assessment
</span></span></span><span class="line"><span class="cl"><span class="s2">        - &#39;suggestion&#39;: an optional improvement if applicable&#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="nb">print</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">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show output</summary>
<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></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="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2"> [                                                                                                          
</span></span></span><span class="line"><span class="cl"><span class="s2">   {                                                                                                        
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;aspect&#34;: &#34;clarity&#34;,                                                                                   
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;feedback&#34;: &#34;The title is somewhat ambiguous - &#39;shiny side&#39; could refer to positive aspects of LLMs or 
</span></span></span><span class="line"><span class="cl"><span class="s2"> the Shiny framework itself. For an audience not deeply familiar with both topics, this creates confusion   
</span></span></span><span class="line"><span class="cl"><span class="s2"> about the main focus.&#34;,                                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;suggestion&#34;: &#34;Consider titles like &#39;Exploring LLMs with Shiny&#39; or &#39;Building LLM Demos in Shiny&#39; to    
</span></span></span><span class="line"><span class="cl"><span class="s2"> clearly establish that Shiny is your vehicle for demonstrating LLMs.&#34;                                      
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                                                       
</span></span></span><span class="line"><span class="cl"><span class="s2">   {                                                                                                        
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;aspect&#34;: &#34;tone&#34;,                                                                                      
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;feedback&#34;: &#34;The tone is appropriately casual and engaging for a lightning talk, with a clever play on 
</span></span></span><span class="line"><span class="cl"><span class="s2"> words that fits the conference atmosphere. However, it may be too playful for conveying the practical valu 
</span></span></span><span class="line"><span class="cl"><span class="s2"> of the content.&#34;,                                                                                          
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;suggestion&#34;: &#34;Balance the wordplay with clearer value proposition, such as &#39;The Shiny Side of LLMs:   
</span></span></span><span class="line"><span class="cl"><span class="s2"> Interactive AI Exploration&#39; to maintain engagement while signaling practical benefits.&#34;                    
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                                                       
</span></span></span><span class="line"><span class="cl"><span class="s2">   {                                                                                                        
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;aspect&#34;: &#34;relevance&#34;,                                                                                 
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;feedback&#34;: &#34;Highly relevant to the posit::conf audience who knows Shiny well, but the connection      
</span></span></span><span class="line"><span class="cl"><span class="s2"> between Shiny and LLMs isn&#39;t immediately clear from the title alone. Given the 10-minute format, you need  
</span></span></span><span class="line"><span class="cl"><span class="s2"> attendees to quickly understand the value.&#34;,                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;suggestion&#34;: &#34;Make the connection more explicit with alternatives like &#39;Hands-On LLM Exploration with 
</span></span></span><span class="line"><span class="cl"><span class="s2"> Shiny Apps&#39; or &#39;From Prompts to Production: LLMs in Shiny&#39; to immediately communicate the practical        
</span></span></span><span class="line"><span class="cl"><span class="s2"> application.&#34;                                                                                              
</span></span></span><span class="line"><span class="cl"><span class="s2">   }                                                                                                        
</span></span></span><span class="line"><span class="cl"><span class="s2"> ]                                                                                                          
</span></span></span><span class="line"><span class="cl"><span class="s2">&lt;chatlas._chat.ChatResponseAsync object at 0x111a5c560&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<p><a href="https://posit-dev.github.io/chatlas/reference/Chat.html#chatlas.Chat.chat_async" target="_blank" rel="noopener"><code>chat_async()</code></a>
 returns a coroutine object, which is basically a special kind of function that runs asynchronously. It doesn&rsquo;t do the work right away when you call it, but it gives you this object that you can later &ldquo;await&rdquo; to actually get the result. If you&rsquo;re running regular (non-asynchronous) code, you use <code>asyncio.run()</code> to start and wait for the task to finish.</p>
</div>
<div id="tabset-2-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><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-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">promises</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">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="s">&#34;You are a presentation coach for data scientists.
</span></span></span><span class="line"><span class="cl"><span class="s">  You give constructive, focused, and practical feedback on titles, structure, and storytelling.&#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">chat</span><span class="o">$</span><span class="nf">chat_async</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;I&#39;m working on a presentation with the title: &#39;The Shiny Side of LLMs&#39;.
</span></span></span><span class="line"><span class="cl"><span class="s">Please evaluate the clarity, tone, and relevance of this title for the intended audience.
</span></span></span><span class="line"><span class="cl"><span class="s">For context, this is a 10-minute lightning talk at posit::conf(2025).
</span></span></span><span class="line"><span class="cl"><span class="s">The audience is Python and R users who are curious about AI and large language models,
</span></span></span><span class="line"><span class="cl"><span class="s">but not all of them have a deep technical background.
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">The talk uses Shiny as a way to explore and demo LLMs in practice.
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">Return your answer as a JSON array of objects, where each object has the following keys:
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">- &#39;aspect&#39;: one of &#39;clarity&#39;, &#39;tone&#39;, or &#39;relevance&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">- &#39;feedback&#39;: your concise assessment
</span></span></span><span class="line"><span class="cl"><span class="s">- &#39;suggestion&#39;: an optional improvement if applicable&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="o">%...&gt;%</span> <span class="nf">print</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show output</summary>
<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="c1">#&gt; [1] &#34;```json\n[\n  {\n    \&#34;aspect\&#34;: \&#34;clarity\&#34;,\n    \&#34;feedback\&#34;: \&#34;The title is somewhat ambiguous - &#39;shiny side&#39; could mean positive aspects of LLMs or reference the Shiny framework. The wordplay may confuse rather than clarify the content focus.\&#34;,\n    \&#34;suggestion\&#34;: \&#34;Consider &#39;Building LLM Demos with Shiny&#39; or &#39;Interactive LLM Exploration Using Shiny&#39; to clearly communicate both the tool and topic.\&#34;\n  },\n  {\n    \&#34;aspect\&#34;: \&#34;tone\&#34;,\n    \&#34;feedback\&#34;: \&#34;The playful wordplay fits well with the lightning talk format and conference atmosphere. It&#39;s approachable and not intimidating for audiences with varying technical backgrounds.\&#34;,\n    \&#34;suggestion\&#34;: null\n  },\n  {\n    \&#34;aspect\&#34;: \&#34;relevance\&#34;,\n    \&#34;feedback\&#34;: \&#34;Highly relevant for posit::conf audience who are familiar with Shiny. The title connects a trending AI topic with a beloved R/Python tool, making LLMs accessible to the community.\&#34;,\n    \&#34;suggestion\&#34;: \&#34;Consider adding a subtitle for context: &#39;The Shiny Side of LLMs: Interactive Demos for AI Exploration&#39; to enhance relevance while keeping the clever wordplay.\&#34;\n  }\n]\n```&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<p><a href="https://ellmer.tidyverse.org/reference/Chat.html#method-chat-async-" target="_blank" rel="noopener"><code>chat_async()</code></a>
 starts the work and returns a promise, this special kind of placeholder. Then <a href="https://rstudio.github.io/promises/reference/pipes.html" target="_blank" rel="noopener"><code>%...&gt;%</code></a>
 attaches the next step, like printing the result, once it&rsquo;s ready. This keeps your R session running without waiting or freezing. Note that it resolves to a string (probably Markdown), which is slightly different than just using the <code>chat()</code> method. This is also why the output looks a little bit different compared to <a href="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/../../../blog/shiny/shiny-side-of-llms-part-2/">part two of this series</a>
.</p>
</div>
</div>
<p>And before you&rsquo;re thinking: &ldquo;hey, we were using structured output <a href="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/../../../blog/shiny/shiny-side-of-llms-part-2/">in the last part</a>
, right?&rdquo; Yes! Luckily there&rsquo;s also a method called <code>chat_structured_async()</code> (see docs for <a href="https://posit-dev.github.io/chatlas/reference/Chat.html#chatlas.Chat.chat_structured_async" target="_blank" rel="noopener">Python</a>
 and <a href="https://ellmer.tidyverse.org/reference/Chat.html#method-extract-data-async-" target="_blank" rel="noopener">R</a>
). How convenient! We&rsquo;ll use that a little bit later.</p>
<h1 id="chatting-with-an-llm-via-shiny">Chatting with an LLM via Shiny
</h1>
<p>Never developed a Shiny app before? That&rsquo;s ok! Before building our DeckCheck app, we&rsquo;ll start with a very basic example that allows you to chat with any LLM, just like you type in your question at ChatGPT. And while this series isn&rsquo;t about building &ldquo;just a chatbot&rdquo;, you can perfectly do so with Shiny. Minimal code required to get started. If you have experience with Shiny this code won&rsquo;t have much surprises, but if you&rsquo;re new to Shiny there&rsquo;s a mini crash-course-like explanation below the code.</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">
<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-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">ui</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatAnthropic</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define UI</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_fluid</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">h1</span><span class="p">(</span><span class="s2">&#34;DeckCheck&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Card with chat component</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;Get started&#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">p</span><span class="p">(</span><span class="s2">&#34;Ask me anything about your presentation 💡&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Chat component</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;my_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="c1"># Define server</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="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;my_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_client</span> <span class="o">=</span> <span class="n">ChatAnthropic</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;claude-sonnet-4-20250514&#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;You are a presentation coach for data scientists. You give constructive, focused, and practical feedback on titles, structure, and storytelling.&#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">@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">chat_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></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="c1"># Create app</span>
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>In the above code <code>app_ui</code> defines the front end of the app. It&rsquo;s everything the user sees and interacts with. Here, it&rsquo;s created with <code>ui.page_fluid()</code> and contains a heading with our app title, a <code>ui.card()</code> with a header (<code>ui.card_header()</code>) and short paragraph (<code>ui.p()</code>). Below, there&rsquo;s a <code>ui.chat_ui()</code> component that serves as the chat interface. The <code>server</code> function controls the app&rsquo;s back end and this part is responsible for how it responds to user actions. Inside this server, we first create a <code>chat</code> object that connects to the chat UI via its ID (the o-so original <code>&quot;my_chat&quot;</code>). Then, we create a <code>chat_client</code> object via <code>chatlas</code>.</p>
<p>The key part is the <code>@chat.on_user_submit</code> decorator. <code>.on_user_submit</code> lets you define a function that runs when the user hits &ldquo;send&rdquo; in the chat UI. This function, in our case called <code>handle_user_input</code>, sends the message asynchronously to the model via <code>chat_client.stream_async()</code> and streams the reply back into the chat UI with <code>append_message_stream()</code>. Under the hood, Shiny makes use of the <a href="https://github.com/posit-dev/shinychat" target="_blank" rel="noopener"><code>shinychat</code></a>
 package.</p>
<p>When we asks a question (e.g. our simple &ldquo;I&rsquo;m working on a presentation with the title: &lsquo;The Shiny Side of LLMs&rsquo;. What&rsquo;s your feedback just based on that title?&rdquo;) we get a nicely formatted response back:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-chat-py.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
<div id="tabset-3-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span></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">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">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="nf">page_fluid</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">theme</span> <span class="o">=</span> <span class="nf">bs_theme</span><span class="p">(</span><span class="n">bootswatch</span> <span class="o">=</span> <span class="s">&#34;flatly&#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"># App title</span>
</span></span><span class="line"><span class="cl">  <span class="nf">h1</span><span class="p">(</span><span class="s">&#34;DeckCheck&#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 card</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;Get started&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="nf">p</span><span class="p">(</span><span class="s">&#34;Ask me anything about your presentation 💡&#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"># Chat component</span>
</span></span><span class="line"><span class="cl">    <span class="nf">chat_mod_ui</span><span class="p">(</span><span class="s">&#34;my_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="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="s">&#34;You are a presentation coach for data scientists. 
</span></span></span><span class="line"><span class="cl"><span class="s">  You give constructive, focused, and practical feedback on titles, structure, and storytelling.&#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="nf">chat_mod_server</span><span class="p">(</span><span class="s">&#34;my_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 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><blockquote>
<p><strong>Custom streaming</strong></p>
<p>If you ever wanted to build something more custom, <code>shinychat::markdown_stream()</code> would let you stream model output into any Shiny interface, chat or not.</p>
</blockquote>
<p>In the above code, <code>ui</code> defines the front end of the app. It&rsquo;s everything the user sees and interacts with. Here, it&rsquo;s created with <code>bslib</code>&rsquo;s <code>page_fluid()</code> with a &ldquo;Flatly&rdquo; Bootstrap theme for styling. Don&rsquo;t like Flatly? There are many <a href="https://rstudio.github.io/bslib/articles/theming/index.html" target="_blank" rel="noopener">themes</a>
 to choose from! And if you don&rsquo;t like any, you can just skip the <code>theme</code> argument or get started with <a href="https://shiny.posit.co/r/articles/build/css/" target="_blank" rel="noopener">custom CSS</a>
.</p>
<p>After specifying the theme, there&rsquo;s a heading with our app title, followed by a <code>card()</code> containing a header, a short description, and most importantly, the chat interface provided by <code>chat_mod_ui(&quot;my_chat&quot;)</code> from the <a href="https://github.com/posit-dev/shinychat" target="_blank" rel="noopener"><code>shinychat</code></a>
 package.</p>
<p>The <code>server</code> function controls the app&rsquo;s back end and this part is responsible for how it responds to user actions. Inside this server, we first create a <code>chat_client</code> object using <code>chat_anthropic()</code>. Then we connect this chat object to the UI with <code>chat_mod_server(&quot;my_chat&quot;, chat_client)</code> via its ID (<code>&quot;my_chat&quot;</code>).</p>
<p><code>chat_mod_ui()</code> and <code>chat_mod_server()</code> together form a Shiny module. This module handles sending user messages to the model, receiving responses, and streaming those responses back into the UI, all asynchronously. The async streaming happens &ldquo;under the hood,&rdquo; so you don&rsquo;t have to manually manage partial chunks of text or async calls yourself.</p>
<p>When we asks a question (e.g. our simple &ldquo;I&rsquo;m working on a presentation with the title: &lsquo;The Shiny Side of LLMs&rsquo;. What&rsquo;s your feedback just based on that title?&rdquo;) we get a nicely formatted response back:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-chat-r.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
</div>
<h1 id="building-deckcheck">Building DeckCheck
</h1>
<p>All the ingredients are there now: we know how to programmatically talk to an LLM, we can make an informed choice when it comes to streaming (or not streaming) and async usage, and we&rsquo;ve seen how to combine it in a simple chat interface (with a bit of help from <code>shinychat</code>). Time to apply that knowledge to our DeckCheck app.</p>
<p>But, first things first&hellip; A design. Nothing too fancy, just a quick conceptual drawing of what our app will look like. No matter what kind of app you&rsquo;re developing, this is always the (and honestly, often overlooked) first step. If you know what you&rsquo;re building towards, it&rsquo;s way easier to start coding. Here&rsquo;s our design:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-design-1.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-design-2.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>We have a section where users can upload information (most importantly, the Quarto presentation itself, but also the required information like audience), there are some value boxes, there&rsquo;s a graph, and a table. Elements you will frequently encounter when developing data-savvy apps. For a reason, of course, as we want to provide you with all the necessary building blocks.</p>
<p>To bring this design to life, here&rsquo;s what we&rsquo;re working towards:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-deckcheck-full.gif"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>But first things first: let&rsquo;s start with building the basic UI before we connect the server part to it.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-4" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-4-1">Python</a></li>
<li><a href="#tabset-4-2">R</a></li>
</ul>
<div id="tabset-4-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">  1
</span><span class="lnt">  2
</span><span class="lnt">  3
</span><span class="lnt">  4
</span><span class="lnt">  5
</span><span class="lnt">  6
</span><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><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</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">ui</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">shinyswatch</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"># Icons</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ======================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># SVG icon copied from https://icons.getbootstrap.com/icons/file-slides-fill/</span>
</span></span><span class="line"><span class="cl"><span class="n">file_slides</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; fill=&#34;currentColor&#34; class=&#34;bi bi-file-slides-fill&#34; viewBox=&#34;0 0 16 16&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M7 7.78V5.22c0-.096.106-.156.19-.106l2.13 1.279a.125.125 0 0 1 0 .214l-2.13 1.28A.125.125 0 0 1 7 7.778z&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M5 4h6a.5.5 0 0 1 .496.438l.5 4A.5.5 0 0 1 11.5 9h-3v2.016c.863.055 1.5.251 1.5.484 0 .276-.895.5-2 .5s-2-.224-2-.5c0-.233.637-.429 1.5-.484V9h-3a.5.5 0 0 1-.496-.562l.5-4A.5.5 0 0 1 5 4&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">&lt;/svg&gt;&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">file_code</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; fill=&#34;currentColor&#34; class=&#34;bi bi-file-code-fill&#34; viewBox=&#34;0 0 16 16&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M6.646 5.646a.5.5 0 1 1 .708.708L5.707 8l1.647 1.646a.5.5 0 0 1-.708.708l-2-2a.5.5 0 0 1 0-.708zm2.708 0 2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 8 8.646 6.354a.5.5 0 1 1 .708-.708&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">&lt;/svg&gt;&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">file_image</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; fill=&#34;currentColor&#34; class=&#34;bi bi-file-image-fill&#34; viewBox=&#34;0 0 16 16&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M4 0h8a2 2 0 0 1 2 2v8.293l-2.73-2.73a1 1 0 0 0-1.52.127l-1.889 2.644-1.769-1.062a1 1 0 0 0-1.222.15L2 12.292V2a2 2 0 0 1 2-2m4.002 5.5a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M10.564 8.27 14 11.708V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-.293l3.578-3.577 2.56 1.536 2.426-3.395z&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">&lt;/svg&gt;&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">robot</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; width=&#34;16&#34; height=&#34;16&#34; fill=&#34;currentColor&#34; class=&#34;bi bi-robot&#34; viewBox=&#34;0 0 16 16&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">&lt;/svg&gt;&#34;&#34;&#34;</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"># Shiny App</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_fillable</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">layout_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">div</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 class="n">ui</span><span class="o">.</span><span class="n">strong</span><span class="p">(</span><span class="s2">&#34;Hey, I am DeckCheck!&#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">p</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;&#34;&#34;I can help you improve your Quarto presentations by analysing them and suggesting improvements. Before I can do that, I need some information about your presentation.&#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="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">ui</span><span class="o">.</span><span class="n">input_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;file&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Upload your Quarto presentation&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">accept</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;.qmd&#34;</span><span class="p">,</span> <span class="s2">&#34;.qmdx&#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">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">input_text_area</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;audience&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Describe your audience&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">height</span><span class="o">=</span><span class="s2">&#34;150px&#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;e.g. Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#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_numeric</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;length&#34;</span><span class="p">,</span> <span class="s2">&#34;Time cap for the presentation (minutes)&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span> <span class="nb">min</span><span class="o">=</span><span class="mi">1</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_text</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;type&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Type of talk&#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;e.g. lightning talk, workshop, or keynote&#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_text</span><span class="p">(</span><span class="s2">&#34;event&#34;</span><span class="p">,</span> <span class="s2">&#34;Event name&#34;</span><span class="p">,</span> <span class="n">placeholder</span><span class="o">=</span><span class="s2">&#34;e.g. posit::conf(2025)&#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_task_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;submit&#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">ui</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">robot</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;Analyse presentation&#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">width</span><span class="o">=</span><span class="mi">400</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">layout_column_wrap</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">value_box</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Showtime&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;9 minutes&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">showcase</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">file_slides</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">theme</span><span class="o">=</span><span class="s2">&#34;primary&#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">value_box</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Code Savviness&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;15%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">showcase</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">file_code</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">theme</span><span class="o">=</span><span class="s2">&#34;primary&#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">value_box</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Image Presence&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;7%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">showcase</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">file_image</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">theme</span><span class="o">=</span><span class="s2">&#34;primary&#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">width</span><span class="o">=</span><span class="mi">1</span> <span class="o">/</span> <span class="mi">3</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">layout_column_wrap</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="n">ui</span><span class="o">.</span><span class="n">strong</span><span class="p">(</span><span class="s2">&#34;Scores per category&#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">p</span><span class="p">(</span><span class="s2">&#34;My beautiful interactive plot...&#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">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">strong</span><span class="p">(</span><span class="s2">&#34;Suggested improvements per category&#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">p</span><span class="p">(</span><span class="s2">&#34;My beatiful 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="n">width</span><span class="o">=</span><span class="mi">1</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">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="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Bootswatch theme</span>
</span></span><span class="line"><span class="cl">    <span class="n">theme</span><span class="o">=</span><span class="n">shinyswatch</span><span class="o">.</span><span class="n">theme</span><span class="o">.</span><span class="n">flatly</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="c1"># Empty for demo purposes</span>
</span></span><span class="line"><span class="cl">    <span class="k">pass</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-4-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><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></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></span><span class="line"><span class="cl"><span class="n">ui</span> <span class="o">&lt;-</span> <span class="nf">page_fillable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="c1">## General theme and styles</span>
</span></span><span class="line"><span class="cl">  <span class="n">theme</span> <span class="o">=</span> <span class="nf">bs_theme</span><span class="p">(</span><span class="n">bootswatch</span> <span class="o">=</span> <span class="s">&#34;flatly&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="nf">layout_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1">## Sidebar content</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">width</span> <span class="o">=</span> <span class="m">400</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Open sidebar on mobile devices and show above content</span>
</span></span><span class="line"><span class="cl">      <span class="n">open</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span><span class="n">mobile</span> <span class="o">=</span> <span class="s">&#34;always-above&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">strong</span><span class="p">(</span><span class="nf">p</span><span class="p">(</span><span class="s">&#34;Hey, I am DeckCheck!&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">p</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s">&#34;I can help you improve your Quarto presentations by analysing them and suggesting improvements.
</span></span></span><span class="line"><span class="cl"><span class="s">      Before I can do that, I need some information about your presentation.&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">fileInput</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;file&#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;Upload your Quarto presentation&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">accept</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;.qmd&#34;</span><span class="p">,</span> <span class="s">&#34;.qmdx&#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">textAreaInput</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;audience&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">height</span> <span class="o">=</span> <span class="s">&#34;150px&#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;Describe your audience&#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="s">&#34;e.g. Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">numericInput</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;length&#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;Time cap for the presentation (minutes)&#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="m">10</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">textInput</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;type&#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;Type of talk&#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="s">&#34;e.g. lightning talk, workshop, or keynote&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">textInput</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;event&#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;Event name&#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="s">&#34;e.g. posit::conf(2025)&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">input_task_button</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;submit&#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">shiny</span><span class="o">::</span><span class="nf">tagList</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">bsicons</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><span class="line"><span class="cl">          <span class="s">&#34;Analyse presentation&#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">label_busy</span> <span class="o">=</span> <span class="s">&#34;DeckCheck is checking...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">type</span> <span class="o">=</span> <span class="s">&#34;default&#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="c1">## Main content</span>
</span></span><span class="line"><span class="cl">    <span class="nf">layout_column_wrap</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="c1">### Value boxes for metrics</span>
</span></span><span class="line"><span class="cl">      <span class="nf">value_box</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;Showtime&#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="s">&#34;9 minutes&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">showcase</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;file-slides&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">theme</span> <span class="o">=</span> <span class="s">&#34;primary&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">value_box</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;Code Savviness&#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="s">&#34;15%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">showcase</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;file-code&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">theme</span> <span class="o">=</span> <span class="s">&#34;primary&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">value_box</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;Image Presence&#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="s">&#34;7%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">showcase</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;file-image&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">theme</span> <span class="o">=</span> <span class="s">&#34;primary&#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="nf">layout_column_wrap</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="n">width</span> <span class="o">=</span> <span class="m">1</span> <span class="o">/</span> <span class="m">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="c1">### Graph with scoring metrics</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">strong</span><span class="p">(</span><span class="s">&#34;Scores per category&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">p</span><span class="p">(</span><span class="s">&#34;My beatiful interactive plot...&#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">### Table with suggested improvements</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">strong</span><span class="p">(</span><span class="s">&#34;Suggested improvements per category&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">p</span><span class="p">(</span><span class="s">&#34;My beatiful 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 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></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>No matter what language you use to display this basic UI, the result is the same:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-ui-only.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>That&rsquo;s already a start! Of course it doesn&rsquo;t do anything yet and is filled with placeholders, so we need some logic in the server part. The main engine behind DeckCheck is our conversation with the LLM. This logic is <em>almost</em> a copy-paste from part two, <a href="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/../../../blog/shiny/shiny-side-of-llms-part-2/">Talking to LLMs: From Prompt to Response</a>
, combined with what we learned earlier in this article about async.</p>
<p>The star of this main engine is something called &ldquo;extended task&rdquo;. As mentioned previously, by default, Shiny runs code synchronously. That means if we ask it to render a Quarto presentation or send a request to an LLM, the app would block until that job is done. The whole interface would freeze. That&rsquo;s no fun for the user. That&rsquo;s why Shiny has an option to run extended tasks (<a href="https://shiny.posit.co/py/docs/nonblocking.html" target="_blank" rel="noopener"><code>extended_task</code></a>
 in Python, <a href="https://shiny.posit.co/r/articles/improve/nonblocking/" target="_blank" rel="noopener"><code>ExtendedTask</code></a>
 in R). It lets us run non-blocking jobs asynchronously in the background, so our app can stay responsive. It works together with a special action button, <code>input_task_button</code>, which is designed to trigger long running tasks. In order for this button to work you need to <strong>bind</strong> the button to the extended task with <code>bind_task_button</code>.</p>
<p>For DeckCheck, there are two main jobs to do:</p>
<ol>
<li><strong>Render the presentation (we call this task<code>quarto_task</code>)</strong>
<ul>
<li>Takes the uploaded Quarto file.</li>
<li>Runs Quarto to produce Markdown and HTML versions.</li>
<li>Returns the file path to the Markdown document.</li>
</ul>
</li>
<li><strong>Analyse our Markdown slides with the LLM (we call this task<code>chat_task</code>)</strong>
<ul>
<li>Waits for the Markdown to be ready, so it can read the content of the Markdown file using the file path.</li>
<li>Starts a chat session with the model.</li>
<li>Runs two subtasks:
<ul>
<li>A &ldquo;regular&rdquo; async chat where the LLM can use our tool to count slides and calculate percentages.</li>
<li>A structured async chat that returns clean, structured data according to our data model.</li>
</ul>
</li>
</ul>
</li>
</ol>
<p>Because the LLM depends on the Markdown output, we have to <strong>chain these tasks</strong>: the button click first kicks off <code>quarto_task</code>, and only when that&rsquo;s finished, we run <code>chat_task</code>.</p>
<p>This chaining is done through Shiny&rsquo;s reactive system:</p>
<ul>
<li>A reactive effect (<code>run_quarto</code> in Python) / observer (R) responds to the button press and invokes our first extended task: <code>quarto_task</code>.</li>
<li>Another reactive effect (<code>run_chat</code> in Python) / observer (R) listens for <code>quarto_task</code> to finish, then reads the rendered Markdown using the file path and invokes our second extended task: <code>chat_task</code>.</li>
<li><code>analysis_result</code> is a reactive that listens for <code>chat_task</code> to complete and prepares the final output for the UI.</li>
</ul>
<p>You can think of it as a pipeline:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-pipeline.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>This setup keeps the app responsive, ensures tasks run in the right order, and makes the logic clear: the button starts things off, the tasks are executed async (but in order!), and our desired data ends up in a reactive that we can use as information source for all the UI components.</p>
<blockquote>
<p><strong>The world of async programming</strong></p>
<p>If you&rsquo;re used to synchronous code and just running scripts, using asynchronous tasks might feel a bit&hellip; complicated? Confusing, perhaps? Ultimately, it requires you to think differently. If you&rsquo;re keen to learn more, and getting hands on with a bunch of different examples, you can check out the <a href="https://shiny.posit.co/py/docs/nonblocking.html" target="_blank" rel="noopener">Python</a>
 or <a href="https://shiny.posit.co/r/articles/improve/nonblocking/" target="_blank" rel="noopener">R</a>
 docs about non-blocking operations. The <a href="https://github.com/hypebright/async_shiny/tree/main" target="_blank" rel="noopener">async_shiny</a>
 repo also contains examples.</p>
</blockquote>
<blockquote>
<p><strong>A note on code snippets</strong></p>
<p>Big chunks of code are generally not nice to look at. So, to make sure it&rsquo;s not too overwhelming we&rsquo;ll take a look at some snippets from the finalised DeckCheck app. Note that you can&rsquo;t run these snippets on their own. If you want to run the full DeckCheck application you can head over to the <a href="#the-end-result">the end result</a>
.</p>
</blockquote>
<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">
<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><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span><span class="lnt">153
</span><span class="lnt">154
</span><span class="lnt">155
</span><span class="lnt">156
</span><span class="lnt">157
</span><span class="lnt">158
</span><span class="lnt">159
</span><span class="lnt">160
</span><span class="lnt">161
</span><span class="lnt">162
</span><span class="lnt">163
</span><span class="lnt">164
</span><span class="lnt">165
</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"># Data Structure</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ======================</span>
</span></span><span class="line"><span class="cl"><span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mi">10</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">PercentType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">float</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mf">100.0</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">MinutesType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">SlideCount</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</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="k">class</span> <span class="nc">ScoringCategory</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">score</span><span class="p">:</span> <span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Score from 1–10.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">justification</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Brief explanation of the score.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">improvements</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Concise, actionable improvements, mentioning slide numbers if applicable.&#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">score_after_improvements</span><span class="p">:</span> <span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Estimated score after suggested improvements.&#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="k">class</span> <span class="nc">DeckAnalysis</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">presentation_title</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;The presentation title.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">total_slides</span><span class="p">:</span> <span class="n">SlideCount</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_code</span><span class="p">:</span> <span class="n">PercentType</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_images</span><span class="p">:</span> <span class="n">PercentType</span>
</span></span><span class="line"><span class="cl">    <span class="n">estimated_duration_minutes</span><span class="p">:</span> <span class="n">MinutesType</span>
</span></span><span class="line"><span class="cl">    <span class="n">tone</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Brief description of the tone of the presentation.&#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">clarity</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?&#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">relevance</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Assess how well the content matches the audience&#39;s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?&#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"># Truncated for brevity</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"># Shiny App</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_fillable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1">## Our 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">.</span><span class="n">input_task_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;submit&#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">ui</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">robot</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;Analyse presentation&#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"># ...</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="nd">@ui.bind_task_button</span><span class="p">(</span><span class="n">button_id</span><span class="o">=</span><span class="s2">&#34;submit&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@reactive.extended_task</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">quarto_task</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">temp_dir</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># We&#39;re using an Extended Task to avoid blocking. Note that</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># a temporary directory called within mirai will be</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># different from the one in the &#34;main&#34; Shiny session. Hence,</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># we pass a temp_dir parameter to the task and use that.</span>
</span></span><span class="line"><span class="cl">        <span class="n">qmd_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">temp_dir</span><span class="p">)</span> <span class="o">/</span> <span class="s2">&#34;my-presentation.qmd&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">shutil</span><span class="o">.</span><span class="n">copy</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">qmd_file</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Run asyncio subprocess</span>
</span></span><span class="line"><span class="cl">        <span class="n">proc</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">create_subprocess_exec</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;quarto&#34;</span><span class="p">,</span> <span class="s2">&#34;render&#34;</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">qmd_file</span><span class="p">),</span> <span class="s2">&#34;--to&#34;</span><span class="p">,</span> <span class="s2">&#34;markdown,html&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="n">proc</span><span class="o">.</span><span class="n">communicate</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Return the path to the markdown file</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">Path</span><span class="p">(</span><span class="n">temp_dir</span><span class="p">)</span> <span class="o">/</span> <span class="s2">&#34;my-presentation.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@ui.bind_task_button</span><span class="p">(</span><span class="n">button_id</span><span class="o">=</span><span class="s2">&#34;submit&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@reactive.extended_task</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">chat_task</span><span class="p">(</span><span class="n">system_prompt</span><span class="p">,</span> <span class="n">markdown_content</span><span class="p">,</span> <span class="n">DeckAnalysis</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># We&#39;re using an extended task to avoid blocking the session and</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># we start a fresh chat session each time.</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># For a feedback loop, we would use a persistent chat session.</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat</span> <span class="o">=</span> <span class="n">ChatAnthropic</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;claude-sonnet-4-20250514&#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="n">system_prompt</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"># Set model parameters (optional)</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat</span><span class="o">.</span><span class="n">set_model_params</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">temperature</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span>  <span class="c1"># default is 1</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 the chat</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">calculate_slide_metric</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Task 1: regular chat to extract meta-data</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat_res1</span> <span class="o">=</span> <span class="k">await</span> <span class="n">chat</span><span class="o">.</span><span class="n">chat_async</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Execute Task 1 (counts). Here are the slides in Markdown: {{ markdown_content }}&#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></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">chat_res1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Task 2: structured chat to further analyse the slides</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat_res2</span> <span class="o">=</span> <span class="k">await</span> <span class="n">chat</span><span class="o">.</span><span class="n">chat_structured_async</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Execute Task 2 (suggestions)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">data_model</span><span class="o">=</span><span class="n">DeckAnalysis</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">return</span> <span class="n">chat_res2</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">submit</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">run_quarto</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">input</span><span class="o">.</span><span class="n">file</span><span class="p">()</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Get file path of the uploaded file</span>
</span></span><span class="line"><span class="cl">        <span class="n">file_path</span> <span class="o">=</span> <span class="nb">input</span><span class="o">.</span><span class="n">file</span><span class="p">()[</span><span class="mi">0</span><span class="p">][</span><span class="s2">&#34;datapath&#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">quarto_task</span><span class="o">.</span><span class="n">invoke</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">tempfile</span><span class="o">.</span><span class="n">gettempdir</span><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="k">def</span> <span class="nf">run_chat</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># require quarto_task result to be available</span>
</span></span><span class="line"><span class="cl">        <span class="n">req</span><span class="p">(</span><span class="n">quarto_task</span><span class="o">.</span><span class="n">result</span><span class="p">()</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># Get the Markdown file path from the complete quarto_task</span>
</span></span><span class="line"><span class="cl">        <span class="n">markdown_file</span> <span class="o">=</span> <span class="n">quarto_task</span><span class="o">.</span><span class="n">result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Read the generated Markdown file containing the slides</span>
</span></span><span class="line"><span class="cl">        <span class="n">markdown_content</span> <span class="o">=</span> <span class="n">markdown_file</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#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"># Define prompt file</span>
</span></span><span class="line"><span class="cl">        <span class="n">system_prompt_file</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">ROOT_DIR</span> <span class="o">/</span> <span class="s2">&#34;prompts&#34;</span> <span class="o">/</span> <span class="s2">&#34;prompt-analyse-slides-structured-tool.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"># Create system prompt</span>
</span></span><span class="line"><span class="cl">        <span class="n">system_prompt</span> <span class="o">=</span> <span class="n">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">variables</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;audience&#34;</span><span class="p">:</span> <span class="nb">input</span><span class="o">.</span><span class="n">audience</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;length&#34;</span><span class="p">:</span> <span class="nb">input</span><span class="o">.</span><span class="n">length</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="nb">input</span><span class="o">.</span><span class="n">type</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;event&#34;</span><span class="p">:</span> <span class="nb">input</span><span class="o">.</span><span class="n">event</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;markdown_content&#34;</span><span class="p">:</span> <span class="n">markdown_content</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"># Trigger the chat task with the provided inputs</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat_task</span><span class="o">.</span><span class="n">invoke</span><span class="p">(</span><span class="n">system_prompt</span><span class="p">,</span> <span class="n">markdown_content</span><span class="p">,</span> <span class="n">DeckAnalysis</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># This reactive will be used as information source for</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># all the UI elements. It uses the result from the chat_task,</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># and does some data wrangling</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">analysis_result</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">res</span> <span class="o">=</span> <span class="n">chat_task</span><span class="o">.</span><span class="n">result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">res</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="c1"># Some data wrangling</span>
</span></span><span class="line"><span class="cl">                <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="n">res</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Note that <code>quarto_task</code> is marked <code>async</code>, so it automatically returns a coroutine. You need a coroutine for <code>extended_task</code> to work. The subprocess call to Quarto runs without blocking (<code>await asyncio.create_subprocess_exec</code>). Once Quarto finishes, the task resolves with the path to the Markdown file. The async chat tasks from <code>chatlas</code> return a coroutine too.</p>
</div>
<div id="tabset-5-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">  1
</span><span class="lnt">  2
</span><span class="lnt">  3
</span><span class="lnt">  4
</span><span class="lnt">  5
</span><span class="lnt">  6
</span><span class="lnt">  7
</span><span class="lnt">  8
</span><span class="lnt">  9
</span><span class="lnt"> 10
</span><span class="lnt"> 11
</span><span class="lnt"> 12
</span><span class="lnt"> 13
</span><span class="lnt"> 14
</span><span class="lnt"> 15
</span><span class="lnt"> 16
</span><span class="lnt"> 17
</span><span class="lnt"> 18
</span><span class="lnt"> 19
</span><span class="lnt"> 20
</span><span class="lnt"> 21
</span><span class="lnt"> 22
</span><span class="lnt"> 23
</span><span class="lnt"> 24
</span><span class="lnt"> 25
</span><span class="lnt"> 26
</span><span class="lnt"> 27
</span><span class="lnt"> 28
</span><span class="lnt"> 29
</span><span class="lnt"> 30
</span><span class="lnt"> 31
</span><span class="lnt"> 32
</span><span class="lnt"> 33
</span><span class="lnt"> 34
</span><span class="lnt"> 35
</span><span class="lnt"> 36
</span><span class="lnt"> 37
</span><span class="lnt"> 38
</span><span class="lnt"> 39
</span><span class="lnt"> 40
</span><span class="lnt"> 41
</span><span class="lnt"> 42
</span><span class="lnt"> 43
</span><span class="lnt"> 44
</span><span class="lnt"> 45
</span><span class="lnt"> 46
</span><span class="lnt"> 47
</span><span class="lnt"> 48
</span><span class="lnt"> 49
</span><span class="lnt"> 50
</span><span class="lnt"> 51
</span><span class="lnt"> 52
</span><span class="lnt"> 53
</span><span class="lnt"> 54
</span><span class="lnt"> 55
</span><span class="lnt"> 56
</span><span class="lnt"> 57
</span><span class="lnt"> 58
</span><span class="lnt"> 59
</span><span class="lnt"> 60
</span><span class="lnt"> 61
</span><span class="lnt"> 62
</span><span class="lnt"> 63
</span><span class="lnt"> 64
</span><span class="lnt"> 65
</span><span class="lnt"> 66
</span><span class="lnt"> 67
</span><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><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span><span class="lnt">153
</span><span class="lnt">154
</span><span class="lnt">155
</span><span class="lnt">156
</span><span class="lnt">157
</span><span class="lnt">158
</span><span class="lnt">159
</span><span class="lnt">160
</span><span class="lnt">161
</span><span class="lnt">162
</span><span class="lnt">163
</span><span class="lnt">164
</span><span class="lnt">165
</span><span class="lnt">166
</span><span class="lnt">167
</span><span class="lnt">168
</span><span class="lnt">169
</span><span class="lnt">170
</span><span class="lnt">171
</span><span class="lnt">172
</span><span class="lnt">173
</span><span class="lnt">174
</span><span class="lnt">175
</span><span class="lnt">176
</span><span class="lnt">177
</span><span class="lnt">178
</span><span class="lnt">179
</span><span class="lnt">180
</span><span class="lnt">181
</span><span class="lnt">182
</span><span class="lnt">183
</span><span class="lnt">184
</span><span class="lnt">185
</span><span class="lnt">186
</span><span class="lnt">187
</span><span class="lnt">188
</span><span class="lnt">189
</span><span class="lnt">190
</span><span class="lnt">191
</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"># Data Structure</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ======================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Reusable scoring category</span>
</span></span><span class="line"><span class="cl"><span class="n">type_scoring_category</span> <span class="o">&lt;-</span> <span class="nf">type_object</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">score</span> <span class="o">=</span> <span class="nf">type_integer</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;Score from 1 to 10.&#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">justification</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Brief explanation of the score.&#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">improvements</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Concise, actionable improvements, mentioning slide numbers if applicable.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">required</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">score_after_improvements</span> <span class="o">=</span> <span class="nf">type_integer</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;Estimated score after suggested improvements.&#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></span><span class="line"><span class="cl"><span class="c1"># Top-level deck analysis object</span>
</span></span><span class="line"><span class="cl"><span class="n">type_deck_analysis</span> <span class="o">&lt;-</span> <span class="nf">type_object</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">presentation_title</span> <span class="o">=</span> <span class="nf">type_string</span><span class="p">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">&#34;The presentation title.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">total_slides</span> <span class="o">=</span> <span class="nf">type_integer</span><span class="p">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Total number of slides.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">percent_with_code</span> <span class="o">=</span> <span class="nf">type_number</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;Percentage of slides containing code blocks (0–100).&#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">percent_with_images</span> <span class="o">=</span> <span class="nf">type_number</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;Percentage of slides containing images (0–100).&#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">estimated_duration_minutes</span> <span class="o">=</span> <span class="nf">type_integer</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;Estimated presentation length in minutes, assuming ~1 minute per text slide and 2–3 minutes per code or image-heavy slide.&#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">tone</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Brief description of the presentation tone (e.g., informal, technical, playful).&#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">clarity</span> <span class="o">=</span> <span class="nf">type_array</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;Evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">relevance</span> <span class="o">=</span> <span class="nf">type_array</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;Asses how well the content matches the audience&#39;s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Truncated for brevity</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"># Shiny App</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_fillable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="c1">## Our UI</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">  <span class="nf">input_task_button</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;submit&#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">shiny</span><span class="o">::</span><span class="nf">tagList</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">bsicons</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><span class="line"><span class="cl">      <span class="s">&#34;Analyse presentation&#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">label_busy</span> <span class="o">=</span> <span class="s">&#34;DeckCheck is checking...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type</span> <span class="o">=</span> <span class="s">&#34;default&#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"># ...</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">quarto_task</span> <span class="o">&lt;-</span> <span class="n">ExtendedTask</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span><span class="kr">function</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">temp_dir</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># We&#39;re using an Extended Task to avoid blocking. Note that</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># a temporary directory called within mirai will be</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># different from the one in the &#34;main&#34; Shiny session. Hence,</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># we pass a temp_dir parameter to the task and use that.</span>
</span></span><span class="line"><span class="cl">    <span class="nf">mirai</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">qmd_file</span> <span class="o">&lt;-</span> <span class="nf">file.path</span><span class="p">(</span><span class="n">temp_dir</span><span class="p">,</span> <span class="s">&#34;my-presentation.qmd&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nf">file.copy</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">qmd_file</span><span class="p">,</span> <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></span><span class="line"><span class="cl">        <span class="n">quarto</span><span class="o">::</span><span class="nf">quarto_render</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">input</span> <span class="o">=</span> <span class="n">qmd_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">output_format</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;markdown&#34;</span><span class="p">,</span> <span class="s">&#34;html&#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"># Return the path to the markdown file</span>
</span></span><span class="line"><span class="cl">        <span class="nf">file.path</span><span class="p">(</span><span class="n">temp_dir</span><span class="p">,</span> <span class="s">&#34;my-presentation.md&#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"># Use the same environment as the Shiny app</span>
</span></span><span class="line"><span class="cl">      <span class="nf">environment</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 class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">bind_task_button</span><span class="p">(</span><span class="s">&#34;submit&#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_task</span> <span class="o">&lt;-</span> <span class="n">ExtendedTask</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span><span class="kr">function</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">markdown_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_deck_analysis</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># We&#39;re using an Extended Task to avoid blocking the session and</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># we start a fresh chat session each time.</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># For a feedback loop, we would use a persistent chat session.</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="n">system_prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">params</span> <span class="o">=</span> <span class="nf">params</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">temperature</span> <span class="o">=</span> <span class="m">0.8</span> <span class="c1"># default is 1</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 the chat</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">calculate_slide_metric</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Task 1: regular chat to extract meta-data</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat_res</span> <span class="o">&lt;-</span> <span class="n">chat</span><span class="o">$</span><span class="nf">chat_async</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="nf">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s">&#34;Execute Task 1 (counts). Here are the slides in Markdown: {{ markdown_content }}&#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></span><span class="line"><span class="cl">    <span class="n">chat_res</span><span class="o">$</span><span class="nf">then</span><span class="p">(</span><span class="kr">function</span><span class="p">(</span><span class="n">res</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Print the response from Task 1</span>
</span></span><span class="line"><span class="cl">      <span class="nf">cat</span><span class="p">(</span><span class="s">&#34;Response from Task 1:\n&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="nf">cat</span><span class="p">(</span><span class="n">res</span><span class="p">,</span> <span class="s">&#34;\n\n&#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"># Execute next task</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Task 2: structured chat to further analyse the slides</span>
</span></span><span class="line"><span class="cl">      <span class="n">chat</span><span class="o">$</span><span class="nf">chat_structured_async</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s">&#34;Execute Task 2 (suggestions)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">type</span> <span class="o">=</span> <span class="n">type_deck_analysis</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 class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">bind_task_button</span><span class="p">(</span><span class="s">&#34;submit&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nf">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">input</span><span class="o">$</span><span class="n">file</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">input</span><span class="o">$</span><span class="n">audience</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">input</span><span class="o">$</span><span class="n">length</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">input</span><span class="o">$</span><span class="n">type</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">input</span><span class="o">$</span><span class="n">event</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Get file path of the uploaded file</span>
</span></span><span class="line"><span class="cl">    <span class="n">file_path</span> <span class="o">&lt;-</span> <span class="n">input</span><span class="o">$</span><span class="n">file</span><span class="o">$</span><span class="n">datapath</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">quarto_task</span><span class="o">$</span><span class="nf">invoke</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">temp_dir</span> <span class="o">=</span> <span class="nf">tempdir</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">bindEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">submit</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</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">quarto_task</span><span class="o">$</span><span class="nf">result</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1"># Get the Markdown file path from the completed quarto_task</span>
</span></span><span class="line"><span class="cl">    <span class="n">markdown_file</span> <span class="o">&lt;-</span> <span class="n">quarto_task</span><span class="o">$</span><span class="nf">result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Read the generated Markdown file containing the slides</span>
</span></span><span class="line"><span class="cl">    <span class="n">markdown_content</span> <span class="o">&lt;-</span> <span class="nf">readChar</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">,</span> <span class="nf">file.size</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Define prompt file</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt_file</span> <span class="o">&lt;-</span> <span class="n">here</span><span class="o">::</span><span class="nf">here</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;prompts&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;prompt-analyse-slides-structured-tool.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"># Create system prompt</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt</span> <span class="o">&lt;-</span> <span class="nf">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">path</span> <span class="o">=</span> <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">audience</span> <span class="o">=</span> <span class="n">input</span><span class="o">$</span><span class="n">audience</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">length</span> <span class="o">=</span> <span class="n">input</span><span class="o">$</span><span class="n">length</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">type</span> <span class="o">=</span> <span class="n">input</span><span class="o">$</span><span class="n">type</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">event</span> <span class="o">=</span> <span class="n">input</span><span class="o">$</span><span class="n">event</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"># Trigger the chat task with the provided inputs</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat_task</span><span class="o">$</span><span class="nf">invoke</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">system_prompt</span> <span class="o">=</span> <span class="n">system_prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">markdown_content</span> <span class="o">=</span> <span class="n">markdown_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">type_deck_analysis</span> <span class="o">=</span> <span class="n">type_deck_analysis</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"># This reactive will be used as information source for</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># all the UI elements. It uses the result from the chat_task,</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># and does some data wrangling</span>
</span></span><span class="line"><span class="cl">  <span class="n">analysis_result</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="n">named_list</span> <span class="o">&lt;-</span> <span class="n">chat_task</span><span class="o">$</span><span class="nf">result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Some data wrangling</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">named_list</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Note that we wrap the Quarto task in <code>mirai</code> because we need a promise. Basically an object that says: &ldquo;I don&rsquo;t have the answer yet, but I will later&rdquo;. <code>ExtendedTask</code> is built to work with promises and it expects whatever you give it to eventually resolve with a value. The async chat tasks from <code>ellmer</code> return a promise too.</p>
</div>
</div>
<p>In this case, we choose to start a fresh chat session each time. Another way to do this would be to bring up the code that initialises the chat client, set model parameters, and registers our tool so it only runs once at the start of the session. Then, we could use <a href="https://posit-dev.github.io/chatlas/reference/Chat.html#chatlas.Chat.set_turns" target="_blank" rel="noopener"><code>chat.set_turns([])</code></a>
 (Python) / <a href="https://ellmer.tidyverse.org/reference/Chat.html#method-set-turns-" target="_blank" rel="noopener"><code>chat$set_turns([])</code></a>
 (R) before each new &ldquo;chat task&rdquo;. This way, we won&rsquo;t accumulate chat history.</p>
<h1 id="towards-a-better-ui">Towards a better UI
</h1>
<p>There&rsquo;s an engine, and there&rsquo;s a basic UI. Time to put them together. But if we want a Shiny app to truly shine, we need to give the UI a little extra love. In this section, we&rsquo;ll go over all the UI elements that we have in our design, plus some extra goodies. The suggestions here are just examples though, the possibilities go far beyond what we&rsquo;ll cover here.</p>
<h2 id="loading-experience">Loading experience
</h2>
<p>The fact that we&rsquo;re executing tasks asynchronously doesn&rsquo;t change anything about the execution speed of those tasks. It still takes time to render Quarto, and it still takes time to get a response from the LLM. And since we do need to finish one task before starting the other, there&rsquo;s zero time gain compared to running the code synchronously for <strong>individual users</strong>. We simply made the app non-blocking, which is especially valuable if there are multiple users running the app at the same time (concurrently).</p>
<p>We already talked about the impatience of users and how visual feedback can make waiting a bit more enjoyable. So, in DeckCheck, we also need to make sure to provide such feedback. We want it to be informative (e.g. &ldquo;Processing your Quarto presentation&hellip;&rdquo; or &ldquo;The LLM is doing its magic&hellip;&rdquo;) and we want it to be entertaining (bouncing robot anyone?!).</p>
<p>A nice way to add such a custom loading experience is with the help of <a href="https://shiny.posit.co/py/api/core/ui.output_ui.html" target="_blank" rel="noopener"><code>output_ui</code></a>
 (Python) / <a href="https://shiny.posit.co/r/reference/shiny/latest/renderui.html" target="_blank" rel="noopener"><code>uiOutput</code></a>
 (R). It serves as a placeholder in your UI that gets filled later with server-generated UI via <code>render.ui()</code> (Python) / <code>renderUI()</code> (R). This lets you create dynamic interfaces that change depending on app state. For example, we can start by rendering a loading animation (like a bouncing icon), and once the results are ready, replace it with more complex UI elements such as value boxes, a graph, and a table.</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">Python</a></li>
<li><a href="#tabset-6-2">R</a></li>
</ul>
<div id="tabset-6-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><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></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"># Shiny App</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_fillable</span><span class="p">(</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">.</span><span class="n">layout_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="c1"># Sidebar 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 class="c1"># Main content is now dynamic</span>
</span></span><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;results&#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"># ...</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></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="nd">@render.ui</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">results</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">quarto_task</span><span class="o">.</span><span class="n">status</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;running&#34;</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">div</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">HTML</span><span class="p">(</span><span class="n">file_slides_loader</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">br</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 class="s2">&#34;Processing your Quarto presentation...&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">class_</span><span class="o">=</span><span class="s2">&#34;text-center d-flex flex-column justify-content-center align-items-center&#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;height: 100%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">chat_task</span><span class="o">.</span><span class="n">status</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;running&#34;</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">div</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">HTML</span><span class="p">(</span><span class="n">robot_loader</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">br</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 class="s2">&#34;The LLM is doing its magic...&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">class_</span><span class="o">=</span><span class="s2">&#34;text-center d-flex flex-column justify-content-center align-items-center&#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;height: 100%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">chat_task</span><span class="o">.</span><span class="n">status</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;success&#34;</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">TagList</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="c1"># Final results 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="n">app</span> <span class="o">=</span> <span class="n">App</span><span class="p">(</span><span class="n">app_ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-6-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><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></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"># Shiny App</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_fillable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">  <span class="nf">layout_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Sidebar 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 class="c1"># Main content is now dynamic</span>
</span></span><span class="line"><span class="cl">    <span class="nf">uiOutput</span><span class="p">(</span><span class="s">&#34;results&#34;</span><span class="p">,</span> <span class="n">height</span> <span class="o">=</span> <span class="s">&#34;100%&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><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></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">output</span><span class="o">$</span><span class="n">results</span> <span class="o">&lt;-</span> <span class="nf">renderUI</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">quarto_task</span><span class="o">$</span><span class="nf">status</span><span class="p">()</span> <span class="o">==</span> <span class="s">&#34;running&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nf">div</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-center d-flex flex-column justify-content-center align-items-center&#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="s">&#34;height: 100%;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;file-slides&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">size</span> <span class="o">=</span> <span class="s">&#34;6em&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-primary bounce&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">br</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">p</span><span class="p">(</span><span class="s">&#34;Processing your Quarto presentation...&#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 class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">chat_task</span><span class="o">$</span><span class="nf">status</span><span class="p">()</span> <span class="o">==</span> <span class="s">&#34;running&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nf">div</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-center d-flex flex-column justify-content-center align-items-center&#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="s">&#34;height: 100%;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;robot&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">size</span> <span class="o">=</span> <span class="s">&#34;6em&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-primary bounce&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">br</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">p</span><span class="p">(</span><span class="s">&#34;The LLM is doing its magic...&#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 class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">chat_task</span><span class="o">$</span><span class="nf">status</span><span class="p">()</span> <span class="o">==</span> <span class="s">&#34;success&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nf">tagList</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Final results 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 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>
</div>
<p>Using extended task, we can easily monitor the status with <code>quarto_task$status()</code> and <code>chat_task$status()</code>. The status can be <code>&quot;initial&quot;</code>, <code>&quot;running&quot;</code>, <code>&quot;success&quot;</code>, or <code>&quot;error&quot;</code>. So whenever our Quarto task is running, we can display a bouncing presentation easel (or whatever you like). And once that task is finished and we start with our chat task, we can show a bouncing robot. The HTML for those bouncing icons is pretty straightforward: a simple div that puts the icon in the middle. For the bounce effect we need some custom CSS that we can add with the <code>.bounce</code> class. It looks like this:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><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-css" data-lang="css"><span class="line"><span class="cl"><span class="p">.</span><span class="nc">bounce</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">animation</span><span class="p">:</span> <span class="n">bounce</span> <span class="mi">2</span><span class="kt">s</span> <span class="kc">infinite</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="p">@</span><span class="k">keyframes</span> <span class="nt">bounce</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">0</span><span class="o">%,</span> <span class="nt">100</span><span class="o">%</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">transform</span><span class="p">:</span> <span class="nb">translateY</span><span class="p">(</span><span class="mi">0</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="nt">50</span><span class="o">%</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">transform</span><span class="p">:</span> <span class="nb">translateY</span><span class="p">(</span><span class="mi">-20</span><span class="kt">px</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>We&rsquo;ll look at how to add this custom CSS to your Shiny app a little later.</p>
<blockquote>
<p><strong>Built-in busy indicators</strong></p>
<p>By default, a page-level pulsing banner and a spinner will be shown on recalculating outputs like plots and tables. This means that whenever the app is busy with calculations (like getting the results from an LLM) there will be some visual feedback for the user. You can change the appearance and options of these busy indicators with <a href="https://shiny.posit.co/py/api/core/ui.busy_indicators.options.html#shiny.ui.busy_indicators.options" target="_blank" rel="noopener">ui.busy_indicators.options</a>
 (Python) / <a href="https://shiny.posit.co/r/reference/shiny/latest/busyindicatoroptions.html" target="_blank" rel="noopener">busyIndicatorOptions</a>
 (R). In Python, a spinner shows by default on <code>output_plot</code> and <code>output_data_frame</code>. In R, on <code>plotOutput</code> and <code>tableOutput</code>. Spinners won&rsquo;t be shown on value boxes and HTML widgets, that&rsquo;s why a spinner overlay like in our example works well.</p>
</blockquote>
<p>What a lovely result 🤖:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-loading-ux.gif"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<h2 id="value-boxes-with-tooltips">Value boxes with tooltips
</h2>
<p>With value boxes you can display key numbers. The idea is simple: show a number or short text, add an image, icon, or sparkline, and make sure the value updates whenever the underlying data changes.</p>
<p>Value boxes can be enhanced with tooltips. Tooltips are one of those small details that make a big difference. They&rsquo;re perfect for adding extra information without cluttering up your interface. Think of them as extra context that appears when someone hovers or taps. You can use them to explain numbers or tricky terms, add short instructions, highlight what a button actually does, or even drop in a quick example. In our case, we could add tooltips to the value boxes to tell our users how the numbers were calculated.</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">
<p>UI:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># SVG icon copied from https://icons.getbootstrap.com/icons/file-slides-fill/</span>
</span></span><span class="line"><span class="cl"><span class="n">file_slides</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; fill=&#34;currentColor&#34; class=&#34;bi bi-file-slides-fill&#34; viewBox=&#34;0 0 16 16&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M7 7.78V5.22c0-.096.106-.156.19-.106l2.13 1.279a.125.125 0 0 1 0 .214l-2.13 1.28A.125.125 0 0 1 7 7.778z&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M5 4h6a.5.5 0 0 1 .496.438l.5 4A.5.5 0 0 1 11.5 9h-3v2.016c.863.055 1.5.251 1.5.484 0 .276-.895.5-2 .5s-2-.224-2-.5c0-.233.637-.429 1.5-.484V9h-3a.5.5 0 0 1-.496-.562l.5-4A.5.5 0 0 1 5 4&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">&lt;/svg&gt;&#34;&#34;&#34;</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">tooltip</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">value_box</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Showtime&#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</span><span class="p">(</span><span class="s2">&#34;showtime&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">showcase</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">file_slides</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">theme</span><span class="o">=</span><span class="s2">&#34;primary&#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;Slides are being counted based on the provided Quarto presentation, then an educated guess is made about the time it will take to present them.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Server:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><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">showtime</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">res</span> <span class="o">=</span> <span class="n">analysis_result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">req</span><span class="p">(</span><span class="n">res</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</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;</span><span class="si">{</span><span class="n">res</span><span class="p">[</span><span class="s1">&#39;meta&#39;</span><span class="p">][</span><span class="s1">&#39;estimated_duration_minutes&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2"> minutes&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>In this case, the value contains some text (the length of the presentation in minutes) and a showcase (an icon), but use your creativity to build anything you want! You can find some fun examples <a href="https://shiny.posit.co/py/components/outputs/value-box/" target="_blank" rel="noopener">here</a>
.</p>
<p>You can add a <a href="https://shiny.posit.co/py/components/display-messages/tooltips/" target="_blank" rel="noopener">tooltip</a>
 to any UI element with <code>ui.tooltip()</code>. In this case, the tooltip is applied to the complete value box.</p>
<p>The result:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-tooltips-py.gif"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
<div id="tabset-7-2">
<p>UI:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></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">value_box</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">title</span> <span class="o">=</span> <span class="nf">tooltip</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">span</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;Showtime &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;question-circle-fill&#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="s">&#34;Slides are being counted based on the provided Quarto presentation, then an educated guess is made about the time it will take to present them.&#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">value</span> <span class="o">=</span> <span class="nf">textOutput</span><span class="p">(</span><span class="s">&#34;showtime&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">showcase</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;file-slides&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">theme</span> <span class="o">=</span> <span class="s">&#34;primary&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Server:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">output</span><span class="o">$</span><span class="n">showtime</span> <span class="o">&lt;-</span> <span class="nf">renderText</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nf">req</span><span class="p">(</span><span class="nf">analysis_result</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">  <span class="nf">paste0</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">analysis_result</span><span class="p">()</span><span class="o">$</span><span class="n">meta</span><span class="o">$</span><span class="n">estimated_duration_minutes</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34; minutes&#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><p>Looking for more fun examples? Take a look <a href="https://shiny.posit.co/r/components/outputs/value-box/" target="_blank" rel="noopener">here</a>
.</p>
<p>You can add a tooltip to any UI element with <code>tooltip()</code>. In this case, the tooltip is applied to a little info icon (generated with <code>span</code> and <code>bs_icon</code>). Alternatively, you could also apply it to the complete value box.</p>
<p>The result:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-tooltips-r.gif"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
</div>
<h2 id="interactive-plots">Interactive plots
</h2>
<p>Is it even a data-powered app if there isn&rsquo;t a good plot?!</p>
<p>There are plenty of options when it comes to visualisation libraries, and many of them support interactivity as well. With Shiny you can easily add interactive plots, and you can even link events (clicking, brushing, hovering) to other parts of your app. For example, adding click and brushing would let users dig deeper into the results. Clicking on a bar in our score graph could filter the table down to just that category&rsquo;s feedback, while brushing across multiple bars would make it possible to look at several categories side by side. And lets not forget about hovering: users expect to see more information when they hover over a score. You can control click, brush, and hover events with <a href="https://shiny.posit.co/py/api/core/ui.output_plot.html" target="_blank" rel="noopener"><code>output_plot</code></a>
 (Python) / <a href="https://shiny.posit.co/r/reference/shiny/latest/plotoutput.html" target="_blank" rel="noopener"><code>plotOutput</code></a>
 (R). This makes it easy to send values back to the server. To give an example: you can show a modal when someone clicks on a bar.</p>
<p>To keep the demo light, we&rsquo;re not going to focus too much on these events. But there&rsquo;s one thing that we can add very easily and is supported by most interactive visualisation libraries: tooltips on hover! When you hover over a bar (or a line, or a point, you get it), you&rsquo;ll see a quick popup with more information. In our case: the score and the justification. Even this small touch already makes the chart feel more alive, and it&rsquo;s easy to imagine how combining clicks, brushing, and tooltips could make DeckCheck even cooler.</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">
<blockquote>
<p><strong>Tip</strong></p>
<p>Want to create plots based on the grammar of graphics? Take a look at <a href="https://plotnine.org" target="_blank" rel="noopener">plotnine</a>
! Unfortunately not interactive (yet), but you can make really pretty figures that would fit nicely into an app.</p>
</blockquote>
<p>In Python, you have plenty of options for interactive plots, like <a href="https://shiny.posit.co/py/components/outputs/plot-plotly" target="_blank" rel="noopener">Plotly</a>
 or <a href="https://altair-viz.github.io" target="_blank" rel="noopener">Altair</a>
. The choice is yours! Our weapon of choice for DeckCheck: Plotly.</p>
<p>To make a Plotly plot interactive, you need <a href="https://github.com/posit-dev/py-shinywidgets" target="_blank" rel="noopener"><code>shinywidgets</code></a>
. More specifically: <code>output_widget</code> on the UI side, and <code>render_widget</code> on the server side. It connects Shiny with ipywidgets, letting your sliders, dropdowns, and buttons actually control the plot.</p>
<p>UI:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><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">strong</span><span class="p">(</span><span class="s2">&#34;Scores per category&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">    <span class="n">output_widget</span><span class="p">(</span><span class="s2">&#34;scores&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">height</span><span class="o">=</span><span class="s2">&#34;600px&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Server:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><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></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_widget</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">scores</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">res</span> <span class="o">=</span> <span class="n">analysis_result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">req</span><span class="p">(</span><span class="n">res</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">evals</span> <span class="o">=</span> <span class="n">res</span><span class="p">[</span><span class="s2">&#34;evals&#34;</span><span class="p">]</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">evals</span> <span class="o">=</span> <span class="n">evals</span><span class="o">.</span><span class="n">sort_values</span><span class="p">(</span><span class="s2">&#34;score&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;category&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">Categorical</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;category&#34;</span><span class="p">],</span> <span class="n">categories</span><span class="o">=</span><span class="n">evals</span><span class="p">[</span><span class="s2">&#34;category&#34;</span><span class="p">],</span> <span class="n">ordered</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"># apply to the justification column</span>
</span></span><span class="line"><span class="cl">    <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;justification_wrapped&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;justification&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="n">add_line_breaks</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 custom tooltip column</span>
</span></span><span class="line"><span class="cl">    <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;tooltip&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Score: &#34;</span>
</span></span><span class="line"><span class="cl">        <span class="o">+</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;score&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">str</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">+</span> <span class="s2">&#34;&lt;br&gt;After improvements: &#34;</span>
</span></span><span class="line"><span class="cl">        <span class="o">+</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;score_after_improvements&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">str</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">+</span> <span class="s2">&#34;&lt;br&gt;Justification: &#34;</span>
</span></span><span class="line"><span class="cl">        <span class="o">+</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;justification_wrapped&#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">plot</span> <span class="o">=</span> <span class="n">px</span><span class="o">.</span><span class="n">bar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">evals</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">x</span><span class="o">=</span><span class="s2">&#34;score&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">y</span><span class="o">=</span><span class="s2">&#34;category&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">orientation</span><span class="o">=</span><span class="s2">&#34;h&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">labels</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;category&#34;</span><span class="p">:</span> <span class="s2">&#34;Category&#34;</span><span class="p">,</span> <span class="s2">&#34;score&#34;</span><span class="p">:</span> <span class="s2">&#34;Score&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="n">hover_data</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;tooltip&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">},</span>  <span class="c1"># include the tooltip column</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"># Set hovertemplate to use our custom tooltip</span>
</span></span><span class="line"><span class="cl">    <span class="n">plot</span><span class="o">.</span><span class="n">update_traces</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">hovertemplate</span><span class="o">=</span><span class="s2">&#34;%</span><span class="si">{customdata[0]}</span><span class="s2">&lt;extra&gt;&lt;/extra&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">customdata</span><span class="o">=</span><span class="n">evals</span><span class="p">[[</span><span class="s2">&#34;tooltip&#34;</span><span class="p">]]</span><span class="o">.</span><span class="n">values</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">plot</span><span class="o">.</span><span class="n">update_traces</span><span class="p">(</span><span class="n">marker_color</span><span class="o">=</span><span class="s2">&#34;#18bc9c&#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">plot</span><span class="o">.</span><span class="n">update_layout</span><span class="p">(</span><span class="n">template</span><span class="o">=</span><span class="s2">&#34;simple_white&#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">plot</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>To construct the tooltip we create a little helper to make sure there are line breaks. Otherwise the tooltip runs of the screen!</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-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"># Tooltip helper</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">add_line_breaks</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">width</span><span class="o">=</span><span class="mi">50</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="nb">isinstance</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">text</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">words</span> <span class="o">=</span> <span class="n">text</span><span class="o">.</span><span class="n">split</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">lines</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="n">current_line</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">words</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># +1 accounts for the space if current_line isn&#39;t empty</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">current_line</span><span class="p">)</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="mi">1</span> <span class="k">if</span> <span class="n">current_line</span> <span class="k">else</span> <span class="mi">0</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="n">width</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">current_line</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34; &#34;</span> <span class="k">if</span> <span class="n">current_line</span> <span class="k">else</span> <span class="s2">&#34;&#34;</span><span class="p">)</span> <span class="o">+</span> <span class="n">word</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">lines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">current_line</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">current_line</span> <span class="o">=</span> <span class="n">word</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">current_line</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">lines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">current_line</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="s2">&#34;&lt;br&gt;&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">lines</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The result is simple, but effective:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-plot-py.gif"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
<div id="tabset-8-2">
<blockquote>
<p><strong>Cross-widget interactions</strong></p>
<p>Interested in cross-widget interactions like linked brushing and filtering? Take a look at <a href="https://rstudio.github.io/crosstalk/shiny.html" target="_blank" rel="noopener">crosstalk</a>
.</p>
</blockquote>
<p>Where would we be without our beloved <code>ggplot2</code>&hellip; So of course it would be nice if we can could make our <code>ggplot2</code> interactive. The solution: <a href="https://davidgohel.github.io/ggiraph/" target="_blank" rel="noopener"><code>ggiraph</code></a>
! With <code>ggiraph</code> you can simply add tooltips, hover effects, and JavaScript actions to your plots. There&rsquo;s plenty of options to alter the look and feel of the interactive elements, so you can be very creative. If you&rsquo;re looking for more inspiration with <code>ggiraph</code>, check out <a href="https://posit.co/blog/shiny-dashboards-with-ggiraph-and-databases/" target="_blank" rel="noopener">this post</a>
 by Isabella Velásquez.</p>
<p>We&rsquo;ll stick to the basic with DeckCheck by adding <a href="https://davidgohel.github.io/ggiraph/reference/geom_bar_interactive.html" target="_blank" rel="noopener"><code>geom_bar_interactive</code></a>
 and some tooltip options. To make it all work we need to use <code>girafeOutput</code> and <code>renderGirafe</code> (as opposed to <code>plotOutput</code> and <code>renderPlot</code>).</p>
<p>UI:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">card</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">height</span> <span class="o">=</span> <span class="m">600</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nf">card_header</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="nf">strong</span><span class="p">(</span><span class="s">&#34;Scores per category&#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">girafeOutput</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">outputId</span> <span class="o">=</span> <span class="s">&#34;scores&#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><p>Server:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><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></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">output</span><span class="o">$</span><span class="n">scores</span> <span class="o">&lt;-</span> <span class="nf">renderGirafe</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">analysis_result</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">evals</span> <span class="o">&lt;-</span> <span class="nf">analysis_result</span><span class="p">()</span><span class="o">$</span><span class="n">evals</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Order by score</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">&lt;-</span> <span class="n">evals</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">arrange</span><span class="p">(</span><span class="n">score</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">mutate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">category</span> <span class="o">=</span> <span class="nf">factor</span><span class="p">(</span><span class="n">category</span><span class="p">,</span> <span class="n">levels</span> <span class="o">=</span> <span class="n">category</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">tooltip</span> <span class="o">=</span> <span class="nf">paste0</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;Score: &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">score</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;\n&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;After improvements: &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">score_after_improvements</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;\n&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;Justification: &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">justification</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">select</span><span class="p">(</span><span class="n">category</span><span class="p">,</span> <span class="n">score</span><span class="p">,</span> <span class="n">score_after_improvements</span><span class="p">,</span> <span class="n">tooltip</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">p</span> <span class="o">&lt;-</span> <span class="nf">ggplot</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">data</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nf">aes</span><span class="p">(</span><span class="n">x</span> <span class="o">=</span> <span class="n">category</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">score</span><span class="p">,</span> <span class="n">tooltip</span> <span class="o">=</span> <span class="n">tooltip</span><span class="p">,</span> <span class="n">data_id</span> <span class="o">=</span> <span class="n">category</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">      <span class="nf">geom_bar_interactive</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">stat</span> <span class="o">=</span> <span class="s">&#34;identity&#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="s">&#34;#18bc9c&#34;</span> <span class="c1"># Success color of Flatly theme</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">      <span class="nf">labs</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">x</span> <span class="o">=</span> <span class="s">&#34;Category&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">y</span> <span class="o">=</span> <span class="s">&#34;Score&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Flip to make horizontal bar chart</span>
</span></span><span class="line"><span class="cl">      <span class="nf">coord_flip</span><span class="p">()</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">      <span class="nf">theme_minimal</span><span class="p">(</span><span class="n">base_family</span> <span class="o">=</span> <span class="s">&#34;Lato&#34;</span><span class="p">,</span> <span class="n">base_size</span> <span class="o">=</span> <span class="m">14</span><span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">      <span class="nf">theme</span><span class="p">(</span><span class="n">legend.position</span> <span class="o">=</span> <span class="s">&#34;none&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">girafe</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">ggobj</span> <span class="o">=</span> <span class="n">p</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">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="nf">opts_selection</span><span class="p">(</span><span class="n">type</span> <span class="o">=</span> <span class="s">&#34;none&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">opts_sizing</span><span class="p">(</span><span class="n">rescale</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">opts_tooltip</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">css</span> <span class="o">=</span> <span class="s">&#34;background-color: #f0f0f0; color: #333; padding: 5px; border-radius: 5px; width: 200px;&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">opts_hover</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">css</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="nf">opts_hover_inv</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">css</span> <span class="o">=</span> <span class="s">&#34;opacity: 0.5;&#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><span class="line"><span class="cl">  <span class="p">})</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>And that&rsquo;s how you add interactivity to your <code>ggplot2</code> fast:</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-plot-r.gif"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
</div>
</div>
<h2 id="tables">Tables
</h2>
<p>Another element that you&rsquo;ll see in web apps: tables. The good news is: displaying your data in Shiny is super easy. Got a pandas/polars/narwhals DataFrame in Python? Or a data.frame/tibble/data.table in R? You can drop it straight into <a href="https://shiny.posit.co/py/api/core/render.data_frame.html" target="_blank" rel="noopener"><code>render.data_frame</code></a>
 / <a href="https://shiny.posit.co/py/api/core/ui.output_data_frame.html" target="_blank" rel="noopener"><code>output_data_frame</code></a>
 (Python) or <a href="https://shiny.posit.co/r/reference/shiny/latest/rendertable.html" target="_blank" rel="noopener"><code>renderTable</code></a>
 / <a href="https://shiny.posit.co/r/reference/shiny/latest/rendertable.html" target="_blank" rel="noopener"><code>tableOutput</code></a>
 (R) (and many other similar functions for some variety).</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">
<blockquote>
<p><strong>Other ways to render your table</strong></p>
<p>You can also take a look at <a href="https://shiny.posit.co/py/api/core/render.DataGrid.html" target="_blank" rel="noopener">render.DataGrid</a>
 (spreadsheet-like view), <a href="https://shiny.posit.co/py/api/core/render.table.html" target="_blank" rel="noopener">render.table</a>
 (basic HTML table, not as nice as <a href="https://shiny.posit.co/py/api/core/render.data_frame.html" target="_blank" rel="noopener">render.data_frame</a>
) and <a href="https://shiny.posit.co/py/api/core/render.DataTable.html#shiny.render.DataTable" target="_blank" rel="noopener">render.DataTable</a>
 (more tabular view of data).</p>
</blockquote>
<p>UI:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">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></span><span class="line"><span class="cl">        <span class="n">ui</span><span class="o">.</span><span class="n">strong</span><span class="p">(</span><span class="s2">&#34;Suggested improvements per category&#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">output_data_frame</span><span class="p">(</span><span class="s2">&#34;suggested_improvements&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">height</span><span class="o">=</span><span class="s2">&#34;600px&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Server:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@render.data_frame</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">suggested_improvements</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">res</span> <span class="o">=</span> <span class="n">analysis_result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">req</span><span class="p">(</span><span class="n">res</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">evals</span> <span class="o">=</span> <span class="n">res</span><span class="p">[</span><span class="s2">&#34;evals&#34;</span><span class="p">]</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">evals</span><span class="p">[</span><span class="s2">&#34;Gain&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;score_after_improvements&#34;</span><span class="p">]</span> <span class="o">-</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;score&#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">result_table</span> <span class="o">=</span> <span class="n">evals</span><span class="o">.</span><span class="n">assign</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">Category</span><span class="o">=</span><span class="n">evals</span><span class="p">[</span><span class="s2">&#34;category&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="o">**</span><span class="p">{</span><span class="s2">&#34;Current score&#34;</span><span class="p">:</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;score&#34;</span><span class="p">]},</span>
</span></span><span class="line"><span class="cl">        <span class="n">Improvements</span><span class="o">=</span><span class="n">evals</span><span class="p">[</span><span class="s2">&#34;improvements&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="o">**</span><span class="p">{</span><span class="s2">&#34;Score After Improvements&#34;</span><span class="p">:</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;score_after_improvements&#34;</span><span class="p">]},</span>
</span></span><span class="line"><span class="cl">        <span class="n">Gain</span><span class="o">=</span><span class="n">evals</span><span class="p">[</span><span class="s2">&#34;Gain&#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="s2">&#34;Category&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Current score&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Improvements&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Score After Improvements&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Gain&#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 class="o">.</span><span class="n">sort_values</span><span class="p">(</span><span class="s2">&#34;Gain&#34;</span><span class="p">,</span> <span class="n">ascending</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">result_table</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-9-2">
<blockquote>
<p><strong>More fun table libraries</strong></p>
<p>Want to have some fun with JavaScript based tables? Check out <a href="https://glin.github.io/reactable/" target="_blank" rel="noopener">reactable</a>
, which is based on the <a href="https://github.com/tanstack/table/tree/v7" target="_blank" rel="noopener">React Table</a>
 library. Or, go for <a href="https://gt.rstudio.com" target="_blank" rel="noopener">gt</a>
! You can get some inspo from the <a href="https://shiny.posit.co/r/components/" target="_blank" rel="noopener">Shiny components gallery</a>
.</p>
</blockquote>
<p>UI:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">card</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">height</span> <span class="o">=</span> <span class="m">600</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">strong</span><span class="p">(</span><span class="s">&#34;Suggested improvements per category&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">  <span class="nf">tableOutput</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">outputId</span> <span class="o">=</span> <span class="s">&#34;suggested_improvements&#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><p>Server:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">output</span><span class="o">$</span><span class="n">suggested_improvements</span> <span class="o">&lt;-</span> <span class="nf">renderTable</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">analysis_result</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">evals</span> <span class="o">&lt;-</span> <span class="nf">analysis_result</span><span class="p">()</span><span class="o">$</span><span class="n">evals</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">evals</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">arrange</span><span class="p">(</span><span class="n">score</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">mutate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">Gain</span> <span class="o">=</span> <span class="n">score_after_improvements</span> <span class="o">-</span> <span class="n">score</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">select</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">Category</span> <span class="o">=</span> <span class="n">category</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">`Current score`</span> <span class="o">=</span> <span class="n">score</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">Improvements</span> <span class="o">=</span> <span class="n">improvements</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">`Score After Improvements`</span> <span class="o">=</span> <span class="n">score_after_improvements</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">Gain</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">arrange</span><span class="p">(</span><span class="nf">desc</span><span class="p">(</span><span class="n">Gain</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>
<h2 id="error-catching">Error catching
</h2>
<p>An error. The thing we don&rsquo;t want to see in our app. Errors reduce user experience, big time. A frozen app or red error messages don&rsquo;t make your users happy. And for you, as a developer, you&rsquo;re not too happy about them either. Unfortunately, it&rsquo;s hard to completely avoid getting errors: you&rsquo;ll always see that there&rsquo;s a specific scenario that you haven&rsquo;t thought about. But that doesn&rsquo;t mean you have to just let it happen: you can catch errors so your users are not confronted with a frozen app or messages they can&rsquo;t understand. Instead, you can confront your users with a friendly (and hopefully useful) message.</p>
<p>One thing worth knowing: in Shiny, if an error happens inside a reactive expression or an observer, the default behaviour is for the app to crash. That&rsquo;s because Shiny has no way of knowing whether the error is fatal or not, and it doesn&rsquo;t really have a natural place to show that error to the user. With LLM apps, this becomes a bigger deal: errors happen often, they&rsquo;re not always fatal, and it&rsquo;s really frustrating for your users if the whole app crashes and they lose their conversation history. Models can return unexpected output, an API call might time out, the API might be overloaded by your requests (been there, done that), or the user could upload something your app doesn&rsquo;t know how to handle. That&rsquo;s why you want to deal with errors &ldquo;gracefully&rdquo;. And luckily, you don&rsquo;t have to reinvent the wheel here. For example, the <code>Chat</code> component automatically catches errors that happen while streaming and shows the user a short explanation instead of breaking the whole app.</p>
<p>However, in a bespoke app like DeckCheck, where we&rsquo;re streaming inside our own reactive expression or observer, you&rsquo;ll want to think about setting up your own error handling. The idea is simple: don&rsquo;t crash the app, and let the user know what went wrong in a friendly way. In our case, we split up our error messages: we can display one when something goes wrong with processing the Quarto file, and we can display one when our chat didn&rsquo;t go as planned. These two error-catching &ldquo;wrappers&rdquo; serve as some inspiration for your next friendly error 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">
<p>To demonstrate what an error-catching &ldquo;wrapper&rdquo; could look like, let&rsquo;s take a look at error handling for the chat task:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</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">@reactive.effect</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">run_chat</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># require quarto_task result to be available</span>
</span></span><span class="line"><span class="cl">    <span class="n">req</span><span class="p">(</span><span class="n">quarto_task</span><span class="o">.</span><span class="n">result</span><span class="p">()</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Error for testing</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># raise ValueError(&#34;Test error&#34;)</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></span><span class="line"><span class="cl">        <span class="c1"># Trigger the chat task with the provided inputs</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat_task</span><span class="o">.</span><span class="n">invoke</span><span class="p">(</span><span class="n">system_prompt</span><span class="p">,</span> <span class="n">markdown_content</span><span class="p">,</span> <span class="n">DeckAnalysis</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">warnings</span><span class="o">.</span><span class="n">warn</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Error when trying to invoke chat_task: </span><span class="si">{</span><span class="n">e</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="c1"># Print stack trace to the console</span>
</span></span><span class="line"><span class="cl">        <span class="n">traceback</span><span class="o">.</span><span class="n">print_exc</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Return value that triggers modal in UI</span>
</span></span><span class="line"><span class="cl">        <span class="n">m</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">modal</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">div</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="c1"># Sad bootstrap icon</span>
</span></span><span class="line"><span class="cl">                <span class="n">ui</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">sad_icon</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">br</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="s2">&#34;The not so Shiny Side of LLMs. Unfortunately, chatting didn&#39;t work out. Do you have enough credits left?&#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"># add class to center the content</span>
</span></span><span class="line"><span class="cl">                <span class="n">class_</span><span class="o">=</span><span class="s2">&#34;text-center&#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">title</span><span class="o">=</span><span class="s2">&#34;Oops, something went wrong!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">easy_close</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">footer</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">modal_button</span><span class="p">(</span><span class="s2">&#34;Close&#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">modal_show</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The same error handling gets applied to the Quarto task.</p>
<p>The Quarto task and the chat task chain together various tasks: copying an uploaded Quarto file, rendering it to Markdown and HTML, building a system prompt, and then invoking a conversation with an LLM. Any of these steps could fail (a bad upload, Quarto not rendering, the model returning something unexpected), but the <code>try/except</code> makes sure the app doesn&rsquo;t just crash or leave the user hanging. Instead, if something goes wrong, it logs the error for debugging and then shows the user a clean modal with a simple message.</p>
<blockquote>
<p><strong>Error notifications</strong></p>
<p>There&rsquo;s also a nice helper for error notifications: <a href="https://shiny.posit.co/py/docs/genai-stream.html#error-handling" target="_blank" rel="noopener"><code>shiny.types.NotifyException</code></a>
. This is what <code>ui.Chat</code> from <a href="https://posit-dev.github.io/shinychat/py/" target="_blank" rel="noopener">shinychat</a>
 uses for its error notifications. You can use it like this:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</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">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">msg</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;An error occurred: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">raise</span> <span class="n">NotifyException</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span> <span class="kn">from</span> <span class="nn">e</span>
</span></span></code></pre></td></tr></table>
</div>
</div></blockquote>
</div>
<div id="tabset-10-2">
<p>To demonstrate what an error-catching &ldquo;wrapper&rdquo; could look like, let&rsquo;s take a look at error handling for the chat task:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><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></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">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">quarto_task</span><span class="o">$</span><span class="nf">result</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="nf">tryCatch</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"># Error for testing</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># stop(&#34;This is a test error.&#34;)</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></span><span class="line"><span class="cl">        <span class="c1"># Trigger the chat task with the provided inputs</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat_task</span><span class="o">$</span><span class="nf">invoke</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">system_prompt</span> <span class="o">=</span> <span class="n">system_prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">markdown_content</span> <span class="o">=</span> <span class="n">markdown_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">type_deck_analysis</span> <span class="o">=</span> <span class="n">type_deck_analysis</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">error</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">rlang</span><span class="o">::</span><span class="nf">warn</span><span class="p">(</span><span class="nf">paste</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;Error when trying to invoke chat_task:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">e</span><span class="o">$</span><span class="n">message</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"># Print stack trace</span>
</span></span><span class="line"><span class="cl">        <span class="nf">print</span><span class="p">(</span><span class="n">rlang</span><span class="o">::</span><span class="nf">trace_back</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Show modal to the user</span>
</span></span><span class="line"><span class="cl">        <span class="nf">showModal</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="nf">modalDialog</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;Oops! Something went wrong&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nf">div</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">              <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-center&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;emoji-frown-fill&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">size</span> <span class="o">=</span> <span class="s">&#34;2em&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-warning&#34;</span>
</span></span><span class="line"><span class="cl">              <span class="p">),</span>
</span></span><span class="line"><span class="cl">              <span class="nf">br</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">              <span class="nf">p</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;The not so Shiny Side of LLMs. Unfortunately, chatting didn&#39;t work out. Do you have enough credits left?&#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="n">easyClose</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">footer</span> <span class="o">=</span> <span class="nf">modalButton</span><span class="p">(</span><span class="s">&#34;Close&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The same error handling gets applied to the Quarto task.</p>
<p>The Quarto task and the chat task chain together various tasks: copying an uploaded Quarto file, rendering it to Markdown and HTML, building a system prompt, and then invoking a conversation with an LLM. Any of these steps could fail (a bad upload, Quarto not rendering, the model returning something unexpected), but the <code>tryCatch</code> wrapper makes sure the app doesn&rsquo;t just crash or leave the user hanging. Instead, if something goes wrong, it logs the error for debugging and then shows the user a clean modal with a simple message.</p>
</div>
</div>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-error-msg.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>Note that the error messages just say &ldquo;something went wrong&rdquo; and a little direction as to what to do next. This is the cleanest and most &ldquo;sanitised&rdquo; way of handling errors. If you were to pass the real error message straight through to the modal, you&rsquo;d risk showing users technical details they&rsquo;re not supposed to see.</p>
<blockquote>
<p><strong>Oops, an error</strong></p>
<p>The <code>result()</code> method of <code>extended_task</code> (Python) / <code>ExtendedTask</code> (R) can return <code>&quot;error&quot;</code> too. If there&rsquo;s an error in the task, the error will be re-thrown if you call the <code>result()</code> method.</p>
</blockquote>
<h2 id="custom-csssass">Custom CSS/Sass
</h2>
<p>So far, we&rsquo;ve been happily using a preset theme in our app (for DeckCheck: the Bootswatch Flatly theme). Nothing wrong with that, but maybe your organisation has a &ldquo;house style&rdquo; (company colours, logos, and fonts that need to be everywhere). The good news: Shiny doesn&rsquo;t get in your way here. Under the hood, a Shiny app is still just HTML, CSS, and JavaScript. And just like any other web app, you can tweak the look and feel with CSS/<a href="https://rstudio.github.io/sass/" target="_blank" rel="noopener">Sass</a>
 until it matches whatever look you&rsquo;re going for.</p>
<p>In Shiny, adding that CSS/Sass is easy. There are actually a few options:</p>
<ul>
<li><strong>Inline CSS (global)</strong>: you can attach CSS directly using a <code>&lt;style&gt;</code> tag (<code>tags.style()</code> in Python or <code>tags$style()</code> in R) at the top of your UI. This is handy when you just need a couple of tweaks that apply globally. You can use it for CSS classes that you use throughout your app, or for things like global fonts. For simplicity, we&rsquo;re going for this option in DeckCheck. Note that if you want to add add styles in the <code>&lt;head&gt;</code> specifically, you need <code>ui.head_content()</code> (Python) / <code>tags$head()</code> (R).</li>
<li><strong>Inline CSS (individual components)</strong>: when you only need to tweak one particular element (e.g. center a particular element), you can append styles directly to that tag. In Python, you can call <a href="https://shiny.posit.co/py/api/core/TagTypes.html#htmltools.Tag.add_style" target="_blank" rel="noopener"><code>.add_style()</code></a>
 on the tag itself. In R, you&rsquo;d use <a href="https://shiny.posit.co/r/reference/shiny/latest/tagappendattributes.html" target="_blank" rel="noopener"><code>tagAppendAttributes()</code></a>
. Note that all HTML tags and a lot of UI components also support <code>style</code> and <code>class</code> arguments, so make sure to check out the documentation if you&rsquo;re looking to make adjustments.</li>
<li><strong>External stylesheets</strong>: if your styles grow beyond a few lines, you&rsquo;ll want to put it in a (S)CSS file inside the <code>www/</code> folder. Shiny will automatically serve that file, and you can include it with <code>tags.link()</code> / <code>include_css()</code> in Python or <code>tags$link()</code> / <code>includeCSS()</code> in R. This keeps your UI code clean and makes your styles easier to manage.</li>
</ul>
<p>In DeckCheck, we add the styles of the <code>.bounce</code> class that we use for our <a href="#loading-experience">bouncing robot</a>
.</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">
<blockquote>
<p><strong>Custom CSS in Shiny</strong></p>
<p>If you want to learn more about styling your app, check out <a href="https://shiny.posit.co/py/docs/ui-customize.html#custom-css" target="_blank" rel="noopener">Custom CSS in Shiny for Python</a>
.</p>
</blockquote>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span></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">app_ui</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">page_fillable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1">## General theme and styles</span>
</span></span><span class="line"><span class="cl">    <span class="c1">## 1. Custom CSS</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">tags</span><span class="o">.</span><span class="n">style</span><span class="p">(</span><span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">        .bounce {
</span></span></span><span class="line"><span class="cl"><span class="s2">            animation: bounce 2s infinite;
</span></span></span><span class="line"><span class="cl"><span class="s2">        }
</span></span></span><span class="line"><span class="cl"><span class="s2">        @keyframes bounce {
</span></span></span><span class="line"><span class="cl"><span class="s2">            0%, 100% {
</span></span></span><span class="line"><span class="cl"><span class="s2">                transform: translateY(0);
</span></span></span><span class="line"><span class="cl"><span class="s2">            }
</span></span></span><span class="line"><span class="cl"><span class="s2">            50% {
</span></span></span><span class="line"><span class="cl"><span class="s2">                transform: translateY(-20px);
</span></span></span><span class="line"><span class="cl"><span class="s2">            }
</span></span></span><span class="line"><span class="cl"><span class="s2">        }
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#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">layout_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="c1">## Sidebar 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 class="c1">## Main 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 class="c1"># Bootswatch theme</span>
</span></span><span class="line"><span class="cl">    <span class="n">theme</span><span class="o">=</span><span class="n">shinyswatch</span><span class="o">.</span><span class="n">theme</span><span class="o">.</span><span class="n">flatly</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Looking for more &ldquo;high-level&rdquo; styling? Check out <a href="https://posit-dev.github.io/brand-yml/" target="_blank" rel="noopener">brand.yml</a>
 for branding with a simple YAML file.</p>
</div>
<div id="tabset-11-2">
<blockquote>
<p><strong>Custom CSS in Shiny</strong></p>
<p>If you want to learn more about styling your app, check out <a href="https://shiny.posit.co/r/articles/build/css" target="_blank" rel="noopener">Custom CSS in Shiny for R</a>
.</p>
</blockquote>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span></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">ui</span> <span class="o">&lt;-</span> <span class="nf">page_fillable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="c1">## Options</span>
</span></span><span class="line"><span class="cl">  <span class="c1">## 1. Bootswatch theme</span>
</span></span><span class="line"><span class="cl">  <span class="n">theme</span> <span class="o">=</span> <span class="nf">bs_theme</span><span class="p">(</span><span class="n">bootswatch</span> <span class="o">=</span> <span class="s">&#34;flatly&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="c1">## 2. Custom CSS</span>
</span></span><span class="line"><span class="cl">  <span class="n">tags</span><span class="o">$</span><span class="nf">style</span><span class="p">(</span><span class="nf">HTML</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">.bounce</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">animation</span><span class="o">:</span> <span class="n">bounce</span> <span class="m">2</span><span class="n">s</span> <span class="n">infinite</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">keyframes</span> <span class="n">bounce</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="m">0</span><span class="o">%, 100%</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">transform</span><span class="o">:</span> <span class="nf">translateY</span><span class="p">(</span><span class="m">0</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="m">50</span>% <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">transform</span><span class="o">:</span> <span class="nf">translateY</span><span class="p">(</span><span class="m">-20</span><span class="n">px</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="s">&#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">## Layout</span>
</span></span><span class="line"><span class="cl">  <span class="nf">layout_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1">## Sidebar content</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="c1">## Sidebar 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 class="c1">## Main 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 class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Note that these are fairly &ldquo;low-level&rdquo; techniques for achieving custom styling. To learn more about higher-level options you can take a look at <a href="https://rstudio.github.io/bslib/articles/theming/index.html" target="_blank" rel="noopener">theming</a>
 or <a href="https://rstudio.github.io/bslib/articles/brand-yml/index.html" target="_blank" rel="noopener">brand.yml</a>
.</p>
</div>
</div>
<h1 id="the-end-result">The end result
</h1>
<p>If we combine everything we talked about, we end up with a polished DeckCheck app! A sidebar layout with users inputs, a nice loading experience, non-blocking async operations, fancy looking graphs and tables, tooltips, you name it.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-deckcheck-full.gif"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</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">
<blockquote>
<p><strong>Get this code from GitHub</strong></p>
<p>You can grab the code directly from <a href="https://github.com/hypebright/the-shiny-side-of-llms/blob/d1094d2774f9d0c213c7ddf6e17f94da706b1b76/Py/deckcheck/app.py" target="_blank" rel="noopener">here</a>
.</p>
</blockquote>
<details class="code-fold">
<summary>See full app</summary>
<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><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span><span class="lnt">153
</span><span class="lnt">154
</span><span class="lnt">155
</span><span class="lnt">156
</span><span class="lnt">157
</span><span class="lnt">158
</span><span class="lnt">159
</span><span class="lnt">160
</span><span class="lnt">161
</span><span class="lnt">162
</span><span class="lnt">163
</span><span class="lnt">164
</span><span class="lnt">165
</span><span class="lnt">166
</span><span class="lnt">167
</span><span class="lnt">168
</span><span class="lnt">169
</span><span class="lnt">170
</span><span class="lnt">171
</span><span class="lnt">172
</span><span class="lnt">173
</span><span class="lnt">174
</span><span class="lnt">175
</span><span class="lnt">176
</span><span class="lnt">177
</span><span class="lnt">178
</span><span class="lnt">179
</span><span class="lnt">180
</span><span class="lnt">181
</span><span class="lnt">182
</span><span class="lnt">183
</span><span class="lnt">184
</span><span class="lnt">185
</span><span class="lnt">186
</span><span class="lnt">187
</span><span class="lnt">188
</span><span class="lnt">189
</span><span class="lnt">190
</span><span class="lnt">191
</span><span class="lnt">192
</span><span class="lnt">193
</span><span class="lnt">194
</span><span class="lnt">195
</span><span class="lnt">196
</span><span class="lnt">197
</span><span class="lnt">198
</span><span class="lnt">199
</span><span class="lnt">200
</span><span class="lnt">201
</span><span class="lnt">202
</span><span class="lnt">203
</span><span class="lnt">204
</span><span class="lnt">205
</span><span class="lnt">206
</span><span class="lnt">207
</span><span class="lnt">208
</span><span class="lnt">209
</span><span class="lnt">210
</span><span class="lnt">211
</span><span class="lnt">212
</span><span class="lnt">213
</span><span class="lnt">214
</span><span class="lnt">215
</span><span class="lnt">216
</span><span class="lnt">217
</span><span class="lnt">218
</span><span class="lnt">219
</span><span class="lnt">220
</span><span class="lnt">221
</span><span class="lnt">222
</span><span class="lnt">223
</span><span class="lnt">224
</span><span class="lnt">225
</span><span class="lnt">226
</span><span class="lnt">227
</span><span class="lnt">228
</span><span class="lnt">229
</span><span class="lnt">230
</span><span class="lnt">231
</span><span class="lnt">232
</span><span class="lnt">233
</span><span class="lnt">234
</span><span class="lnt">235
</span><span class="lnt">236
</span><span class="lnt">237
</span><span class="lnt">238
</span><span class="lnt">239
</span><span class="lnt">240
</span><span class="lnt">241
</span><span class="lnt">242
</span><span class="lnt">243
</span><span class="lnt">244
</span><span class="lnt">245
</span><span class="lnt">246
</span><span class="lnt">247
</span><span class="lnt">248
</span><span class="lnt">249
</span><span class="lnt">250
</span><span class="lnt">251
</span><span class="lnt">252
</span><span class="lnt">253
</span><span class="lnt">254
</span><span class="lnt">255
</span><span class="lnt">256
</span><span class="lnt">257
</span><span class="lnt">258
</span><span class="lnt">259
</span><span class="lnt">260
</span><span class="lnt">261
</span><span class="lnt">262
</span><span class="lnt">263
</span><span class="lnt">264
</span><span class="lnt">265
</span><span class="lnt">266
</span><span class="lnt">267
</span><span class="lnt">268
</span><span class="lnt">269
</span><span class="lnt">270
</span><span class="lnt">271
</span><span class="lnt">272
</span><span class="lnt">273
</span><span class="lnt">274
</span><span class="lnt">275
</span><span class="lnt">276
</span><span class="lnt">277
</span><span class="lnt">278
</span><span class="lnt">279
</span><span class="lnt">280
</span><span class="lnt">281
</span><span class="lnt">282
</span><span class="lnt">283
</span><span class="lnt">284
</span><span class="lnt">285
</span><span class="lnt">286
</span><span class="lnt">287
</span><span class="lnt">288
</span><span class="lnt">289
</span><span class="lnt">290
</span><span class="lnt">291
</span><span class="lnt">292
</span><span class="lnt">293
</span><span class="lnt">294
</span><span class="lnt">295
</span><span class="lnt">296
</span><span class="lnt">297
</span><span class="lnt">298
</span><span class="lnt">299
</span><span class="lnt">300
</span><span class="lnt">301
</span><span class="lnt">302
</span><span class="lnt">303
</span><span class="lnt">304
</span><span class="lnt">305
</span><span class="lnt">306
</span><span class="lnt">307
</span><span class="lnt">308
</span><span class="lnt">309
</span><span class="lnt">310
</span><span class="lnt">311
</span><span class="lnt">312
</span><span class="lnt">313
</span><span class="lnt">314
</span><span class="lnt">315
</span><span class="lnt">316
</span><span class="lnt">317
</span><span class="lnt">318
</span><span class="lnt">319
</span><span class="lnt">320
</span><span class="lnt">321
</span><span class="lnt">322
</span><span class="lnt">323
</span><span class="lnt">324
</span><span class="lnt">325
</span><span class="lnt">326
</span><span class="lnt">327
</span><span class="lnt">328
</span><span class="lnt">329
</span><span class="lnt">330
</span><span class="lnt">331
</span><span class="lnt">332
</span><span class="lnt">333
</span><span class="lnt">334
</span><span class="lnt">335
</span><span class="lnt">336
</span><span class="lnt">337
</span><span class="lnt">338
</span><span class="lnt">339
</span><span class="lnt">340
</span><span class="lnt">341
</span><span class="lnt">342
</span><span class="lnt">343
</span><span class="lnt">344
</span><span class="lnt">345
</span><span class="lnt">346
</span><span class="lnt">347
</span><span class="lnt">348
</span><span class="lnt">349
</span><span class="lnt">350
</span><span class="lnt">351
</span><span class="lnt">352
</span><span class="lnt">353
</span><span class="lnt">354
</span><span class="lnt">355
</span><span class="lnt">356
</span><span class="lnt">357
</span><span class="lnt">358
</span><span class="lnt">359
</span><span class="lnt">360
</span><span class="lnt">361
</span><span class="lnt">362
</span><span class="lnt">363
</span><span class="lnt">364
</span><span class="lnt">365
</span><span class="lnt">366
</span><span class="lnt">367
</span><span class="lnt">368
</span><span class="lnt">369
</span><span class="lnt">370
</span><span class="lnt">371
</span><span class="lnt">372
</span><span class="lnt">373
</span><span class="lnt">374
</span><span class="lnt">375
</span><span class="lnt">376
</span><span class="lnt">377
</span><span class="lnt">378
</span><span class="lnt">379
</span><span class="lnt">380
</span><span class="lnt">381
</span><span class="lnt">382
</span><span class="lnt">383
</span><span class="lnt">384
</span><span class="lnt">385
</span><span class="lnt">386
</span><span class="lnt">387
</span><span class="lnt">388
</span><span class="lnt">389
</span><span class="lnt">390
</span><span class="lnt">391
</span><span class="lnt">392
</span><span class="lnt">393
</span><span class="lnt">394
</span><span class="lnt">395
</span><span class="lnt">396
</span><span class="lnt">397
</span><span class="lnt">398
</span><span class="lnt">399
</span><span class="lnt">400
</span><span class="lnt">401
</span><span class="lnt">402
</span><span class="lnt">403
</span><span class="lnt">404
</span><span class="lnt">405
</span><span class="lnt">406
</span><span class="lnt">407
</span><span class="lnt">408
</span><span class="lnt">409
</span><span class="lnt">410
</span><span class="lnt">411
</span><span class="lnt">412
</span><span class="lnt">413
</span><span class="lnt">414
</span><span class="lnt">415
</span><span class="lnt">416
</span><span class="lnt">417
</span><span class="lnt">418
</span><span class="lnt">419
</span><span class="lnt">420
</span><span class="lnt">421
</span><span class="lnt">422
</span><span class="lnt">423
</span><span class="lnt">424
</span><span class="lnt">425
</span><span class="lnt">426
</span><span class="lnt">427
</span><span class="lnt">428
</span><span class="lnt">429
</span><span class="lnt">430
</span><span class="lnt">431
</span><span class="lnt">432
</span><span class="lnt">433
</span><span class="lnt">434
</span><span class="lnt">435
</span><span class="lnt">436
</span><span class="lnt">437
</span><span class="lnt">438
</span><span class="lnt">439
</span><span class="lnt">440
</span><span class="lnt">441
</span><span class="lnt">442
</span><span class="lnt">443
</span><span class="lnt">444
</span><span class="lnt">445
</span><span class="lnt">446
</span><span class="lnt">447
</span><span class="lnt">448
</span><span class="lnt">449
</span><span class="lnt">450
</span><span class="lnt">451
</span><span class="lnt">452
</span><span class="lnt">453
</span><span class="lnt">454
</span><span class="lnt">455
</span><span class="lnt">456
</span><span class="lnt">457
</span><span class="lnt">458
</span><span class="lnt">459
</span><span class="lnt">460
</span><span class="lnt">461
</span><span class="lnt">462
</span><span class="lnt">463
</span><span class="lnt">464
</span><span class="lnt">465
</span><span class="lnt">466
</span><span class="lnt">467
</span><span class="lnt">468
</span><span class="lnt">469
</span><span class="lnt">470
</span><span class="lnt">471
</span><span class="lnt">472
</span><span class="lnt">473
</span><span class="lnt">474
</span><span class="lnt">475
</span><span class="lnt">476
</span><span class="lnt">477
</span><span class="lnt">478
</span><span class="lnt">479
</span><span class="lnt">480
</span><span class="lnt">481
</span><span class="lnt">482
</span><span class="lnt">483
</span><span class="lnt">484
</span><span class="lnt">485
</span><span class="lnt">486
</span><span class="lnt">487
</span><span class="lnt">488
</span><span class="lnt">489
</span><span class="lnt">490
</span><span class="lnt">491
</span><span class="lnt">492
</span><span class="lnt">493
</span><span class="lnt">494
</span><span class="lnt">495
</span><span class="lnt">496
</span><span class="lnt">497
</span><span class="lnt">498
</span><span class="lnt">499
</span><span class="lnt">500
</span><span class="lnt">501
</span><span class="lnt">502
</span><span class="lnt">503
</span><span class="lnt">504
</span><span class="lnt">505
</span><span class="lnt">506
</span><span class="lnt">507
</span><span class="lnt">508
</span><span class="lnt">509
</span><span class="lnt">510
</span><span class="lnt">511
</span><span class="lnt">512
</span><span class="lnt">513
</span><span class="lnt">514
</span><span class="lnt">515
</span><span class="lnt">516
</span><span class="lnt">517
</span><span class="lnt">518
</span><span class="lnt">519
</span><span class="lnt">520
</span><span class="lnt">521
</span><span class="lnt">522
</span><span class="lnt">523
</span><span class="lnt">524
</span><span class="lnt">525
</span><span class="lnt">526
</span><span class="lnt">527
</span><span class="lnt">528
</span><span class="lnt">529
</span><span class="lnt">530
</span><span class="lnt">531
</span><span class="lnt">532
</span><span class="lnt">533
</span><span class="lnt">534
</span><span class="lnt">535
</span><span class="lnt">536
</span><span class="lnt">537
</span><span class="lnt">538
</span><span class="lnt">539
</span><span class="lnt">540
</span><span class="lnt">541
</span><span class="lnt">542
</span><span class="lnt">543
</span><span class="lnt">544
</span><span class="lnt">545
</span><span class="lnt">546
</span><span class="lnt">547
</span><span class="lnt">548
</span><span class="lnt">549
</span><span class="lnt">550
</span><span class="lnt">551
</span><span class="lnt">552
</span><span class="lnt">553
</span><span class="lnt">554
</span><span class="lnt">555
</span><span class="lnt">556
</span><span class="lnt">557
</span><span class="lnt">558
</span><span class="lnt">559
</span><span class="lnt">560
</span><span class="lnt">561
</span><span class="lnt">562
</span><span class="lnt">563
</span><span class="lnt">564
</span><span class="lnt">565
</span><span class="lnt">566
</span><span class="lnt">567
</span><span class="lnt">568
</span><span class="lnt">569
</span><span class="lnt">570
</span><span class="lnt">571
</span><span class="lnt">572
</span><span class="lnt">573
</span><span class="lnt">574
</span><span class="lnt">575
</span><span class="lnt">576
</span><span class="lnt">577
</span><span class="lnt">578
</span><span class="lnt">579
</span><span class="lnt">580
</span><span class="lnt">581
</span><span class="lnt">582
</span><span class="lnt">583
</span><span class="lnt">584
</span><span class="lnt">585
</span><span class="lnt">586
</span><span class="lnt">587
</span><span class="lnt">588
</span><span class="lnt">589
</span><span class="lnt">590
</span><span class="lnt">591
</span><span class="lnt">592
</span><span class="lnt">593
</span><span class="lnt">594
</span><span class="lnt">595
</span><span class="lnt">596
</span><span class="lnt">597
</span><span class="lnt">598
</span><span class="lnt">599
</span><span class="lnt">600
</span><span class="lnt">601
</span><span class="lnt">602
</span><span class="lnt">603
</span><span class="lnt">604
</span><span class="lnt">605
</span><span class="lnt">606
</span><span class="lnt">607
</span><span class="lnt">608
</span><span class="lnt">609
</span><span class="lnt">610
</span><span class="lnt">611
</span><span class="lnt">612
</span><span class="lnt">613
</span><span class="lnt">614
</span><span class="lnt">615
</span><span class="lnt">616
</span><span class="lnt">617
</span><span class="lnt">618
</span><span class="lnt">619
</span><span class="lnt">620
</span><span class="lnt">621
</span><span class="lnt">622
</span><span class="lnt">623
</span><span class="lnt">624
</span><span class="lnt">625
</span><span class="lnt">626
</span><span class="lnt">627
</span><span class="lnt">628
</span><span class="lnt">629
</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">reactive</span><span class="p">,</span> <span class="n">render</span><span class="p">,</span> <span class="n">ui</span><span class="p">,</span> <span class="n">req</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">shinywidgets</span> <span class="kn">import</span> <span class="n">output_widget</span><span class="p">,</span> <span class="n">render_widget</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">shinyswatch</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 class="kn">import</span> <span class="nn">plotly.express</span> <span class="k">as</span> <span class="nn">px</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 class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatAnthropic</span><span class="p">,</span> <span class="n">interpolate_file</span><span class="p">,</span> <span class="n">interpolate</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 class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span><span class="p">,</span> <span class="n">Field</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Annotated</span><span class="p">,</span> <span class="n">Optional</span><span class="p">,</span> <span class="n">Union</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">tempfile</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">shutil</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">asyncio</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">warnings</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">traceback</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 class="c1"># Loads key from the .env file</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Path to the current file</span>
</span></span><span class="line"><span class="cl"><span class="n">APP_DIR</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></span><span class="line"><span class="cl"><span class="c1"># Root directory of the project</span>
</span></span><span class="line"><span class="cl"><span class="n">ROOT_DIR</span> <span class="o">=</span> <span class="n">APP_DIR</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</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"># Data Structure</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ======================</span>
</span></span><span class="line"><span class="cl"><span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mi">10</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">PercentType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">float</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mf">100.0</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">MinutesType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">SlideCount</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</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="k">class</span> <span class="nc">ScoringCategory</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">score</span><span class="p">:</span> <span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Score from 1–10.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">justification</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Brief explanation of the score.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">improvements</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Concise, actionable improvements, mentioning slide numbers if applicable.&#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">score_after_improvements</span><span class="p">:</span> <span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Estimated score after suggested improvements.&#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="k">class</span> <span class="nc">DeckAnalysis</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">presentation_title</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;The presentation title.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">total_slides</span><span class="p">:</span> <span class="n">SlideCount</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_code</span><span class="p">:</span> <span class="n">PercentType</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_images</span><span class="p">:</span> <span class="n">PercentType</span>
</span></span><span class="line"><span class="cl">    <span class="n">estimated_duration_minutes</span><span class="p">:</span> <span class="n">MinutesType</span>
</span></span><span class="line"><span class="cl">    <span class="n">tone</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Brief description of the tone of the presentation.&#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">clarity</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?&#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">relevance</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Assess how well the content matches the audience&#39;s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?&#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">visual_design</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?&#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">engagement</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?&#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">pacing</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Analyze the distribution of content across slides. Are some slides too dense or too light? &#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">structure</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?&#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">consistency</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Evaluatue whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?&#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">accessibility</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?&#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"># Tool definition</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">calculate_slide_metric</span><span class="p">(</span><span class="n">metric</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Union</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">float</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">    Calculates the total number of slides, percentage of slides with code blocks,
</span></span></span><span class="line"><span class="cl"><span class="s2">    and percentage of slides with images in a Quarto presentation HTML file.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    Parameters
</span></span></span><span class="line"><span class="cl"><span class="s2">    ----------
</span></span></span><span class="line"><span class="cl"><span class="s2">    metric : str
</span></span></span><span class="line"><span class="cl"><span class="s2">        The metric to calculate: &#34;total_slides&#34; for total number of slides,
</span></span></span><span class="line"><span class="cl"><span class="s2">        &#34;code&#34; for percentage of slides containing fenced code blocks,
</span></span></span><span class="line"><span class="cl"><span class="s2">        or &#34;images&#34; for percentage of slides containing images.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    Returns
</span></span></span><span class="line"><span class="cl"><span class="s2">    -------
</span></span></span><span class="line"><span class="cl"><span class="s2">    float or int
</span></span></span><span class="line"><span class="cl"><span class="s2">        The calculated metric value.
</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="n">html_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./Quarto/docs/my-presentation.html&#34;</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">html_file</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">FileNotFoundError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;HTML file </span><span class="si">{</span><span class="n">html_file</span><span class="si">}</span><span class="s2"> does not exist.&#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"># Read HTML file</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="n">html_file</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#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">html_content</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Split on &lt;section&gt; tags to get individual slides</span>
</span></span><span class="line"><span class="cl">    <span class="n">slides</span> <span class="o">=</span> <span class="n">html_content</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;&lt;section&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">total_slides</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">slides</span><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="n">metric</span> <span class="o">==</span> <span class="s2">&#34;total_slides&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">total_slides</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="n">metric</span> <span class="o">==</span> <span class="s2">&#34;code&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">slides_with_code</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="s1">&#39;class=&#34;sourceCode&#34;&#39;</span> <span class="ow">in</span> <span class="n">slide</span> <span class="k">for</span> <span class="n">slide</span> <span class="ow">in</span> <span class="n">slides</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="nb">round</span><span class="p">((</span><span class="n">slides_with_code</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="n">metric</span> <span class="o">==</span> <span class="s2">&#34;images&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">slides_with_image</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="s2">&#34;&lt;img&#34;</span> <span class="ow">in</span> <span class="n">slide</span> <span class="k">for</span> <span class="n">slide</span> <span class="ow">in</span> <span class="n">slides</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="nb">round</span><span class="p">((</span><span class="n">slides_with_image</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;Unknown metric: choose &#39;total_slides&#39;, &#39;code&#39;, or &#39;images&#39;&#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">result</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"># Data wrangling</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">make_frames</span><span class="p">(</span><span class="n">d</span><span class="p">:</span> <span class="nb">dict</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">    Convert the dictionary returned by the LLM into a meta dictionary and
</span></span></span><span class="line"><span class="cl"><span class="s2">    a DataFrame for the eval categories.
</span></span></span><span class="line"><span class="cl"><span class="s2">    Parameters
</span></span></span><span class="line"><span class="cl"><span class="s2">    ----------
</span></span></span><span class="line"><span class="cl"><span class="s2">    d : dict
</span></span></span><span class="line"><span class="cl"><span class="s2">        The dictionary returned by the LLM.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    Returns
</span></span></span><span class="line"><span class="cl"><span class="s2">    -------
</span></span></span><span class="line"><span class="cl"><span class="s2">    dict
</span></span></span><span class="line"><span class="cl"><span class="s2">        A dictionary with two keys: &#34;meta&#34; and &#34;evals&#34;. &#34;meta&#34; contains the
</span></span></span><span class="line"><span class="cl"><span class="s2">        meta information as a dictionary, and &#34;evals&#34; contains a DataFrame with
</span></span></span><span class="line"><span class="cl"><span class="s2">        the eval categories.
</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="n">meta_keys</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;presentation_title&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;total_slides&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;percent_with_code&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;percent_with_images&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;estimated_duration_minutes&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;tone&#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">meta</span> <span class="o">=</span> <span class="p">{</span><span class="n">k</span><span class="p">:</span> <span class="n">d</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">meta_keys</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># eval categories (everything else)</span>
</span></span><span class="line"><span class="cl">    <span class="n">evals</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">d</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">k</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">meta_keys</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># fix typo</span>
</span></span><span class="line"><span class="cl">            <span class="n">evals</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></span><span class="line"><span class="cl">                    <span class="s2">&#34;category&#34;</span><span class="p">:</span> <span class="n">k</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;score&#34;</span><span class="p">:</span> <span class="n">v</span><span class="p">[</span><span class="s2">&#34;score&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;justification&#34;</span><span class="p">:</span> <span class="n">v</span><span class="p">[</span><span class="s2">&#34;justification&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;improvements&#34;</span><span class="p">:</span> <span class="n">v</span><span class="p">[</span><span class="s2">&#34;improvements&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;score_after_improvements&#34;</span><span class="p">:</span> <span class="n">v</span><span class="p">[</span><span class="s2">&#34;score_after_improvements&#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">evals_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">evals</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="p">{</span><span class="s2">&#34;meta&#34;</span><span class="p">:</span> <span class="n">meta</span><span class="p">,</span> <span class="s2">&#34;evals&#34;</span><span class="p">:</span> <span class="n">evals_df</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"># Tooltip helper</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">add_line_breaks</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">width</span><span class="o">=</span><span class="mi">50</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="nb">isinstance</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">text</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">words</span> <span class="o">=</span> <span class="n">text</span><span class="o">.</span><span class="n">split</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">lines</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="n">current_line</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">words</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># +1 accounts for the space if current_line isn&#39;t empty</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">current_line</span><span class="p">)</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="mi">1</span> <span class="k">if</span> <span class="n">current_line</span> <span class="k">else</span> <span class="mi">0</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="n">width</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">current_line</span> <span class="o">+=</span> <span class="p">(</span><span class="s2">&#34; &#34;</span> <span class="k">if</span> <span class="n">current_line</span> <span class="k">else</span> <span class="s2">&#34;&#34;</span><span class="p">)</span> <span class="o">+</span> <span class="n">word</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">lines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">current_line</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">current_line</span> <span class="o">=</span> <span class="n">word</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">current_line</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">lines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">current_line</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="s2">&#34;&lt;br&gt;&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">lines</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"># Icons</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ======================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># SVG icon copied from https://icons.getbootstrap.com/icons/file-slides-fill/</span>
</span></span><span class="line"><span class="cl"><span class="n">file_slides</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; fill=&#34;currentColor&#34; class=&#34;bi bi-file-slides-fill&#34; viewBox=&#34;0 0 16 16&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M7 7.78V5.22c0-.096.106-.156.19-.106l2.13 1.279a.125.125 0 0 1 0 .214l-2.13 1.28A.125.125 0 0 1 7 7.778z&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M5 4h6a.5.5 0 0 1 .496.438l.5 4A.5.5 0 0 1 11.5 9h-3v2.016c.863.055 1.5.251 1.5.484 0 .276-.895.5-2 .5s-2-.224-2-.5c0-.233.637-.429 1.5-.484V9h-3a.5.5 0 0 1-.496-.562l.5-4A.5.5 0 0 1 5 4&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">&lt;/svg&gt;&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">file_slides_loader</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; width=&#34;6em&#34; height=&#34;6em&#34; fill=&#34;currentColor&#34; class=&#34;bi bi-file-slides bounce&#34; viewBox=&#34;0 0 16 16&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M5 4a.5.5 0 0 0-.496.438l-.5 4A.5.5 0 0 0 4.5 9h3v2.016c-.863.055-1.5.251-1.5.484 0 .276.895.5 2 .5s2-.224 2-.5c0-.233-.637-.429-1.5-.484V9h3a.5.5 0 0 0 .496-.562l-.5-4A.5.5 0 0 0 11 4zm2 3.78V5.22c0-.096.106-.156.19-.106l2.13 1.279a.125.125 0 0 1 0 .214l-2.13 1.28A.125.125 0 0 1 7 7.778z&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">&lt;/svg&gt;&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># SVG icon copied from https://icons.getbootstrap.com/icons/file-code-fill/</span>
</span></span><span class="line"><span class="cl"><span class="n">file_code</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; fill=&#34;currentColor&#34; class=&#34;bi bi-file-code-fill&#34; viewBox=&#34;0 0 16 16&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M6.646 5.646a.5.5 0 1 1 .708.708L5.707 8l1.647 1.646a.5.5 0 0 1-.708.708l-2-2a.5.5 0 0 1 0-.708zm2.708 0 2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 8 8.646 6.354a.5.5 0 1 1 .708-.708&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">&lt;/svg&gt;&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># SVG icon copied from https://icons.getbootstrap.com/icons/file-image-fill/</span>
</span></span><span class="line"><span class="cl"><span class="n">file_image</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; fill=&#34;currentColor&#34; class=&#34;bi bi-file-image-fill&#34; viewBox=&#34;0 0 16 16&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M4 0h8a2 2 0 0 1 2 2v8.293l-2.73-2.73a1 1 0 0 0-1.52.127l-1.889 2.644-1.769-1.062a1 1 0 0 0-1.222.15L2 12.292V2a2 2 0 0 1 2-2m4.002 5.5a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M10.564 8.27 14 11.708V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-.293l3.578-3.577 2.56 1.536 2.426-3.395z&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">&lt;/svg&gt;&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># SVG icon copied from https://icons.getbootstrap.com/icons/robot/</span>
</span></span><span class="line"><span class="cl"><span class="n">robot</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; width=&#34;16&#34; height=&#34;16&#34; fill=&#34;currentColor&#34; class=&#34;bi bi-robot&#34; viewBox=&#34;0 0 16 16&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">&lt;/svg&gt;&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">robot_loader</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; width=&#34;6em&#34; height=&#34;6em&#34; fill=&#34;currentColor&#34; class=&#34;bi bi-robot text-primary bounce&#34; viewBox=&#34;0 0 16 16&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">&lt;/svg&gt;&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># SVG icon copied from https://icons.getbootstrap.com/icons/emoji-frown-fill/</span>
</span></span><span class="line"><span class="cl"><span class="n">sad_icon</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;&lt;svg xmlns=&#34;http://www.w3.org/2000/svg&#34; width=&#34;32&#34; height=&#34;32&#34; fill=&#34;currentColor&#34; class=&#34;bi bi-emoji-frown-fill text-warning&#34; viewBox=&#34;0 0 16 16&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">  &lt;path d=&#34;M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16M7 6.5C7 7.328 6.552 8 6 8s-1-.672-1-1.5S5.448 5 6 5s1 .672 1 1.5m-2.715 5.933a.5.5 0 0 1-.183-.683A4.5 4.5 0 0 1 8 9.5a4.5 4.5 0 0 1 3.898 2.25.5.5 0 0 1-.866.5A3.5 3.5 0 0 0 8 10.5a3.5 3.5 0 0 0-3.032 1.75.5.5 0 0 1-.683.183M10 8c-.552 0-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5S10.552 8 10 8&#34;/&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">&lt;/svg&gt;&#34;&#34;&#34;</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"># Shiny App</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_fillable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1">## General theme and styles</span>
</span></span><span class="line"><span class="cl">    <span class="c1">## 1. Custom CSS</span>
</span></span><span class="line"><span class="cl">    <span class="n">ui</span><span class="o">.</span><span class="n">tags</span><span class="o">.</span><span class="n">style</span><span class="p">(</span><span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">        .bounce {
</span></span></span><span class="line"><span class="cl"><span class="s2">            animation: bounce 2s infinite;
</span></span></span><span class="line"><span class="cl"><span class="s2">        }
</span></span></span><span class="line"><span class="cl"><span class="s2">        @keyframes bounce {
</span></span></span><span class="line"><span class="cl"><span class="s2">            0%, 100% {
</span></span></span><span class="line"><span class="cl"><span class="s2">                transform: translateY(0);
</span></span></span><span class="line"><span class="cl"><span class="s2">            }
</span></span></span><span class="line"><span class="cl"><span class="s2">            50% {
</span></span></span><span class="line"><span class="cl"><span class="s2">                transform: translateY(-20px);
</span></span></span><span class="line"><span class="cl"><span class="s2">            }
</span></span></span><span class="line"><span class="cl"><span class="s2">        }
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#34;&#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">layout_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="c1">## Sidebar content</span>
</span></span><span class="line"><span class="cl">            <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="n">ui</span><span class="o">.</span><span class="n">p</span><span class="p">(</span><span class="n">ui</span><span class="o">.</span><span class="n">strong</span><span class="p">(</span><span class="s2">&#34;Hey, I am DeckCheck!&#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">p</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;&#34;&#34;I can help you improve your Quarto presentations by analysing them and suggesting improvements. Before I can do that, I need some information about your presentation.&#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="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">ui</span><span class="o">.</span><span class="n">input_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;file&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Upload your Quarto presentation&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">accept</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;.qmd&#34;</span><span class="p">,</span> <span class="s2">&#34;.qmdx&#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">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">input_text_area</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;audience&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Describe your audience&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">height</span><span class="o">=</span><span class="s2">&#34;150px&#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;e.g. Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#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_numeric</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;length&#34;</span><span class="p">,</span> <span class="s2">&#34;Time cap for the presentation (minutes)&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span> <span class="nb">min</span><span class="o">=</span><span class="mi">1</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_text</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;type&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Type of talk&#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;e.g. lightning talk, workshop, or keynote&#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_text</span><span class="p">(</span><span class="s2">&#34;event&#34;</span><span class="p">,</span> <span class="s2">&#34;Event name&#34;</span><span class="p">,</span> <span class="n">placeholder</span><span class="o">=</span><span class="s2">&#34;e.g. posit::conf(2025)&#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_task_button</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;submit&#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">ui</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">robot</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;Analyse presentation&#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">width</span><span class="o">=</span><span class="mi">400</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">## Main content</span>
</span></span><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;results&#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"># Bootswatch theme</span>
</span></span><span class="line"><span class="cl">    <span class="n">theme</span><span class="o">=</span><span class="n">shinyswatch</span><span class="o">.</span><span class="n">theme</span><span class="o">.</span><span class="n">flatly</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="nd">@ui.bind_task_button</span><span class="p">(</span><span class="n">button_id</span><span class="o">=</span><span class="s2">&#34;submit&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@reactive.extended_task</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">quarto_task</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">temp_dir</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># We&#39;re using an Extended Task to avoid blocking. Note that</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># a temporary directory called within mirai will be</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># different from the one in the &#34;main&#34; Shiny session. Hence,</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># we pass a temp_dir parameter to the task and use that.</span>
</span></span><span class="line"><span class="cl">        <span class="n">qmd_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">temp_dir</span><span class="p">)</span> <span class="o">/</span> <span class="s2">&#34;my-presentation.qmd&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">shutil</span><span class="o">.</span><span class="n">copy</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">qmd_file</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Run asyncio subprocess</span>
</span></span><span class="line"><span class="cl">        <span class="n">proc</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">create_subprocess_exec</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;quarto&#34;</span><span class="p">,</span> <span class="s2">&#34;render&#34;</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">qmd_file</span><span class="p">),</span> <span class="s2">&#34;--to&#34;</span><span class="p">,</span> <span class="s2">&#34;markdown,html&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="n">proc</span><span class="o">.</span><span class="n">communicate</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Return the path to the markdown file</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">Path</span><span class="p">(</span><span class="n">temp_dir</span><span class="p">)</span> <span class="o">/</span> <span class="s2">&#34;my-presentation.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@ui.bind_task_button</span><span class="p">(</span><span class="n">button_id</span><span class="o">=</span><span class="s2">&#34;submit&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@reactive.extended_task</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">chat_task</span><span class="p">(</span><span class="n">system_prompt</span><span class="p">,</span> <span class="n">markdown_content</span><span class="p">,</span> <span class="n">DeckAnalysis</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># We&#39;re using an extended task to avoid blocking the session and</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># we start a fresh chat session each time.</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># For a feedback loop, we would use a persistent chat session.</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat</span> <span class="o">=</span> <span class="n">ChatAnthropic</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;claude-sonnet-4-20250514&#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="n">system_prompt</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"># Set model parameters (optional)</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat</span><span class="o">.</span><span class="n">set_model_params</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">temperature</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span>  <span class="c1"># default is 1</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 the chat</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">calculate_slide_metric</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Task 1: regular chat to extract meta-data</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat_res1</span> <span class="o">=</span> <span class="k">await</span> <span class="n">chat</span><span class="o">.</span><span class="n">chat_async</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Execute Task 1 (counts). Here are the slides in Markdown: {{ markdown_content }}&#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></span><span class="line"><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">chat_res1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Task 2: structured chat to further analyse the slides</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat_res2</span> <span class="o">=</span> <span class="k">await</span> <span class="n">chat</span><span class="o">.</span><span class="n">chat_structured_async</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Execute Task 2 (suggestions)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">data_model</span><span class="o">=</span><span class="n">DeckAnalysis</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">return</span> <span class="n">chat_res2</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">submit</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">run_quarto</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">input</span><span class="o">.</span><span class="n">file</span><span class="p">()</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># Error for testing</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># raise ValueError(&#34;Test error&#34;)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="c1"># Get file path of the uploaded file</span>
</span></span><span class="line"><span class="cl">            <span class="n">file_path</span> <span class="o">=</span> <span class="nb">input</span><span class="o">.</span><span class="n">file</span><span class="p">()[</span><span class="mi">0</span><span class="p">][</span><span class="s2">&#34;datapath&#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">quarto_task</span><span class="o">.</span><span class="n">invoke</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">tempfile</span><span class="o">.</span><span class="n">gettempdir</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">warnings</span><span class="o">.</span><span class="n">warn</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Error when trying to invoke quarto_task: </span><span class="si">{</span><span class="n">e</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="c1"># Print stack trace to the console</span>
</span></span><span class="line"><span class="cl">            <span class="n">traceback</span><span class="o">.</span><span class="n">print_exc</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="c1"># Return value that triggers modal in UI</span>
</span></span><span class="line"><span class="cl">            <span class="n">m</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">modal</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">div</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="c1"># Sad bootstrap icon</span>
</span></span><span class="line"><span class="cl">                    <span class="n">ui</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">sad_icon</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">br</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="s2">&#34;The not so Shiny Side of LLMs. Please check that your Quarto presentation is valid and contains slides.&#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"># add class to center the content</span>
</span></span><span class="line"><span class="cl">                    <span class="n">class_</span><span class="o">=</span><span class="s2">&#34;text-center&#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">title</span><span class="o">=</span><span class="s2">&#34;Oops, something went wrong!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">easy_close</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">footer</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">modal_button</span><span class="p">(</span><span class="s2">&#34;Close&#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">modal_show</span><span class="p">(</span><span class="n">m</span><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="k">def</span> <span class="nf">run_chat</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># require quarto_task result to be available</span>
</span></span><span class="line"><span class="cl">        <span class="n">req</span><span class="p">(</span><span class="n">quarto_task</span><span class="o">.</span><span class="n">result</span><span class="p">()</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># Error for testing</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># raise ValueError(&#34;Test error&#34;)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="c1"># Get the Markdown file path from the complete quarto_task</span>
</span></span><span class="line"><span class="cl">            <span class="n">markdown_file</span> <span class="o">=</span> <span class="n">quarto_task</span><span class="o">.</span><span class="n">result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># Read the generated Markdown file containing the slides</span>
</span></span><span class="line"><span class="cl">            <span class="n">markdown_content</span> <span class="o">=</span> <span class="n">markdown_file</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#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"># Define prompt file</span>
</span></span><span class="line"><span class="cl">            <span class="n">system_prompt_file</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">ROOT_DIR</span> <span class="o">/</span> <span class="s2">&#34;prompts&#34;</span> <span class="o">/</span> <span class="s2">&#34;prompt-analyse-slides-structured-tool.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"># Create system prompt</span>
</span></span><span class="line"><span class="cl">            <span class="n">system_prompt</span> <span class="o">=</span> <span class="n">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">variables</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;audience&#34;</span><span class="p">:</span> <span class="nb">input</span><span class="o">.</span><span class="n">audience</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;length&#34;</span><span class="p">:</span> <span class="nb">input</span><span class="o">.</span><span class="n">length</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="nb">input</span><span class="o">.</span><span class="n">type</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;event&#34;</span><span class="p">:</span> <span class="nb">input</span><span class="o">.</span><span class="n">event</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">                    <span class="s2">&#34;markdown_content&#34;</span><span class="p">:</span> <span class="n">markdown_content</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"># Trigger the chat task with the provided inputs</span>
</span></span><span class="line"><span class="cl">            <span class="n">chat_task</span><span class="o">.</span><span class="n">invoke</span><span class="p">(</span><span class="n">system_prompt</span><span class="p">,</span> <span class="n">markdown_content</span><span class="p">,</span> <span class="n">DeckAnalysis</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">warnings</span><span class="o">.</span><span class="n">warn</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Error when trying to invoke chat_task: </span><span class="si">{</span><span class="n">e</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="c1"># Print stack trace to the console</span>
</span></span><span class="line"><span class="cl">            <span class="n">traceback</span><span class="o">.</span><span class="n">print_exc</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="c1"># Return value that triggers modal in UI</span>
</span></span><span class="line"><span class="cl">            <span class="n">m</span> <span class="o">=</span> <span class="n">ui</span><span class="o">.</span><span class="n">modal</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">div</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                    <span class="c1"># Sad bootstrap icon</span>
</span></span><span class="line"><span class="cl">                    <span class="n">ui</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">sad_icon</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">br</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="s2">&#34;The not so Shiny Side of LLMs. Unfortunately, chatting didn&#39;t work out. Do you have enough credits left?&#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"># add class to center the content</span>
</span></span><span class="line"><span class="cl">                    <span class="n">class_</span><span class="o">=</span><span class="s2">&#34;text-center&#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">title</span><span class="o">=</span><span class="s2">&#34;Oops, something went wrong!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">easy_close</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">footer</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">modal_button</span><span class="p">(</span><span class="s2">&#34;Close&#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">modal_show</span><span class="p">(</span><span class="n">m</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</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">analysis_result</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">res</span> <span class="o">=</span> <span class="n">chat_task</span><span class="o">.</span><span class="n">result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">res</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="n">make_frames</span><span class="p">(</span><span class="n">res</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">
</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">async</span> <span class="k">def</span> <span class="nf">results</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">quarto_task</span><span class="o">.</span><span class="n">status</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;running&#34;</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">div</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">HTML</span><span class="p">(</span><span class="n">file_slides_loader</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">br</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 class="s2">&#34;Processing your Quarto presentation...&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">class_</span><span class="o">=</span><span class="s2">&#34;text-center d-flex flex-column justify-content-center align-items-center&#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;height: 100%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">chat_task</span><span class="o">.</span><span class="n">status</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;running&#34;</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">div</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">HTML</span><span class="p">(</span><span class="n">robot_loader</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">br</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 class="s2">&#34;The LLM is doing its magic...&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">class_</span><span class="o">=</span><span class="s2">&#34;text-center d-flex flex-column justify-content-center align-items-center&#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;height: 100%&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">elif</span> <span class="n">chat_task</span><span class="o">.</span><span class="n">status</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;success&#34;</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">TagList</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">layout_column_wrap</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">tooltip</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">value_box</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                            <span class="s2">&#34;Showtime&#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</span><span class="p">(</span><span class="s2">&#34;showtime&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                            <span class="n">showcase</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">file_slides</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                            <span class="n">theme</span><span class="o">=</span><span class="s2">&#34;primary&#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;Slides are being counted based on the provided Quarto presentation, then an educated guess is made about the time it will take to present them.&#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">tooltip</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">value_box</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                            <span class="s2">&#34;Code Savviness&#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</span><span class="p">(</span><span class="s2">&#34;code_savviness&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                            <span class="n">showcase</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">file_code</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                            <span class="n">theme</span><span class="o">=</span><span class="s2">&#34;primary&#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;Code Saviness is calculated based on the slides that contain code chunks. The percentage is the ratio of those slides to total slides.&#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">tooltip</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">value_box</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                            <span class="s2">&#34;Image Presence&#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</span><span class="p">(</span><span class="s2">&#34;image_presence&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                            <span class="n">showcase</span><span class="o">=</span><span class="n">ui</span><span class="o">.</span><span class="n">HTML</span><span class="p">(</span><span class="n">file_image</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                            <span class="n">theme</span><span class="o">=</span><span class="s2">&#34;primary&#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;Image Presence is calculated based on the slides that contain images. The percentage is the ratio of those slides to total slides.&#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">width</span><span class="o">=</span><span class="mi">1</span> <span class="o">/</span> <span class="mi">3</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">layout_column_wrap</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="n">ui</span><span class="o">.</span><span class="n">strong</span><span class="p">(</span><span class="s2">&#34;Scores per category&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">                        <span class="n">output_widget</span><span class="p">(</span><span class="s2">&#34;scores&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                        <span class="n">height</span><span class="o">=</span><span class="s2">&#34;600px&#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">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></span><span class="line"><span class="cl">                            <span class="n">ui</span><span class="o">.</span><span class="n">strong</span><span class="p">(</span><span class="s2">&#34;Suggested improvements per category&#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">output_data_frame</span><span class="p">(</span><span class="s2">&#34;suggested_improvements&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                        <span class="n">height</span><span class="o">=</span><span class="s2">&#34;600px&#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">width</span><span class="o">=</span><span class="mi">1</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">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="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nd">@render_widget</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">scores</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">res</span> <span class="o">=</span> <span class="n">analysis_result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">req</span><span class="p">(</span><span class="n">res</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">evals</span> <span class="o">=</span> <span class="n">res</span><span class="p">[</span><span class="s2">&#34;evals&#34;</span><span class="p">]</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">evals</span> <span class="o">=</span> <span class="n">evals</span><span class="o">.</span><span class="n">sort_values</span><span class="p">(</span><span class="s2">&#34;score&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;category&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">pd</span><span class="o">.</span><span class="n">Categorical</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;category&#34;</span><span class="p">],</span> <span class="n">categories</span><span class="o">=</span><span class="n">evals</span><span class="p">[</span><span class="s2">&#34;category&#34;</span><span class="p">],</span> <span class="n">ordered</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"># apply to the justification column</span>
</span></span><span class="line"><span class="cl">        <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;justification_wrapped&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;justification&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">apply</span><span class="p">(</span><span class="n">add_line_breaks</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 custom tooltip column</span>
</span></span><span class="line"><span class="cl">        <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;tooltip&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;Score: &#34;</span>
</span></span><span class="line"><span class="cl">            <span class="o">+</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;score&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">str</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="o">+</span> <span class="s2">&#34;&lt;br&gt;After improvements: &#34;</span>
</span></span><span class="line"><span class="cl">            <span class="o">+</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;score_after_improvements&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="nb">str</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="o">+</span> <span class="s2">&#34;&lt;br&gt;Justification: &#34;</span>
</span></span><span class="line"><span class="cl">            <span class="o">+</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;justification_wrapped&#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">plot</span> <span class="o">=</span> <span class="n">px</span><span class="o">.</span><span class="n">bar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">evals</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">x</span><span class="o">=</span><span class="s2">&#34;score&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">y</span><span class="o">=</span><span class="s2">&#34;category&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">orientation</span><span class="o">=</span><span class="s2">&#34;h&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">labels</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;category&#34;</span><span class="p">:</span> <span class="s2">&#34;Category&#34;</span><span class="p">,</span> <span class="s2">&#34;score&#34;</span><span class="p">:</span> <span class="s2">&#34;Score&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">            <span class="n">hover_data</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;tooltip&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">},</span>  <span class="c1"># include the tooltip column</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"># Set hovertemplate to use our custom tooltip</span>
</span></span><span class="line"><span class="cl">        <span class="n">plot</span><span class="o">.</span><span class="n">update_traces</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">hovertemplate</span><span class="o">=</span><span class="s2">&#34;%</span><span class="si">{customdata[0]}</span><span class="s2">&lt;extra&gt;&lt;/extra&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="n">customdata</span><span class="o">=</span><span class="n">evals</span><span class="p">[[</span><span class="s2">&#34;tooltip&#34;</span><span class="p">]]</span><span class="o">.</span><span class="n">values</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">plot</span><span class="o">.</span><span class="n">update_traces</span><span class="p">(</span><span class="n">marker_color</span><span class="o">=</span><span class="s2">&#34;#18bc9c&#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">plot</span><span class="o">.</span><span class="n">update_layout</span><span class="p">(</span><span class="n">template</span><span class="o">=</span><span class="s2">&#34;simple_white&#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">plot</span>
</span></span><span class="line"><span class="cl">
</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">suggested_improvements</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">res</span> <span class="o">=</span> <span class="n">analysis_result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">req</span><span class="p">(</span><span class="n">res</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">evals</span> <span class="o">=</span> <span class="n">res</span><span class="p">[</span><span class="s2">&#34;evals&#34;</span><span class="p">]</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">evals</span><span class="p">[</span><span class="s2">&#34;Gain&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;score_after_improvements&#34;</span><span class="p">]</span> <span class="o">-</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;score&#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">result_table</span> <span class="o">=</span> <span class="n">evals</span><span class="o">.</span><span class="n">assign</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">Category</span><span class="o">=</span><span class="n">evals</span><span class="p">[</span><span class="s2">&#34;category&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">            <span class="o">**</span><span class="p">{</span><span class="s2">&#34;Current score&#34;</span><span class="p">:</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;score&#34;</span><span class="p">]},</span>
</span></span><span class="line"><span class="cl">            <span class="n">Improvements</span><span class="o">=</span><span class="n">evals</span><span class="p">[</span><span class="s2">&#34;improvements&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">            <span class="o">**</span><span class="p">{</span><span class="s2">&#34;Score After Improvements&#34;</span><span class="p">:</span> <span class="n">evals</span><span class="p">[</span><span class="s2">&#34;score_after_improvements&#34;</span><span class="p">]},</span>
</span></span><span class="line"><span class="cl">            <span class="n">Gain</span><span class="o">=</span><span class="n">evals</span><span class="p">[</span><span class="s2">&#34;Gain&#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="s2">&#34;Category&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Current score&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Improvements&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Score After Improvements&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;Gain&#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 class="o">.</span><span class="n">sort_values</span><span class="p">(</span><span class="s2">&#34;Gain&#34;</span><span class="p">,</span> <span class="n">ascending</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">result_table</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">showtime</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">res</span> <span class="o">=</span> <span class="n">analysis_result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">req</span><span class="p">(</span><span class="n">res</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</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;</span><span class="si">{</span><span class="n">res</span><span class="p">[</span><span class="s1">&#39;meta&#39;</span><span class="p">][</span><span class="s1">&#39;estimated_duration_minutes&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2"> minutes&#34;</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">code_savviness</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">res</span> <span class="o">=</span> <span class="n">analysis_result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">req</span><span class="p">(</span><span class="n">res</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</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;</span><span class="si">{</span><span class="n">res</span><span class="p">[</span><span class="s1">&#39;meta&#39;</span><span class="p">][</span><span class="s1">&#39;percent_with_code&#39;</span><span class="p">]</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 class="nd">@render.text</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">image_presence</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">res</span> <span class="o">=</span> <span class="n">analysis_result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">req</span><span class="p">(</span><span class="n">res</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</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;</span><span class="si">{</span><span class="n">res</span><span class="p">[</span><span class="s1">&#39;meta&#39;</span><span class="p">][</span><span class="s1">&#39;percent_with_images&#39;</span><span class="p">]</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 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></details>
</div>
<div id="tabset-12-2">
<blockquote>
<p><strong>Get this code from GitHub</strong></p>
<p>You can grab the code directly from <a href="https://github.com/hypebright/the-shiny-side-of-llms/blob/d1094d2774f9d0c213c7ddf6e17f94da706b1b76/R/deckcheck/app.R" target="_blank" rel="noopener">here</a>
.</p>
</blockquote>
<details class="code-fold">
<summary>See full app</summary>
<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><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span><span class="lnt">153
</span><span class="lnt">154
</span><span class="lnt">155
</span><span class="lnt">156
</span><span class="lnt">157
</span><span class="lnt">158
</span><span class="lnt">159
</span><span class="lnt">160
</span><span class="lnt">161
</span><span class="lnt">162
</span><span class="lnt">163
</span><span class="lnt">164
</span><span class="lnt">165
</span><span class="lnt">166
</span><span class="lnt">167
</span><span class="lnt">168
</span><span class="lnt">169
</span><span class="lnt">170
</span><span class="lnt">171
</span><span class="lnt">172
</span><span class="lnt">173
</span><span class="lnt">174
</span><span class="lnt">175
</span><span class="lnt">176
</span><span class="lnt">177
</span><span class="lnt">178
</span><span class="lnt">179
</span><span class="lnt">180
</span><span class="lnt">181
</span><span class="lnt">182
</span><span class="lnt">183
</span><span class="lnt">184
</span><span class="lnt">185
</span><span class="lnt">186
</span><span class="lnt">187
</span><span class="lnt">188
</span><span class="lnt">189
</span><span class="lnt">190
</span><span class="lnt">191
</span><span class="lnt">192
</span><span class="lnt">193
</span><span class="lnt">194
</span><span class="lnt">195
</span><span class="lnt">196
</span><span class="lnt">197
</span><span class="lnt">198
</span><span class="lnt">199
</span><span class="lnt">200
</span><span class="lnt">201
</span><span class="lnt">202
</span><span class="lnt">203
</span><span class="lnt">204
</span><span class="lnt">205
</span><span class="lnt">206
</span><span class="lnt">207
</span><span class="lnt">208
</span><span class="lnt">209
</span><span class="lnt">210
</span><span class="lnt">211
</span><span class="lnt">212
</span><span class="lnt">213
</span><span class="lnt">214
</span><span class="lnt">215
</span><span class="lnt">216
</span><span class="lnt">217
</span><span class="lnt">218
</span><span class="lnt">219
</span><span class="lnt">220
</span><span class="lnt">221
</span><span class="lnt">222
</span><span class="lnt">223
</span><span class="lnt">224
</span><span class="lnt">225
</span><span class="lnt">226
</span><span class="lnt">227
</span><span class="lnt">228
</span><span class="lnt">229
</span><span class="lnt">230
</span><span class="lnt">231
</span><span class="lnt">232
</span><span class="lnt">233
</span><span class="lnt">234
</span><span class="lnt">235
</span><span class="lnt">236
</span><span class="lnt">237
</span><span class="lnt">238
</span><span class="lnt">239
</span><span class="lnt">240
</span><span class="lnt">241
</span><span class="lnt">242
</span><span class="lnt">243
</span><span class="lnt">244
</span><span class="lnt">245
</span><span class="lnt">246
</span><span class="lnt">247
</span><span class="lnt">248
</span><span class="lnt">249
</span><span class="lnt">250
</span><span class="lnt">251
</span><span class="lnt">252
</span><span class="lnt">253
</span><span class="lnt">254
</span><span class="lnt">255
</span><span class="lnt">256
</span><span class="lnt">257
</span><span class="lnt">258
</span><span class="lnt">259
</span><span class="lnt">260
</span><span class="lnt">261
</span><span class="lnt">262
</span><span class="lnt">263
</span><span class="lnt">264
</span><span class="lnt">265
</span><span class="lnt">266
</span><span class="lnt">267
</span><span class="lnt">268
</span><span class="lnt">269
</span><span class="lnt">270
</span><span class="lnt">271
</span><span class="lnt">272
</span><span class="lnt">273
</span><span class="lnt">274
</span><span class="lnt">275
</span><span class="lnt">276
</span><span class="lnt">277
</span><span class="lnt">278
</span><span class="lnt">279
</span><span class="lnt">280
</span><span class="lnt">281
</span><span class="lnt">282
</span><span class="lnt">283
</span><span class="lnt">284
</span><span class="lnt">285
</span><span class="lnt">286
</span><span class="lnt">287
</span><span class="lnt">288
</span><span class="lnt">289
</span><span class="lnt">290
</span><span class="lnt">291
</span><span class="lnt">292
</span><span class="lnt">293
</span><span class="lnt">294
</span><span class="lnt">295
</span><span class="lnt">296
</span><span class="lnt">297
</span><span class="lnt">298
</span><span class="lnt">299
</span><span class="lnt">300
</span><span class="lnt">301
</span><span class="lnt">302
</span><span class="lnt">303
</span><span class="lnt">304
</span><span class="lnt">305
</span><span class="lnt">306
</span><span class="lnt">307
</span><span class="lnt">308
</span><span class="lnt">309
</span><span class="lnt">310
</span><span class="lnt">311
</span><span class="lnt">312
</span><span class="lnt">313
</span><span class="lnt">314
</span><span class="lnt">315
</span><span class="lnt">316
</span><span class="lnt">317
</span><span class="lnt">318
</span><span class="lnt">319
</span><span class="lnt">320
</span><span class="lnt">321
</span><span class="lnt">322
</span><span class="lnt">323
</span><span class="lnt">324
</span><span class="lnt">325
</span><span class="lnt">326
</span><span class="lnt">327
</span><span class="lnt">328
</span><span class="lnt">329
</span><span class="lnt">330
</span><span class="lnt">331
</span><span class="lnt">332
</span><span class="lnt">333
</span><span class="lnt">334
</span><span class="lnt">335
</span><span class="lnt">336
</span><span class="lnt">337
</span><span class="lnt">338
</span><span class="lnt">339
</span><span class="lnt">340
</span><span class="lnt">341
</span><span class="lnt">342
</span><span class="lnt">343
</span><span class="lnt">344
</span><span class="lnt">345
</span><span class="lnt">346
</span><span class="lnt">347
</span><span class="lnt">348
</span><span class="lnt">349
</span><span class="lnt">350
</span><span class="lnt">351
</span><span class="lnt">352
</span><span class="lnt">353
</span><span class="lnt">354
</span><span class="lnt">355
</span><span class="lnt">356
</span><span class="lnt">357
</span><span class="lnt">358
</span><span class="lnt">359
</span><span class="lnt">360
</span><span class="lnt">361
</span><span class="lnt">362
</span><span class="lnt">363
</span><span class="lnt">364
</span><span class="lnt">365
</span><span class="lnt">366
</span><span class="lnt">367
</span><span class="lnt">368
</span><span class="lnt">369
</span><span class="lnt">370
</span><span class="lnt">371
</span><span class="lnt">372
</span><span class="lnt">373
</span><span class="lnt">374
</span><span class="lnt">375
</span><span class="lnt">376
</span><span class="lnt">377
</span><span class="lnt">378
</span><span class="lnt">379
</span><span class="lnt">380
</span><span class="lnt">381
</span><span class="lnt">382
</span><span class="lnt">383
</span><span class="lnt">384
</span><span class="lnt">385
</span><span class="lnt">386
</span><span class="lnt">387
</span><span class="lnt">388
</span><span class="lnt">389
</span><span class="lnt">390
</span><span class="lnt">391
</span><span class="lnt">392
</span><span class="lnt">393
</span><span class="lnt">394
</span><span class="lnt">395
</span><span class="lnt">396
</span><span class="lnt">397
</span><span class="lnt">398
</span><span class="lnt">399
</span><span class="lnt">400
</span><span class="lnt">401
</span><span class="lnt">402
</span><span class="lnt">403
</span><span class="lnt">404
</span><span class="lnt">405
</span><span class="lnt">406
</span><span class="lnt">407
</span><span class="lnt">408
</span><span class="lnt">409
</span><span class="lnt">410
</span><span class="lnt">411
</span><span class="lnt">412
</span><span class="lnt">413
</span><span class="lnt">414
</span><span class="lnt">415
</span><span class="lnt">416
</span><span class="lnt">417
</span><span class="lnt">418
</span><span class="lnt">419
</span><span class="lnt">420
</span><span class="lnt">421
</span><span class="lnt">422
</span><span class="lnt">423
</span><span class="lnt">424
</span><span class="lnt">425
</span><span class="lnt">426
</span><span class="lnt">427
</span><span class="lnt">428
</span><span class="lnt">429
</span><span class="lnt">430
</span><span class="lnt">431
</span><span class="lnt">432
</span><span class="lnt">433
</span><span class="lnt">434
</span><span class="lnt">435
</span><span class="lnt">436
</span><span class="lnt">437
</span><span class="lnt">438
</span><span class="lnt">439
</span><span class="lnt">440
</span><span class="lnt">441
</span><span class="lnt">442
</span><span class="lnt">443
</span><span class="lnt">444
</span><span class="lnt">445
</span><span class="lnt">446
</span><span class="lnt">447
</span><span class="lnt">448
</span><span class="lnt">449
</span><span class="lnt">450
</span><span class="lnt">451
</span><span class="lnt">452
</span><span class="lnt">453
</span><span class="lnt">454
</span><span class="lnt">455
</span><span class="lnt">456
</span><span class="lnt">457
</span><span class="lnt">458
</span><span class="lnt">459
</span><span class="lnt">460
</span><span class="lnt">461
</span><span class="lnt">462
</span><span class="lnt">463
</span><span class="lnt">464
</span><span class="lnt">465
</span><span class="lnt">466
</span><span class="lnt">467
</span><span class="lnt">468
</span><span class="lnt">469
</span><span class="lnt">470
</span><span class="lnt">471
</span><span class="lnt">472
</span><span class="lnt">473
</span><span class="lnt">474
</span><span class="lnt">475
</span><span class="lnt">476
</span><span class="lnt">477
</span><span class="lnt">478
</span><span class="lnt">479
</span><span class="lnt">480
</span><span class="lnt">481
</span><span class="lnt">482
</span><span class="lnt">483
</span><span class="lnt">484
</span><span class="lnt">485
</span><span class="lnt">486
</span><span class="lnt">487
</span><span class="lnt">488
</span><span class="lnt">489
</span><span class="lnt">490
</span><span class="lnt">491
</span><span class="lnt">492
</span><span class="lnt">493
</span><span class="lnt">494
</span><span class="lnt">495
</span><span class="lnt">496
</span><span class="lnt">497
</span><span class="lnt">498
</span><span class="lnt">499
</span><span class="lnt">500
</span><span class="lnt">501
</span><span class="lnt">502
</span><span class="lnt">503
</span><span class="lnt">504
</span><span class="lnt">505
</span><span class="lnt">506
</span><span class="lnt">507
</span><span class="lnt">508
</span><span class="lnt">509
</span><span class="lnt">510
</span><span class="lnt">511
</span><span class="lnt">512
</span><span class="lnt">513
</span><span class="lnt">514
</span><span class="lnt">515
</span><span class="lnt">516
</span><span class="lnt">517
</span><span class="lnt">518
</span><span class="lnt">519
</span><span class="lnt">520
</span><span class="lnt">521
</span><span class="lnt">522
</span><span class="lnt">523
</span><span class="lnt">524
</span><span class="lnt">525
</span><span class="lnt">526
</span><span class="lnt">527
</span><span class="lnt">528
</span><span class="lnt">529
</span><span class="lnt">530
</span><span class="lnt">531
</span><span class="lnt">532
</span><span class="lnt">533
</span><span class="lnt">534
</span><span class="lnt">535
</span><span class="lnt">536
</span><span class="lnt">537
</span><span class="lnt">538
</span><span class="lnt">539
</span><span class="lnt">540
</span><span class="lnt">541
</span><span class="lnt">542
</span><span class="lnt">543
</span><span class="lnt">544
</span><span class="lnt">545
</span><span class="lnt">546
</span><span class="lnt">547
</span><span class="lnt">548
</span><span class="lnt">549
</span><span class="lnt">550
</span><span class="lnt">551
</span><span class="lnt">552
</span><span class="lnt">553
</span><span class="lnt">554
</span><span class="lnt">555
</span><span class="lnt">556
</span><span class="lnt">557
</span><span class="lnt">558
</span><span class="lnt">559
</span><span class="lnt">560
</span><span class="lnt">561
</span><span class="lnt">562
</span><span class="lnt">563
</span><span class="lnt">564
</span><span class="lnt">565
</span><span class="lnt">566
</span><span class="lnt">567
</span><span class="lnt">568
</span><span class="lnt">569
</span><span class="lnt">570
</span><span class="lnt">571
</span><span class="lnt">572
</span><span class="lnt">573
</span><span class="lnt">574
</span><span class="lnt">575
</span><span class="lnt">576
</span><span class="lnt">577
</span><span class="lnt">578
</span><span class="lnt">579
</span><span class="lnt">580
</span><span class="lnt">581
</span><span class="lnt">582
</span><span class="lnt">583
</span><span class="lnt">584
</span><span class="lnt">585
</span><span class="lnt">586
</span><span class="lnt">587
</span><span class="lnt">588
</span><span class="lnt">589
</span><span class="lnt">590
</span><span class="lnt">591
</span><span class="lnt">592
</span><span class="lnt">593
</span><span class="lnt">594
</span><span class="lnt">595
</span><span class="lnt">596
</span><span class="lnt">597
</span><span class="lnt">598
</span><span class="lnt">599
</span><span class="lnt">600
</span><span class="lnt">601
</span><span class="lnt">602
</span><span class="lnt">603
</span><span class="lnt">604
</span><span class="lnt">605
</span><span class="lnt">606
</span><span class="lnt">607
</span><span class="lnt">608
</span><span class="lnt">609
</span><span class="lnt">610
</span><span class="lnt">611
</span><span class="lnt">612
</span><span class="lnt">613
</span><span class="lnt">614
</span><span class="lnt">615
</span><span class="lnt">616
</span><span class="lnt">617
</span><span class="lnt">618
</span><span class="lnt">619
</span><span class="lnt">620
</span><span class="lnt">621
</span><span class="lnt">622
</span><span class="lnt">623
</span><span class="lnt">624
</span><span class="lnt">625
</span><span class="lnt">626
</span><span class="lnt">627
</span><span class="lnt">628
</span><span class="lnt">629
</span><span class="lnt">630
</span><span class="lnt">631
</span><span class="lnt">632
</span><span class="lnt">633
</span><span class="lnt">634
</span><span class="lnt">635
</span><span class="lnt">636
</span><span class="lnt">637
</span><span class="lnt">638
</span><span class="lnt">639
</span><span class="lnt">640
</span><span class="lnt">641
</span><span class="lnt">642
</span><span class="lnt">643
</span><span class="lnt">644
</span><span class="lnt">645
</span><span class="lnt">646
</span><span class="lnt">647
</span><span class="lnt">648
</span><span class="lnt">649
</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">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">mirai</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 class="nf">library</span><span class="p">(</span><span class="n">ggiraph</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">gdtools</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">purrr</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">dplyr</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"># Data Structure</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ======================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Reusable scoring category</span>
</span></span><span class="line"><span class="cl"><span class="n">type_scoring_category</span> <span class="o">&lt;-</span> <span class="nf">type_object</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">score</span> <span class="o">=</span> <span class="nf">type_integer</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;Score from 1 to 10.&#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">justification</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Brief explanation of the score.&#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">improvements</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Concise, actionable improvements, mentioning slide numbers if applicable.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">required</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">score_after_improvements</span> <span class="o">=</span> <span class="nf">type_integer</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;Estimated score after suggested improvements.&#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></span><span class="line"><span class="cl"><span class="c1"># Top-level deck analysis object</span>
</span></span><span class="line"><span class="cl"><span class="n">type_deck_analysis</span> <span class="o">&lt;-</span> <span class="nf">type_object</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">presentation_title</span> <span class="o">=</span> <span class="nf">type_string</span><span class="p">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">&#34;The presentation title.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">total_slides</span> <span class="o">=</span> <span class="nf">type_integer</span><span class="p">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Total number of slides.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">percent_with_code</span> <span class="o">=</span> <span class="nf">type_number</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;Percentage of slides containing code blocks (0–100).&#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">percent_with_images</span> <span class="o">=</span> <span class="nf">type_number</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;Percentage of slides containing images (0–100).&#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">estimated_duration_minutes</span> <span class="o">=</span> <span class="nf">type_integer</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;Estimated presentation length in minutes, assuming ~1 minute per text slide and 2–3 minutes per code or image-heavy slide.&#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">tone</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Brief description of the presentation tone (e.g., informal, technical, playful).&#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">clarity</span> <span class="o">=</span> <span class="nf">type_array</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;Evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">relevance</span> <span class="o">=</span> <span class="nf">type_array</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;Asses how well the content matches the audience&#39;s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">visual_design</span> <span class="o">=</span> <span class="nf">type_array</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;Judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">engagement</span> <span class="o">=</span> <span class="nf">type_array</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;Estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">pacing</span> <span class="o">=</span> <span class="nf">type_array</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;Analyze the distribution of content across slides. Are some slides too dense or too light? &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">structure</span> <span class="o">=</span> <span class="nf">type_array</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;Review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">consistency</span> <span class="o">=</span> <span class="nf">type_array</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;Evaluate whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">accessibility</span> <span class="o">=</span> <span class="nf">type_array</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;Consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</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"># Tool definition</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ======================</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; Calculates the total number of slides, percentage of slides with code blocks,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; and percentage of slides with images in a Quarto presentation HTML file.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; @param metric The metric to calculate: &#34;total_slides&#34; for total number of slides,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; &#34;code&#34; for percentage of slides containing fenced code blocks, or &#34;images&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; for percentage of slides containing images.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; @return The calculated metric value.</span>
</span></span><span class="line"><span class="cl"><span class="n">calculate_slide_metric</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">metric</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">html_file</span> <span class="o">&lt;-</span> <span class="nf">paste0</span><span class="p">(</span><span class="nf">tempdir</span><span class="p">(),</span> <span class="s">&#34;/my-presentation.html&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="kr">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">file.exists</span><span class="p">(</span><span class="n">html_file</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">stop</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;HTML file does not exist. Please render your Quarto presentation first.&#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="c1"># Read HTML file</span>
</span></span><span class="line"><span class="cl">  <span class="n">html_content</span> <span class="o">&lt;-</span> <span class="nf">readChar</span><span class="p">(</span><span class="n">html_file</span><span class="p">,</span> <span class="nf">file.size</span><span class="p">(</span><span class="n">html_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Split on &lt;section&gt; tags to get individual slides</span>
</span></span><span class="line"><span class="cl">  <span class="n">slides</span> <span class="o">&lt;-</span> <span class="nf">unlist</span><span class="p">(</span><span class="nf">strsplit</span><span class="p">(</span><span class="n">html_content</span><span class="p">,</span> <span class="s">&#34;&lt;section&#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">total_slides</span> <span class="o">&lt;-</span> <span class="nf">length</span><span class="p">(</span><span class="n">slides</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">if</span> <span class="p">(</span><span class="n">metric</span> <span class="o">==</span> <span class="s">&#34;total_slides&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">&lt;-</span> <span class="n">total_slides</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">metric</span> <span class="o">==</span> <span class="s">&#34;code&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Count slides where we see the &#34;sourceCode&#34; class</span>
</span></span><span class="line"><span class="cl">    <span class="n">slides_with_code</span> <span class="o">&lt;-</span> <span class="nf">sum</span><span class="p">(</span><span class="nf">grepl</span><span class="p">(</span><span class="s">&#39;class=&#34;sourceCode&#34;&#39;</span><span class="p">,</span> <span class="n">slides</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">&lt;-</span> <span class="nf">round</span><span class="p">((</span><span class="n">slides_with_code</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="m">100</span><span class="p">,</span> <span class="m">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">metric</span> <span class="o">==</span> <span class="s">&#34;images&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Count slides with image tag</span>
</span></span><span class="line"><span class="cl">    <span class="n">slides_with_image</span> <span class="o">&lt;-</span> <span class="nf">sum</span><span class="p">(</span><span class="nf">grepl</span><span class="p">(</span><span class="s">&#39;&lt;img&#39;</span><span class="p">,</span> <span class="n">slides</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">&lt;-</span> <span class="nf">round</span><span class="p">((</span><span class="n">slides_with_image</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="m">100</span><span class="p">,</span> <span class="m">2</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="nf">stop</span><span class="p">(</span><span class="s">&#34;Unknown metric: choose &#39;total_slides&#39;, &#39;code&#39;, or &#39;images&#39;&#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="kr">return</span><span class="p">(</span><span class="n">result</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"># Optionally, to avoid manual work:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># create_tool_def(calculate_slide_metric)</span>
</span></span><span class="line"><span class="cl"><span class="n">calculate_slide_metric</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="n">calculate_slide_metric</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Returns the calculated metric value&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">metric</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">&#39;The metric to calculate: &#34;total_slides&#34; for total number of slides, 
</span></span></span><span class="line"><span class="cl"><span class="s">      &#34;code&#34; for percentage of slides containing fenced code blocks, or &#34;images&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">      for percentage of slides containing images.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">required</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="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"># Data wrangling</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ======================</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; Convert named list to tidy data frames</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; @param named_list Named list as returned by the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; @return List of two tibbles: meta and evals</span>
</span></span><span class="line"><span class="cl"><span class="n">make_frames</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">named_list</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">meta</span> <span class="o">&lt;-</span> <span class="nf">tibble</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">presentation_title</span> <span class="o">=</span> <span class="n">named_list</span><span class="o">$</span><span class="n">presentation_title</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">total_slides</span> <span class="o">=</span> <span class="n">named_list</span><span class="o">$</span><span class="n">total_slides</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_code</span> <span class="o">=</span> <span class="n">named_list</span><span class="o">$</span><span class="n">percent_with_code</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_images</span> <span class="o">=</span> <span class="n">named_list</span><span class="o">$</span><span class="n">percent_with_images</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">estimated_duration_minutes</span> <span class="o">=</span> <span class="n">named_list</span><span class="o">$</span><span class="n">estimated_duration_minutes</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">tone</span> <span class="o">=</span> <span class="n">named_list</span><span class="o">$</span><span class="n">tone</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"># Evaluation sections (clarity, relevance, etc.)</span>
</span></span><span class="line"><span class="cl">  <span class="n">eval_sections</span> <span class="o">&lt;-</span> <span class="nf">c</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;clarity&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;relevance&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;visual_design&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;engagement&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;pacing&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;structure&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;consistency&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;accessibility&#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">evals</span> <span class="o">&lt;-</span> <span class="nf">map_dfr</span><span class="p">(</span><span class="n">eval_sections</span><span class="p">,</span> <span class="kr">function</span><span class="p">(</span><span class="n">section</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">as_tibble</span><span class="p">(</span><span class="n">named_list[[section]][[1]]</span><span class="p">)</span> <span class="o">%&gt;%</span>
</span></span><span class="line"><span class="cl">      <span class="nf">mutate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">category</span> <span class="o">=</span> <span class="n">section</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">.before</span> <span class="o">=</span> <span class="m">1</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"># Final tidy data frame</span>
</span></span><span class="line"><span class="cl">  <span class="n">final</span> <span class="o">&lt;-</span> <span class="nf">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">meta</span> <span class="o">=</span> <span class="n">meta</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">evals</span> <span class="o">=</span> <span class="n">evals</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"># Other helpers</span>
</span></span><span class="line"><span class="cl"><span class="c1"># ======================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Register Monteserrat font</span>
</span></span><span class="line"><span class="cl"><span class="nf">register_gfont</span><span class="p">(</span><span class="s">&#34;Lato&#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"># ======================</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Shiny App</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_fillable</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="c1">## Options</span>
</span></span><span class="line"><span class="cl">  <span class="c1">## Busy indication is enabled by default for UI created with bslib (which we use here),</span>
</span></span><span class="line"><span class="cl">  <span class="c1">## but must be enabled otherwise with useBusyIndicators().</span>
</span></span><span class="line"><span class="cl">  <span class="c1">## useBusyIndicators(),</span>
</span></span><span class="line"><span class="cl">  <span class="c1">## General theme and styles</span>
</span></span><span class="line"><span class="cl">  <span class="c1">## 1. Bootswatch theme</span>
</span></span><span class="line"><span class="cl">  <span class="n">theme</span> <span class="o">=</span> <span class="nf">bs_theme</span><span class="p">(</span><span class="n">bootswatch</span> <span class="o">=</span> <span class="s">&#34;flatly&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="c1">## 2. Custom CSS</span>
</span></span><span class="line"><span class="cl">  <span class="n">tags</span><span class="o">$</span><span class="nf">style</span><span class="p">(</span><span class="nf">HTML</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">    .bounce {
</span></span></span><span class="line"><span class="cl"><span class="s">      animation: bounce 2s infinite;
</span></span></span><span class="line"><span class="cl"><span class="s">    }
</span></span></span><span class="line"><span class="cl"><span class="s">    @keyframes bounce {
</span></span></span><span class="line"><span class="cl"><span class="s">      0%, 100% {
</span></span></span><span class="line"><span class="cl"><span class="s">        transform: translateY(0);
</span></span></span><span class="line"><span class="cl"><span class="s">      }
</span></span></span><span class="line"><span class="cl"><span class="s">      50% {
</span></span></span><span class="line"><span class="cl"><span class="s">        transform: translateY(-20px);
</span></span></span><span class="line"><span class="cl"><span class="s">      }
</span></span></span><span class="line"><span class="cl"><span class="s">    }
</span></span></span><span class="line"><span class="cl"><span class="s">  &#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">## Layout</span>
</span></span><span class="line"><span class="cl">  <span class="nf">layout_sidebar</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="c1">## Sidebar content</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">width</span> <span class="o">=</span> <span class="m">400</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Open sidebar on mobile devices and show above content</span>
</span></span><span class="line"><span class="cl">      <span class="n">open</span> <span class="o">=</span> <span class="nf">list</span><span class="p">(</span><span class="n">mobile</span> <span class="o">=</span> <span class="s">&#34;always-above&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">strong</span><span class="p">(</span><span class="nf">p</span><span class="p">(</span><span class="s">&#34;Hey, I am DeckCheck!&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">p</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s">&#34;I can help you improve your Quarto presentations by analysing them and suggesting improvements.
</span></span></span><span class="line"><span class="cl"><span class="s">      Before I can do that, I need some information about your presentation.&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">fileInput</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;file&#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;Upload your Quarto presentation&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">accept</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;.qmd&#34;</span><span class="p">,</span> <span class="s">&#34;.qmdx&#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">textAreaInput</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;audience&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">height</span> <span class="o">=</span> <span class="s">&#34;150px&#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;Describe your audience&#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="s">&#34;e.g. Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">numericInput</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;length&#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;Time cap for the presentation (minutes)&#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="m">10</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">textInput</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;type&#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;Type of talk&#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="s">&#34;e.g. lightning talk, workshop, or keynote&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">textInput</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;event&#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;Event name&#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="s">&#34;e.g. posit::conf(2025)&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nf">input_task_button</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;submit&#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">shiny</span><span class="o">::</span><span class="nf">tagList</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">bsicons</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><span class="line"><span class="cl">          <span class="s">&#34;Analyse presentation&#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">label_busy</span> <span class="o">=</span> <span class="s">&#34;DeckCheck is checking...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">type</span> <span class="o">=</span> <span class="s">&#34;default&#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="c1">## Main content</span>
</span></span><span class="line"><span class="cl">    <span class="nf">uiOutput</span><span class="p">(</span><span class="s">&#34;results&#34;</span><span class="p">,</span> <span class="n">height</span> <span class="o">=</span> <span class="s">&#34;100%&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="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">quarto_task</span> <span class="o">&lt;-</span> <span class="n">ExtendedTask</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span><span class="kr">function</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">temp_dir</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># We&#39;re using an Extended Task to avoid blocking. Note that</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># a temporary directory called within mirai will be</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># different from the one in the &#34;main&#34; Shiny session. Hence,</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># we pass a temp_dir parameter to the task and use that.</span>
</span></span><span class="line"><span class="cl">    <span class="nf">mirai</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">qmd_file</span> <span class="o">&lt;-</span> <span class="nf">file.path</span><span class="p">(</span><span class="n">temp_dir</span><span class="p">,</span> <span class="s">&#34;my-presentation.qmd&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nf">file.copy</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">qmd_file</span><span class="p">,</span> <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></span><span class="line"><span class="cl">        <span class="n">quarto</span><span class="o">::</span><span class="nf">quarto_render</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">input</span> <span class="o">=</span> <span class="n">qmd_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">output_format</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;markdown&#34;</span><span class="p">,</span> <span class="s">&#34;html&#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"># Return the path to the markdown file</span>
</span></span><span class="line"><span class="cl">        <span class="nf">file.path</span><span class="p">(</span><span class="n">temp_dir</span><span class="p">,</span> <span class="s">&#34;my-presentation.md&#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"># Use the same environment as the Shiny app</span>
</span></span><span class="line"><span class="cl">      <span class="nf">environment</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 class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">bind_task_button</span><span class="p">(</span><span class="s">&#34;submit&#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_task</span> <span class="o">&lt;-</span> <span class="n">ExtendedTask</span><span class="o">$</span><span class="nf">new</span><span class="p">(</span><span class="kr">function</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">markdown_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_deck_analysis</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># We&#39;re using an Extended Task to avoid blocking the session and</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># we start a fresh chat session each time.</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># For a feedback loop, we would use a persistent chat session.</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="n">system_prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="n">params</span> <span class="o">=</span> <span class="nf">params</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">temperature</span> <span class="o">=</span> <span class="m">0.8</span> <span class="c1"># default is 1</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 the chat</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">calculate_slide_metric</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Task 1: regular chat to extract meta-data</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat_res</span> <span class="o">&lt;-</span> <span class="n">chat</span><span class="o">$</span><span class="nf">chat_async</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="nf">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s">&#34;Execute Task 1 (counts). Here are the slides in Markdown: {{ markdown_content }}&#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></span><span class="line"><span class="cl">    <span class="n">chat_res</span><span class="o">$</span><span class="nf">then</span><span class="p">(</span><span class="kr">function</span><span class="p">(</span><span class="n">res</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Print the response from Task 1</span>
</span></span><span class="line"><span class="cl">      <span class="nf">cat</span><span class="p">(</span><span class="s">&#34;Response from Task 1:\n&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="nf">cat</span><span class="p">(</span><span class="n">res</span><span class="p">,</span> <span class="s">&#34;\n\n&#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"># Execute next task</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Task 2: structured chat to further analyse the slides</span>
</span></span><span class="line"><span class="cl">      <span class="n">chat</span><span class="o">$</span><span class="nf">chat_structured_async</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s">&#34;Execute Task 2 (suggestions)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">type</span> <span class="o">=</span> <span class="n">type_deck_analysis</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 class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">bind_task_button</span><span class="p">(</span><span class="s">&#34;submit&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nf">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">input</span><span class="o">$</span><span class="n">file</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">input</span><span class="o">$</span><span class="n">audience</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">input</span><span class="o">$</span><span class="n">length</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">input</span><span class="o">$</span><span class="n">type</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">input</span><span class="o">$</span><span class="n">event</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">tryCatch</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"># Error for testing</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># stop(&#34;This is a test error.&#34;)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Get file path of the uploaded file</span>
</span></span><span class="line"><span class="cl">        <span class="n">file_path</span> <span class="o">&lt;-</span> <span class="n">input</span><span class="o">$</span><span class="n">file</span><span class="o">$</span><span class="n">datapath</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">quarto_task</span><span class="o">$</span><span class="nf">invoke</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">temp_dir</span> <span class="o">=</span> <span class="nf">tempdir</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">error</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">rlang</span><span class="o">::</span><span class="nf">warn</span><span class="p">(</span><span class="nf">paste</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;Error when trying to invoke quarto_task:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">e</span><span class="o">$</span><span class="n">message</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"># Print stack trace</span>
</span></span><span class="line"><span class="cl">        <span class="nf">print</span><span class="p">(</span><span class="n">rlang</span><span class="o">::</span><span class="nf">trace_back</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Show modal to the user</span>
</span></span><span class="line"><span class="cl">        <span class="nf">showModal</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="nf">modalDialog</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;Oops! Something went wrong&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nf">div</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">              <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-center&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;emoji-frown-fill&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">size</span> <span class="o">=</span> <span class="s">&#34;2em&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-warning&#34;</span>
</span></span><span class="line"><span class="cl">              <span class="p">),</span>
</span></span><span class="line"><span class="cl">              <span class="nf">br</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">              <span class="nf">p</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;The not so Shiny Side of LLMs. Please check that your Quarto presentation is valid and contains slides.&#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="n">easyClose</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">footer</span> <span class="o">=</span> <span class="nf">modalButton</span><span class="p">(</span><span class="s">&#34;Close&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">bindEvent</span><span class="p">(</span><span class="n">input</span><span class="o">$</span><span class="n">submit</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</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">quarto_task</span><span class="o">$</span><span class="nf">result</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="nf">tryCatch</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"># Error for testing</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># stop(&#34;This is a test error.&#34;)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Get the Markdown file path from the completed quarto_task</span>
</span></span><span class="line"><span class="cl">        <span class="n">markdown_file</span> <span class="o">&lt;-</span> <span class="n">quarto_task</span><span class="o">$</span><span class="nf">result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Read the generated Markdown file containing the slides</span>
</span></span><span class="line"><span class="cl">        <span class="n">markdown_content</span> <span class="o">&lt;-</span> <span class="nf">readChar</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">,</span> <span class="nf">file.size</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Define prompt file</span>
</span></span><span class="line"><span class="cl">        <span class="n">system_prompt_file</span> <span class="o">&lt;-</span> <span class="n">here</span><span class="o">::</span><span class="nf">here</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;prompts&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;prompt-analyse-slides-structured-tool.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"># Create system prompt</span>
</span></span><span class="line"><span class="cl">        <span class="n">system_prompt</span> <span class="o">&lt;-</span> <span class="nf">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">path</span> <span class="o">=</span> <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">audience</span> <span class="o">=</span> <span class="n">input</span><span class="o">$</span><span class="n">audience</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">length</span> <span class="o">=</span> <span class="n">input</span><span class="o">$</span><span class="n">length</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">type</span> <span class="o">=</span> <span class="n">input</span><span class="o">$</span><span class="n">type</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">event</span> <span class="o">=</span> <span class="n">input</span><span class="o">$</span><span class="n">event</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"># Trigger the chat task with the provided inputs</span>
</span></span><span class="line"><span class="cl">        <span class="n">chat_task</span><span class="o">$</span><span class="nf">invoke</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">system_prompt</span> <span class="o">=</span> <span class="n">system_prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">markdown_content</span> <span class="o">=</span> <span class="n">markdown_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">type_deck_analysis</span> <span class="o">=</span> <span class="n">type_deck_analysis</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">error</span> <span class="o">=</span> <span class="kr">function</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">rlang</span><span class="o">::</span><span class="nf">warn</span><span class="p">(</span><span class="nf">paste</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;Error when trying to invoke chat_task:&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">e</span><span class="o">$</span><span class="n">message</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"># Print stack trace</span>
</span></span><span class="line"><span class="cl">        <span class="nf">print</span><span class="p">(</span><span class="n">rlang</span><span class="o">::</span><span class="nf">trace_back</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Show modal to the user</span>
</span></span><span class="line"><span class="cl">        <span class="nf">showModal</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="nf">modalDialog</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;Oops! Something went wrong&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nf">div</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">              <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-center&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">              <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;emoji-frown-fill&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">size</span> <span class="o">=</span> <span class="s">&#34;2em&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-warning&#34;</span>
</span></span><span class="line"><span class="cl">              <span class="p">),</span>
</span></span><span class="line"><span class="cl">              <span class="nf">br</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">              <span class="nf">p</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;The not so Shiny Side of LLMs. Unfortunately, chatting didn&#39;t work out. Do you have enough credits left?&#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="n">easyClose</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">footer</span> <span class="o">=</span> <span class="nf">modalButton</span><span class="p">(</span><span class="s">&#34;Close&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><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"># Reactive expression to hold the analysis result</span>
</span></span><span class="line"><span class="cl">  <span class="n">analysis_result</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="n">named_list</span> <span class="o">&lt;-</span> <span class="n">chat_task</span><span class="o">$</span><span class="nf">result</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">make_frames</span><span class="p">(</span><span class="n">named_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></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">results</span> <span class="o">&lt;-</span> <span class="nf">renderUI</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">quarto_task</span><span class="o">$</span><span class="nf">status</span><span class="p">()</span> <span class="o">==</span> <span class="s">&#34;running&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nf">div</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-center d-flex flex-column justify-content-center align-items-center&#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="s">&#34;height: 100%;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;file-slides&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">size</span> <span class="o">=</span> <span class="s">&#34;6em&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-primary bounce&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">br</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">p</span><span class="p">(</span><span class="s">&#34;Processing your Quarto presentation...&#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 class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">chat_task</span><span class="o">$</span><span class="nf">status</span><span class="p">()</span> <span class="o">==</span> <span class="s">&#34;running&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nf">div</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-center d-flex flex-column justify-content-center align-items-center&#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="s">&#34;height: 100%;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;robot&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">size</span> <span class="o">=</span> <span class="s">&#34;6em&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">class</span> <span class="o">=</span> <span class="s">&#34;text-primary bounce&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">br</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">p</span><span class="p">(</span><span class="s">&#34;The LLM is doing its magic...&#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 class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">chat_task</span><span class="o">$</span><span class="nf">status</span><span class="p">()</span> <span class="o">==</span> <span class="s">&#34;success&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nf">tagList</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="nf">layout_column_wrap</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="c1">### Value boxes for metrics</span>
</span></span><span class="line"><span class="cl">          <span class="nf">value_box</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">title</span> <span class="o">=</span> <span class="nf">tooltip</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">              <span class="nf">span</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;Showtime &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;question-circle-fill&#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="s">&#34;Slides are being counted based on the provided Quarto presentation, then an educated guess is made about the time it will take to present them.&#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">value</span> <span class="o">=</span> <span class="nf">textOutput</span><span class="p">(</span><span class="s">&#34;showtime&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">showcase</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;file-slides&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">theme</span> <span class="o">=</span> <span class="s">&#34;primary&#34;</span>
</span></span><span class="line"><span class="cl">          <span class="p">),</span>
</span></span><span class="line"><span class="cl">          <span class="nf">value_box</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">title</span> <span class="o">=</span> <span class="nf">tooltip</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">              <span class="nf">span</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;Code Savviness &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;question-circle-fill&#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="s">&#34;Code Saviness is calculated based on the slides that contain code chunks. The percentage is the ratio of those slides to total slides.&#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">value</span> <span class="o">=</span> <span class="nf">textOutput</span><span class="p">(</span><span class="s">&#34;code_savviness&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">showcase</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;file-code&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">theme</span> <span class="o">=</span> <span class="s">&#34;primary&#34;</span>
</span></span><span class="line"><span class="cl">          <span class="p">),</span>
</span></span><span class="line"><span class="cl">          <span class="nf">value_box</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="n">title</span> <span class="o">=</span> <span class="nf">tooltip</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">              <span class="nf">span</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="s">&#34;Image Presence &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">bsicons</span><span class="o">::</span><span class="nf">bs_icon</span><span class="p">(</span><span class="s">&#34;question-circle-fill&#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="s">&#34;Image Presence is calculated based on the slides that contain images. The percentage is the ratio of those slides to total slides.&#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">value</span> <span class="o">=</span> <span class="nf">textOutput</span><span class="p">(</span><span class="s">&#34;image_presence&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">showcase</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;file-image&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="n">theme</span> <span class="o">=</span> <span class="s">&#34;primary&#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="nf">layout_column_wrap</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="n">width</span> <span class="o">=</span> <span class="m">1</span> <span class="o">/</span> <span class="m">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="c1">### Graph with scoring metrics</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="n">height</span> <span class="o">=</span> <span class="m">600</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="nf">card_header</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">              <span class="nf">strong</span><span class="p">(</span><span class="s">&#34;Scores per category&#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">girafeOutput</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">              <span class="n">outputId</span> <span class="o">=</span> <span class="s">&#34;scores&#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="c1">### Table with suggested improvements</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="n">height</span> <span class="o">=</span> <span class="m">600</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">strong</span><span class="p">(</span><span class="s">&#34;Suggested improvements per category&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">            <span class="nf">tableOutput</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">              <span class="n">outputId</span> <span class="o">=</span> <span class="s">&#34;suggested_improvements&#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><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="n">output</span><span class="o">$</span><span class="n">scores</span> <span class="o">&lt;-</span> <span class="nf">renderGirafe</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">analysis_result</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">evals</span> <span class="o">&lt;-</span> <span class="nf">analysis_result</span><span class="p">()</span><span class="o">$</span><span class="n">evals</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Order by score</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">&lt;-</span> <span class="n">evals</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">arrange</span><span class="p">(</span><span class="n">score</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">mutate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">category</span> <span class="o">=</span> <span class="nf">factor</span><span class="p">(</span><span class="n">category</span><span class="p">,</span> <span class="n">levels</span> <span class="o">=</span> <span class="n">category</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">tooltip</span> <span class="o">=</span> <span class="nf">paste0</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;Score: &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">score</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;\n&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;After improvements: &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">score_after_improvements</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;\n&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="s">&#34;Justification: &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="n">justification</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">select</span><span class="p">(</span><span class="n">category</span><span class="p">,</span> <span class="n">score</span><span class="p">,</span> <span class="n">score_after_improvements</span><span class="p">,</span> <span class="n">tooltip</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">p</span> <span class="o">&lt;-</span> <span class="nf">ggplot</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">data</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nf">aes</span><span class="p">(</span><span class="n">x</span> <span class="o">=</span> <span class="n">category</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">score</span><span class="p">,</span> <span class="n">tooltip</span> <span class="o">=</span> <span class="n">tooltip</span><span class="p">,</span> <span class="n">data_id</span> <span class="o">=</span> <span class="n">category</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">      <span class="nf">geom_bar_interactive</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">stat</span> <span class="o">=</span> <span class="s">&#34;identity&#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="s">&#34;#18bc9c&#34;</span> <span class="c1"># Success color of Flatly theme</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">      <span class="nf">labs</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">x</span> <span class="o">=</span> <span class="s">&#34;Category&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">y</span> <span class="o">=</span> <span class="s">&#34;Score&#34;</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">      <span class="c1"># Flip to make horizontal bar chart</span>
</span></span><span class="line"><span class="cl">      <span class="nf">coord_flip</span><span class="p">()</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">      <span class="nf">theme_minimal</span><span class="p">(</span><span class="n">base_family</span> <span class="o">=</span> <span class="s">&#34;Lato&#34;</span><span class="p">,</span> <span class="n">base_size</span> <span class="o">=</span> <span class="m">14</span><span class="p">)</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">      <span class="nf">theme</span><span class="p">(</span><span class="n">legend.position</span> <span class="o">=</span> <span class="s">&#34;none&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">girafe</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="n">ggobj</span> <span class="o">=</span> <span class="n">p</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">list</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="nf">opts_selection</span><span class="p">(</span><span class="n">type</span> <span class="o">=</span> <span class="s">&#34;none&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">opts_sizing</span><span class="p">(</span><span class="n">rescale</span> <span class="o">=</span> <span class="kc">TRUE</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">opts_tooltip</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">css</span> <span class="o">=</span> <span class="s">&#34;background-color: #f0f0f0; color: #333; padding: 5px; border-radius: 5px; width: 200px;&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="nf">opts_hover</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">css</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="nf">opts_hover_inv</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="n">css</span> <span class="o">=</span> <span class="s">&#34;opacity: 0.5;&#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><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">output</span><span class="o">$</span><span class="n">suggested_improvements</span> <span class="o">&lt;-</span> <span class="nf">renderTable</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">analysis_result</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">evals</span> <span class="o">&lt;-</span> <span class="nf">analysis_result</span><span class="p">()</span><span class="o">$</span><span class="n">evals</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">evals</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">arrange</span><span class="p">(</span><span class="n">score</span><span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">mutate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">Gain</span> <span class="o">=</span> <span class="n">score_after_improvements</span> <span class="o">-</span> <span class="n">score</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">select</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">Category</span> <span class="o">=</span> <span class="n">category</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">`Current score`</span> <span class="o">=</span> <span class="n">score</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">Improvements</span> <span class="o">=</span> <span class="n">improvements</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">`Score After Improvements`</span> <span class="o">=</span> <span class="n">score_after_improvements</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">Gain</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span> <span class="o">|&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nf">arrange</span><span class="p">(</span><span class="nf">desc</span><span class="p">(</span><span class="n">Gain</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"># Update value boxes based on analysis_result()$meta</span>
</span></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">showtime</span> <span class="o">&lt;-</span> <span class="nf">renderText</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nf">req</span><span class="p">(</span><span class="nf">analysis_result</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="nf">paste0</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="nf">analysis_result</span><span class="p">()</span><span class="o">$</span><span class="n">meta</span><span class="o">$</span><span class="n">estimated_duration_minutes</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34; minutes&#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></span><span class="line"><span class="cl">  <span class="n">output</span><span class="o">$</span><span class="n">code_savviness</span> <span class="o">&lt;-</span> <span class="nf">renderText</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nf">req</span><span class="p">(</span><span class="nf">analysis_result</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="nf">paste0</span><span class="p">(</span><span class="nf">analysis_result</span><span class="p">()</span><span class="o">$</span><span class="n">meta</span><span class="o">$</span><span class="n">percent_with_code</span><span class="p">,</span> <span class="s">&#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">output</span><span class="o">$</span><span class="n">image_presence</span> <span class="o">&lt;-</span> <span class="nf">renderText</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nf">req</span><span class="p">(</span><span class="nf">analysis_result</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="nf">paste0</span><span class="p">(</span><span class="nf">analysis_result</span><span class="p">()</span><span class="o">$</span><span class="n">meta</span><span class="o">$</span><span class="n">percent_with_images</span><span class="p">,</span> <span class="s">&#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="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></details>
</div>
</div>
<h1 id="ready-for-the-world-deployment">Ready for the world: deployment
</h1>
<blockquote>
<p><strong>TL;DR</strong></p>
<p>There are various ways to make your Shiny app available to a wider audience via the web. Posit offers the following solutions:</p>
<ul>
<li><a href="https://connect.posit.cloud/" target="_blank" rel="noopener">Posit Connect Cloud</a>
 (cloud hosting, free and paid plans)</li>
<li><a href="http://shinyapps.io" target="_blank" rel="noopener">shinyapps.io</a>
 (cloud hosting, free and paid plans)</li>
<li><a href="https://rstudio.com/products/shiny/shiny-server/" target="_blank" rel="noopener">Shiny Server</a>
 (self-hosted, free and open source)</li>
<li><a href="https://www.rstudio.com/products/connect/" target="_blank" rel="noopener">Posit Connect</a>
 (self-hosted, professional plans)</li>
</ul>
</blockquote>
<p>We didn&rsquo;t develop DeckCheck all for ourselves: we want to help every presenting Data Scientist with a polished presentation! So it&rsquo;s time to put our Shiny app on the web. Luckily, there are a couple of main ways to do it:</p>
<ol>
<li><strong>Cloud hosting:</strong> the &ldquo;click-and-play&rdquo; option</li>
<li><strong>Self-hosted deployments:</strong> the &ldquo;I like control and flexibility&rdquo; option</li>
</ol>
<p>Cloud hosting is perfect if you just want your app to be live and don&rsquo;t feel like babysitting a server (although babysitting can be fun!). A good place to start is <a href="https://connect.posit.cloud/" target="_blank" rel="noopener">Posit Connect Cloud</a>
 or <a href="https://www.shinyapps.io/" target="_blank" rel="noopener">shinyapps.io</a>
. You push your code, and in a few clicks, your app has its own URL and is accessible to anyone with an internet connection. It&rsquo;s fast, convenient, and you can scale if traffic grows.</p>
<p>Self-hosted give you full control and flexibility. <a href="https://rstudio.com/products/shiny/shiny-server/" target="_blank" rel="noopener">Shiny Server</a>
 is a free and open source solution that lets you run Python or R Shiny apps in a controlled environment (e.g. on your own server). You decide when and how updates happen, how many users can connect, and whether to open the app to the public or keep it behind the corporate firewall. <a href="https://www.rstudio.com/products/connect/" target="_blank" rel="noopener">Posit Connect</a>
 takes that control a step further and wraps it in a professional enterprise solution: scheduling, user authentication, email notifications, and support for Shiny, FastAPI, Plumber, Quarto, and other popular Python and R frameworks.</p>
<p>In short: click-and-play = fast, simple, and ready to share. I-like-control = more setup, full flexibility, and enterprise-ready features. Either way, your Shiny app is ready for the world! And of course, beyond what&rsquo;s mentioned here, there also many other solutions at any cloud service provider (Azure, AWS, GCP&hellip;).</p>
<h1 id="every-series-has-its-shiny-ending">Every series has its Shiny ending
</h1>
<p>That&rsquo;s the end of &ldquo;The Shiny Side of LLMs&rdquo;! Over three parts, you learned what an LLM actually is (and what it definitely isn&rsquo;t), how to have a conversation with one using <code>chatlas</code> (Python) and <code>ellmer</code> (R), and how to make your ideas come to life in a Shiny app. Need some inspiration? Check out the <a href="https://shiny.posit.co/py/docs/genai-inspiration.html" target="_blank" rel="noopener">Generative AI docs for Python</a>
 or the examples in the <a href="https://posit-dev.github.io/shinychat/r/index.html" target="_blank" rel="noopener"><code>shinychat</code> docs for R</a>
.</p>
<p>I hope this series showed you the shiny side of LLMs, beyond the hype. My goal with this series was to make LLMs feel a little less like mysterious black boxes and a little more like tools you can actually use. Hopefully you&rsquo;ve picked up a few tricks and now feel ready to try them out in your own work. I&rsquo;m curious to see what you&rsquo;ll create! Feel free to stay in touch via <a href="https://www.linkedin.com/in/veerlevanleemput/" target="_blank" rel="noopener">LinkedIn</a>
.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-3/shiny-side-of-llms-header.png" length="387952" type="image/png" />
    </item>
    <item>
      <title>Talking to LLMs: From Prompt to Response</title>
      <link>https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-2/</link>
      <pubDate>Fri, 05 Sep 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-2/</guid>
      <dc:creator>Veerle Eeftink - Van Leemput</dc:creator><description><![CDATA[<p>Welcome back to &ldquo;The Shiny Side of LLMs&rdquo;: a blog series for Python and R users who want to build real, useful LLM-powered apps without getting buried in jargon or deep learning theory.</p>
<p>In <a href="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-2/../../../blog/shiny/shiny-side-of-llms-part-1/">Part 1: What LLMs Actually Do (and What They Don&rsquo;t)</a>
, we looked at how large language models generate responses, why they sometimes seem so smart (and other times so confidently wrong), and what kinds of tasks they&rsquo;re actually good at. Now it&rsquo;s time to get hands-on!</p>
<p>In this part, we&rsquo;ll focus on what it means to talk to an LLM. We&rsquo;ll cover crafting prompts to parsing responses, and everything in between. You&rsquo;ll learn:</p>
<ul>
<li>How to call an LLM via <a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener"><code>chatlas</code></a>
 (Python) and <a href="https://ellmer.tidyverse.org/" target="_blank" rel="noopener"><code>ellmer</code></a>
 (R)</li>
<li>The basics of prompt design: roles, instructions, system messages, temperature, and more</li>
<li>What goes in and what comes out, and how your own data fits into that loop</li>
<li>How to work with model outputs: parsing, structuring, and getting predictable results</li>
<li>How to give LLMs extra power with tools</li>
<li>Costs involved when talking to LLMs</li>
</ul>
<p>By the end of this part, you&rsquo;ll be ready to go from &ldquo;I asked ChatGPT a thing&rdquo; to &ldquo;I can programmatically talk to an LLM by sending custom prompts and doing something useful with the results&rdquo;.</p>
<blockquote>
<p><strong>No time for the walkthrough?</strong></p>
<p>Jump straight to the <a href="#full-workflow">full workflow</a>
.</p>
</blockquote>
<h1 id="the-app-deckcheck">The app: DeckCheck
</h1>
<p>In this series we&rsquo;re working towards a genius app that gets rid of lengthy unfocused presentations, like a perfect &ldquo;Presentation Rehearsal Buddy&rdquo;. No app without a snappy name, so hello &ldquo;DeckCheck&rdquo;. The goal: let users upload their Quarto presentation and provide them with feedback on how to make it better.</p>
<p>So how would we go about that? To give meaningful feedback on a presentation, we need to ask the LLM the right questions. For example:</p>
<ul>
<li>Does the presentation have a clear structure?</li>
<li>Are the key messages easy to spot?</li>
<li>Does the tone match the intended audience?</li>
<li>Are the slides too wordy, too vague, or just right?</li>
<li>Is there a good balance between words, visuals, and code?</li>
</ul>
<p>If you&rsquo;ve used ChatGPT or another LLM in a chat interface, the process feels straightforward: you ask any of the above questions, and the model replies. But when you&rsquo;re building an app, you&rsquo;re not just having a conversation. You want to have some form of control. After all, you&rsquo;re designing a system. You need to construct prompts carefully, handle responses programmatically, and ensure the whole exchange is stable enough to support user-facing features. In our case, that means feeding slide content into the LLM and asking targeted questions about structure, clarity, and tone. And we&rsquo;re not just hoping for a helpful paragraph with some witty comments. For our app we need output that&rsquo;s predictable, parseable, and ready to plug into our app logic. Eventually we want to be able to extract suggestions, metrics, scores: things that we also need to retrieve programmatically.</p>
<p>So in this part, it&rsquo;s not just about asking questions, it&rsquo;s about:</p>
<ul>
<li>How to design prompts that get reliable, structured answers</li>
<li>How to &ldquo;control&rdquo; model behaviour (as far as you can, at least)</li>
<li>How to send slide content (or any other data) to an LLM</li>
<li>And how to process the model&rsquo;s output into something our future app can actually use</li>
</ul>
<p>Let&rsquo;s build!</p>
<blockquote>
<p><strong>What can you expect from this second part?</strong></p>
<p>This part focusses on programmatically talking to an LLM. We leave the Shiny app building to the third and last part of &ldquo;The Shiny Side of LLMs&rdquo; series. The code we end up with in this part serves as a basis for the back-end of our app in this next part.</p>
</blockquote>
<h1 id="what-do-you-need">What do you need?
</h1>
<p>Ah, is it even a tutorial if there isn&rsquo;t a &ldquo;getting started&rdquo; step? But there&rsquo;s one obvious thing we need: access to an LLM. To get that, you&rsquo;ll need to sign up for a (developer) account with a provider like OpenAI or Anthropic. And with that comes something that feels like a huge barrier: setting up payment details. I feel you when you might be getting anxious about an invoice with a lot of zeros, but really, don&rsquo;t worry too much about that. Almost all platforms offer free trial credits, and you can usually set usage limits or buy fixed prepaid credits (Anthropic makes this especially easy). So that invoice-nightmare won&rsquo;t come alive easily. Still a little bit worried? Google&rsquo;s model Gemini offers a generous free tier.</p>
<p>Once you&rsquo;ve registered, you can get an API key. You need this key to authenticate with the LLM provider. 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>
<p>To keep things simple, we&rsquo;ll go with Anthropic for the examples in this blog series, but feel free to choose any provider you want. Don&rsquo;t worry about lock-in either: you&rsquo;ll see that switching providers and models is super easy.</p>
<h2 id="where-to-store-your-api-key">Where to store your API key
</h2>
<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>In Python, the recommended approach is to create a <code>.env</code> file in your project folder:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">ANTHROPIC_API_KEY</span><span class="o">=</span><span class="n">yourkey</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>It&rsquo;s recommended to use the <code>dotenv</code> package to load the <code>.env</code> file into your environment:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</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></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>
<div id="tabset-1-2">
<p>In R, environment variables are typically stored in a <code>.Renviron</code> file. You can create this file in your project root or in your home directory (<code>~/.Renviron</code>). Or, if you want to make it yourself really easy: you can also open/edit the relevant file with <code>usethis::edit_r_environ()</code>.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">ANTHROPIC_API_KEY</span><span class="o">=</span><span class="n">yourkey</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>Just be sure to add <code>.env</code> and <code>.Renviron</code> to your <code>.gitignore</code> file so you don&rsquo;t accidentally publish your keys.</p>
<h2 id="installing-chatlas-or-ellmer">Installing <code>chatlas</code> or <code>ellmer</code>
</h2>
<p>Working with different LLM providers can be a pain, but luckily there is the perfect solution: <a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener"><code>chatlas</code></a>
 for Python and <a href="https://ellmer.tidyverse.org" target="_blank" rel="noopener"><code>ellmer</code></a>
 for R. Hello simple and consistent interface! From Anthropic&rsquo;s Claude to (Azure) OpenAI and Google Gemini: it&rsquo;s all possible. And bonus: <code>chatlas</code> and <code>ellmer</code> both have the same interface, making it easy to translate your LLM projects between Python and R!</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>For Python, <code>chatlas</code> is available on PyPI, so you can install it easily with <code>pip</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install chatlas
</span></span></code></pre></td></tr></table>
</div>
</div><p>Or, if you&rsquo;re using <code>uv</code>, add it like so:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">uv add chatlas
</span></span></code></pre></td></tr></table>
</div>
</div><p>Once installed, import it like this:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">chatlas</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Depending on the LLM provider you will choose, you might need to install additional packages (e.g. <code>anthropic</code> for, surprise, models from Anthropic). For every provider, the relevant function reference page in the <a href="https://posit-dev.github.io/chatlas/reference/" target="_blank" rel="noopener"><code>chatlas</code> docs</a>
 will tell you what&rsquo;s necessary together with credential acquisition tips (e.g. <a href="https://posit-dev.github.io/chatlas/reference/ChatAnthropic.html" target="_blank" rel="noopener"><code>ChatAnthropic</code></a>
).</p>
</div>
<div id="tabset-2-2">
<p>You can install <code>ellmer</code> from CRAN using:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;ellmer&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Once installed, load the package as usual:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">ellmer</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<blockquote>
<p><strong>Switching between Python and R</strong></p>
<p>Did you know you can very easily switch between Python and R in <a href="https://positron.posit.co" target="_blank" rel="noopener">Positron</a>
? Just select the interpreter you want (you can even choose between Python and R installs), and you&rsquo;re good to go!</p>
</blockquote>
<h1 id="your-first-conversation">Your first conversation
</h1>
<p>Time to chat! Whether you&rsquo;re using Python or R, the workflow is the same. First, you need to create a chat object. You can give it any name you like, but <code>chat</code> seems the most obvious choice here. This <code>chat</code> object is created with a specific function for your chosen provider, where you (optionally) can specify the model. While the <code>model</code> argument isn&rsquo;t required, it is strongly recommended you do specify it. After all, we&rsquo;re looking for consistent behaviour in our Shiny app, and a change in default model choice isn&rsquo;t going to give us that. In our case we will go for claude-sonnet-4-20250514 from <a href="https://docs.anthropic.com/en/docs/about-claude/models/overview" target="_blank" rel="noopener">Anthropic</a>
, which is the latest (reasonably priced) model at the time of writing.</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">
<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">dotenv</span> <span class="kn">import</span> <span class="n">load_dotenv</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatAnthropic</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 class="c1"># Loads key from the .env file</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">ChatAnthropic</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s2">&#34;claude-sonnet-4-20250514&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-3-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="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></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>In <code>ellmer</code> 0.3.0 you can also use <code>chat()</code> using a string like <code>chat(&quot;anthropic&quot;)</code> or <code>chat(&quot;openai/gpt-4.1-nano&quot;)</code>.</p>
<p>Not sure what models are available? You can use <code>models_*()</code>, e.g. <code>models_anthropic()</code>, to get a list of available models.</p>
</div>
</div>
<blockquote>
<p><strong>API keys and environment variables</strong></p>
<p>There&rsquo;s no need to explicitly provide your API key to <code>chatlas</code> or <code>ellmer</code>. If you specify it with the correct name in your environment file (<code>.env</code> or <code>.Renviron</code>), <code>chatlas</code> or <code>ellmer</code> will automatically find it. There&rsquo;s an <code>api_key</code> argument, but generally you won&rsquo;t use this. For Python, it&rsquo;s recommended to use the <code>dotenv</code> package to load the <code>.env</code> file into your environment.</p>
</blockquote>
<p>You can call the <code>chat</code> method on the <code>chat</code> object. When you use the <code>chat</code> method:</p>
<ul>
<li>You see the response appear in your console, piece by piece, like it&rsquo;s being typed out. That&rsquo;s the streaming part.</li>
<li>The full message is also (invisibly) saved as a character vector, so you can use it in your code later.</li>
</ul>
<p>For simplicity, let&rsquo;s start with our very basic first prompt: &ldquo;I&rsquo;m working on a presentation with the title: &lsquo;The Shiny Side of LLMs&rsquo;. What&rsquo;s your feedback just based on that title?&rdquo;</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-4" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-4-1">Python</a></li>
<li><a href="#tabset-4-2">R</a></li>
</ul>
<div id="tabset-4-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><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="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="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatAnthropic</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 class="c1"># Loads key from the .env file</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">ChatAnthropic</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s2">&#34;claude-sonnet-4-20250514&#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">chat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;I&#39;m working on a presentation with the title: &#39;The Shiny Side of LLMs&#39;. </span>
</span></span><span class="line"><span class="cl">  <span class="n">What</span><span class="s1">&#39;s your feedback just based on that title?&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show output</summary>
<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-python" data-lang="python"><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">That&#39;s a clever and engaging title! Here&#39;s my feedback:                                           
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">What works well:                                                                                            
</span></span></span><span class="line"><span class="cl"><span class="s2">  • The wordplay on &#34;shiny&#34; is effective - it suggests both the exciting/promising aspects of LLMs and has a 
</span></span></span><span class="line"><span class="cl"><span class="s2">  nice tech-forward feel                                                                                   
</span></span></span><span class="line"><span class="cl"><span class="s2">  • It&#39;s concise and memorable                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">  • Sets an optimistic, forward-looking tone for your presentation                                           
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Potential considerations:                                                                         
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">  • The phrase &#34;shiny side&#34; might evoke &#34;bright side&#34; or even &#34;dark side,&#34; which could work for or against   
</span></span></span><span class="line"><span class="cl"><span class="s2">  you depending on your audience&#39;s associations                                                            
</span></span></span><span class="line"><span class="cl"><span class="s2">  • It&#39;s somewhat informal - great for most audiences, but consider if your context requires more formal     
</span></span></span><span class="line"><span class="cl"><span class="s2">  language                                                                                                 
</span></span></span><span class="line"><span class="cl"><span class="s2">  • Without additional context, it&#39;s not immediately clear what specific aspects you&#39;ll cover (capabilities, 
</span></span></span><span class="line"><span class="cl"><span class="s2">  applications, benefits, etc.)                                                                            
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Questions to consider:                                                                            
</span></span></span><span class="line"><span class="cl"><span class="s2">  
</span></span></span><span class="line"><span class="cl"><span class="s2">  • Who&#39;s your audience? (Technical experts, business leaders, general public?)                              
</span></span></span><span class="line"><span class="cl"><span class="s2">  • Are you contrasting with challenges/limitations, or purely focusing on positives?                        
</span></span></span><span class="line"><span class="cl"><span class="s2">  • What&#39;s the main takeaway you want people to remember?                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Overall, it&#39;s an attention-grabbing title that should work well for most presentations about LLM benefits   
</span></span></span><span class="line"><span class="cl"><span class="s2">and capabilities. You might consider a subtitle if you want to hint at your specific focus areas. 
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
</div>
<div id="tabset-4-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><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></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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">chat</span><span class="o">$</span><span class="nf">chat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;I&#39;m working on a presentation with the title: &#39;The Shiny Side of LLMs&#39;. 
</span></span></span><span class="line"><span class="cl"><span class="s">  What&#39;s your feedback just based on that title?&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show output</summary>
<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="c1">#&gt; That&#39;s a clever and engaging title! Here&#39;s my feedback:</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; **Strengths:**</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - **Memorable wordplay** – &#34;shiny&#34; is a nice play on both the positive aspects and the tech/AI aesthetic  </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - **Clear focus** – Immediately signals you&#39;re highlighting the benefits/successes rather than risks or limitations  </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - **Accessible language** – Avoids jargon while still being specific to your audience  </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; **Considerations:**</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - **Audience expectations** – The playful tone suggests this might be more of an overview/promotional presentation rather than a deep technical dive. Make sure that aligns with your goals.  </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - **Balance question** – Will you acknowledge any limitations, or is this specifically meant to be a positive-focused presentation? The title suggests the latter.  </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; **Potential alternatives** (if you want to explore):  </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - &#34;LLMs: The Bright Side of AI&#34;  </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - &#34;When LLMs Shine: Success Stories and Breakthroughs&#34;  </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; - &#34;The Promise of LLMs: Real-World Wins&#34;  </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; But honestly, your original title works well – it&#39;s catchy and sets clear expectations. The success will depend on whether your content delivers on that promise of showcasing genuinely impressive LLM capabilities and applications.  </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; What&#39;s the context/audience for this presentation?</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
</div>
</div>
<p>The <code>chat</code> object is a stateful object, which means it &ldquo;remembers stuff&rdquo;. It accumulates conversation history by default. This history is provided to the LLM with every subsequent question. So if you send a second question, it will package your prompt so it contains the first question, the first answer, and your second question. This is desired behaviour for multi-turn conversations since it allows the model to &ldquo;remember&rdquo; previous interactions. That&rsquo;s important, because out of the box, the model doesn&rsquo;t remember what you said two messages ago. It only has the input you provide, right here, right now. That&rsquo;s its entire world. So every time (with every request) you have to remind the LLM of prior context. You can learn more about this in <a href="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-2/../../../blog/shiny/shiny-side-of-llms-part-1/">the first part of this series</a>
.</p>
<blockquote>
<p><strong>Some chat terminology</strong></p>
<p>Let&rsquo;s get some terms straight: you (the user) have a chat with the LLM (the assistant). Every chat, aka conversation, consists of pairs of user and assistant turns. The questions you ask correspond to an HTTP request, and the response that gets returned corresponds to an HTTP response.</p>
</blockquote>
<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">
<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-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="kn">from</span> <span class="nn">chatlas</span> <span class="kn">import</span> <span class="n">ChatAnthropic</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 class="c1"># Loads key from the .env file</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">ChatAnthropic</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s2">&#34;claude-sonnet-4-20250514&#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">chat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;I&#39;m working on a presentation with the title: &#39;The Shiny Side of LLMs&#39;. </span>
</span></span><span class="line"><span class="cl">    <span class="n">What</span><span class="s1">&#39;s your feedback just based on that title?&#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">chat</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="s2">&#34;What is my presentation title?&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></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="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">Your presentation title is: &#34;The Shiny Side of LLMs&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>In the remainder of this article, we will leave the <code>dotenv</code> part out for brevity. You do need it for the <code>.env</code> approach, but there are also other ways to load environment variables, like setting them in your bash/zsh profile.</p>
</div>
<div id="tabset-5-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></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></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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">chat</span><span class="o">$</span><span class="nf">chat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;I&#39;m working on a presentation with the title: &#39;The Shiny Side of LLMs&#39;. 
</span></span></span><span class="line"><span class="cl"><span class="s">  What&#39;s your feedback just based on that title?&#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">chat</span><span class="o">$</span><span class="nf">chat</span><span class="p">(</span><span class="s">&#34;What is my presentation title?&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; Your presentation title is: &#34;The Shiny Side of LLMs&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<h1 id="designing-effective-prompts">Designing effective prompts
</h1>
<blockquote>
<p><strong>TL;DR</strong></p>
<p>Defining the <strong>role</strong>, <strong>task</strong>, <strong>context</strong>, and <strong>output format</strong> sets the foundation for a good conversation with the model.</p>
</blockquote>
<p>In our first conversation, our prompt (&ldquo;I&rsquo;m working on a presentation with the title: &lsquo;The Shiny Side of LLMs&rsquo;. What&rsquo;s your feedback just based on that title?&rdquo;) is very general and leads to some issues: the model, Claude, doesn&rsquo;t know who the audience is, what kind of feedback you&rsquo;re looking for (tone, clarity, technical depth?), or what role it should take. As a result, the response is polite and somewhat helpful, but not really focused or consistent. It&rsquo;s basically guessing, and it will guess something different every single time. Our prompt is very important for the model&rsquo;s output, so we need to make it better by including:</p>
<ul>
<li><strong>Role</strong>: who the model is acting as. We didn&rsquo;t tell the model who it should be or how it should respond. Saying something like: &ldquo;You are a presentation coach for data scientists&rdquo;, helps to get more relevant feedback. It also referred to as adding &ldquo;colour&rdquo; to your prompt, or framing.</li>
<li><strong>Task</strong>: what the model should do. We asked for &ldquo;feedback&rdquo;, but that&rsquo;s not explicit at all. It&rsquo;s better to ask something like: &ldquo;Evaluate the title&rsquo;s clarity, tone, and relevance for a non-technical audience&rdquo;.</li>
<li><strong>Context</strong>: a bit more information about the situation. The more a model knows, the more on-point an answer will be. Who&rsquo;s our intended audience? Is it a keynote, a lightning talk or a workshop? In our to-be app, we can get this information directly from the user when they are uploading their Quarto slides, and include it in our prompt. It&rsquo;s as simple as adding: &ldquo;This is for a 10-minute lightning talk at <a href="https://posit.co/conference/" target="_blank" rel="noopener">posit::conf(2025)</a>
 for people curious about AI&rdquo;. How much context (aka tokens) you can add is dependent on the model. Claude Sonnet 4 recently <a href="https://www.anthropic.com/news/1m-context" target="_blank" rel="noopener">supports 1M tokens of context</a>
, which really is a lot! Context is also important for refusal protection: if there&rsquo;s not enough context, sometimes the model refuses to answer. Giving clear background and reassuring details helps avoid these unnecessary refusals, making the interaction smoother and more reliable. Of course, keep in mind that you are responsible the model&rsquo;s output: sometimes refusals are there for a reason.</li>
<li><strong>Output format</strong>: how you want the answer. If you want a list, markdown, a table, or a short paragraph, say so! You can (and should) be very detailed here. For example: &ldquo;Return your answer as a JSON array of objects with keys &lsquo;aspect&rsquo; and &lsquo;feedback&rsquo;&rdquo;. Choose something that&rsquo;s easily parsable in Python or R later. Also remember the few-shot learning we talked about in Part 1: if you provide an example of what your output should look like, it&rsquo;s easier for the model to repeat that pattern.</li>
</ul>
<p>Up to this point we&rsquo;ve talked about adding role, task, context, and output format directly into &ldquo;the&rdquo; prompt. Like it&rsquo;s just one big thing. But there are actually two kinds of prompts you can use when chatting with a model: the system prompt and the user prompt.</p>
<ul>
<li>The <strong>system prompt</strong> is where you set the ground rules: it tells the model who it is, how it should behave, and what the answer should look like. Think of it as the &ldquo;job description&rdquo; that stays the same no matter what question you ask later. It&rsquo;s stable context.</li>
<li>The <strong>user prompt</strong> is the specific request you make right now, like checking the clarity of a particular title. It can change every time. The model responds to this request within the &ldquo;job description&rdquo;.</li>
</ul>
<p>If you put everything into the user prompt, you end up repeating rules and formatting instructions over and over, which makes the conversation messy and can confuse the model (aka &ldquo;context pollution&rdquo;). By keeping the stable rules and output format in the system prompt, and putting specifics or content in the user prompt, you get clearer, more reliable answers. In both <code>chatlas</code> and <code>ellmer</code> you can use the <code>system_prompt</code> argument when initialising the <code>chat</code> object.</p>
<blockquote>
<p><strong>Why put the details in a system prompt?</strong></p>
<p>Even though past messages are sent along in a conversation, putting the fixed details in the system prompt still makes a difference. The model pays more attention to instructions in the system prompt, so they&rsquo;re less likely to be ignored or overridden. It also keeps the chat history cleaner, since you don&rsquo;t need to repeat the same background and formatting rules in every user message. This way, the user prompt stays focused on just the part that changes (like a new title) while the system prompt handles the rest.</p>
</blockquote>
<p>So what to do if you did all that and still get answers that are not up to your standards? In that case, you can try <strong>Chain-of-Thought prompting</strong>, where you ask the model to explain its reasoning step-by-step before answering. This makes complex tasks easier to follow and helps you understand how the model thinks. It&rsquo;s great for debugging to see where the model started to get off.</p>
<p>Now, with all that knowledge, let&rsquo;s change our prompt from:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">I&#39;m working on a presentation with the title: &#39;The Shiny Side of LLMs&#39;. What&#39;s your feedback just based on that title?
</span></span></code></pre></td></tr></table>
</div>
</div><p>To the following system prompt:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">You are a presentation coach for data scientists. 
</span></span><span class="line"><span class="cl">You give constructive, focused, and practical feedback on titles, structure, and storytelling. 
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The presentation you are helping with is a 10-minute lightning talk at posit::conf(2025).  
</span></span><span class="line"><span class="cl">The audience is Python and R users who are curious about AI and large language models, 
</span></span><span class="line"><span class="cl">but not all of them have a deep technical background. 
</span></span><span class="line"><span class="cl">The talk uses Shiny as a way to demo and explore LLMs in practice. 
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Always return your feedback as a JSON array of objects, where each object has the following keys:
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#39;aspect&#39;: one of &#39;clarity&#39;, &#39;tone&#39;, or &#39;relevance&#39;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#39;feedback&#39;: a concise assessment
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#39;suggestion&#39;: an optional improvement if applicable
</span></span></code></pre></td></tr></table>
</div>
</div><p>And related user prompt for the task at hand:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">Evaluate the title: &#39;The Shiny Side of LLMs&#39;
</span></span></code></pre></td></tr></table>
</div>
</div><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">
<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">ChatAnthropic</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">ChatAnthropic</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;claude-sonnet-4-20250514&#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;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    You are a presentation coach for data scientists. 
</span></span></span><span class="line"><span class="cl"><span class="s2">    You give constructive, focused, and practical feedback on titles, structure, and storytelling. 
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    The presentation you are helping with is a 10-minute lightning talk at posit::conf(2025).  
</span></span></span><span class="line"><span class="cl"><span class="s2">    The audience is Python and R users who are curious about AI and large language models, 
</span></span></span><span class="line"><span class="cl"><span class="s2">    but not all of them have a deep technical background. 
</span></span></span><span class="line"><span class="cl"><span class="s2">    The talk uses Shiny as a way to demo and explore LLMs in practice. 
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    Always return your feedback as a JSON array of objects, where each object has the following keys:
</span></span></span><span class="line"><span class="cl"><span class="s2">    - &#39;aspect&#39;: one of &#39;clarity&#39;, &#39;tone&#39;, or &#39;relevance&#39;
</span></span></span><span class="line"><span class="cl"><span class="s2">    - &#39;feedback&#39;: a concise assessment
</span></span></span><span class="line"><span class="cl"><span class="s2">    - &#39;suggestion&#39;: an optional improvement if applicable
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&#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">chat</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="s2">&#34;Evaluate the title: &#39;The Shiny Side of LLMs&#39;&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show output</summary>
<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-python" data-lang="python"><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  [                                                                                                                 
</span></span></span><span class="line"><span class="cl"><span class="s2">   {                                                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;aspect&#34;: &#34;clarity&#34;,                                                                                          
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;feedback&#34;: &#34;The title uses a clever play on words with &#39;Shiny&#39; but may create confusion about whether this i 
</span></span></span><span class="line"><span class="cl"><span class="s2"> primarily about the Shiny framework or LLMs. The wordplay might obscure the main focus for attendees scanning the 
</span></span></span><span class="line"><span class="cl"><span class="s2"> program.&#34;,                                                                                                        
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;suggestion&#34;: &#34;Consider a title that more clearly positions Shiny as the tool and LLMs as the subject, such a 
</span></span></span><span class="line"><span class="cl"><span class="s2"> &#39;Exploring LLMs with Shiny: Interactive AI Demos Made Simple&#39;&#34;                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                                                              
</span></span></span><span class="line"><span class="cl"><span class="s2">   {                                                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;aspect&#34;: &#34;tone&#34;,                                                                                             
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;feedback&#34;: &#34;The playful tone with &#39;Shiny Side&#39; is appropriate for a lightning talk and fits the Posit        
</span></span></span><span class="line"><span class="cl"><span class="s2"> conference atmosphere. It suggests an accessible, positive approach that aligns well with the audience&#39;s curiosit 
</span></span></span><span class="line"><span class="cl"><span class="s2"> about AI.&#34;,                                                                                                       
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;suggestion&#34;: null                                                                                            
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                                                              
</span></span></span><span class="line"><span class="cl"><span class="s2">   {                                                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;aspect&#34;: &#34;relevance&#34;,                                                                                        
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;feedback&#34;: &#34;Highly relevant for the audience - Python/R users at Posit::conf would immediately recognize     
</span></span></span><span class="line"><span class="cl"><span class="s2"> &#39;Shiny&#39; and appreciate learning how to apply it to trending AI/LLM topics. The focus on practical demos matches   
</span></span></span><span class="line"><span class="cl"><span class="s2"> what this audience values.&#34;,                                                                                      
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;suggestion&#34;: &#34;Consider adding a subtitle to reinforce the practical angle: &#39;The Shiny Side of LLMs: Building 
</span></span></span><span class="line"><span class="cl"><span class="s2"> Interactive AI Demos&#39;&#34;                                                                                            
</span></span></span><span class="line"><span class="cl"><span class="s2">   }                                                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2"> ]
</span></span></span><span class="line"><span class="cl"><span class="s2"> &#34;&#34;&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
</div>
<div id="tabset-6-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><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-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></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="s">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">  You are a presentation coach for data scientists. 
</span></span></span><span class="line"><span class="cl"><span class="s">  You give constructive, focused, and practical feedback on titles, structure, and storytelling. 
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">  The presentation you are helping with is a 10-minute lightning talk at posit::conf(2025).  
</span></span></span><span class="line"><span class="cl"><span class="s">  The audience is Python and R users who are curious about AI and large language models, 
</span></span></span><span class="line"><span class="cl"><span class="s">  but not all of them have a deep technical background. 
</span></span></span><span class="line"><span class="cl"><span class="s">  The talk uses Shiny as a way to demo and explore LLMs in practice. 
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">  Always return your feedback as a JSON array of objects, where each object has the following keys:
</span></span></span><span class="line"><span class="cl"><span class="s">  - &#39;aspect&#39;: one of &#39;clarity&#39;, &#39;tone&#39;, or &#39;relevance&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">  - &#39;feedback&#39;: a concise assessment
</span></span></span><span class="line"><span class="cl"><span class="s">  - &#39;suggestion&#39;: an optional improvement if applicable
</span></span></span><span class="line"><span class="cl"><span class="s">  &#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">chat</span><span class="o">$</span><span class="nf">chat</span><span class="p">(</span><span class="s">&#34;Evaluate the title: &#39;The Shiny Side of LLMs&#39;&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show output</summary>
<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="c1">#&gt; ```json</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;   {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;aspect&#34;: &#34;clarity&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;feedback&#34;: &#34;The title is somewhat ambiguous - &#39;shiny side&#39; could refer to positive aspects of LLMs or literally to the Shiny framework. This wordplay might confuse audiences unfamiliar with both concepts.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;suggestion&#34;: &#34;Consider &#39;Exploring LLMs with Shiny: Interactive AI Demos&#39; or &#39;Building LLM Interfaces with Shiny&#39; for clearer technical focus&#34;</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;   {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;aspect&#34;: &#34;tone&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;feedback&#34;: &#34;The playful wordplay fits well with the posit::conf audience and lightning talk format. It&#39;s approachable and not intimidating, which works for mixed technical backgrounds.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;suggestion&#34;: null</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;   {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;aspect&#34;: &#34;relevance&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;feedback&#34;: &#34;Highly relevant for the audience - directly connects familiar Shiny technology with trending AI/LLM topics. Perfect for Python/R users wanting practical AI applications.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;suggestion&#34;: null</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; ]</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ```</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
</div>
</div>
<p>That&rsquo;s already something we can programmatically work with. Note that there are still differences between runs and between Python and R (just take a look at both examples). That is expected, even though our prompt is exactly the same. We&rsquo;ll come back to that a bit later.</p>
<h1 id="choosing-the-right-input-format">Choosing the right input format
</h1>
<p>The goal of the DeckCheck app is to do more than just looking at a title: we want to analyse Quarto slides. That means we need to provide our presentation in the chat. The question is&hellip; How do we do that? The most straightforward choice to provide this kind of data to an LLM is markdown. Markdown is lightweight, readable, and has a nice structure. It captures headings, bullet points, code blocks, and text, which is perfect.</p>
<p>But markdown isn&rsquo;t the only option:</p>
<ul>
<li><strong>Plain text</strong> is the most basic. It&rsquo;s simple, but you lose structure. Titles become regular text, as well as bullet points, code blocks etc. This makes it harder to analyse.</li>
<li><strong>HTML</strong> keeps all the structure but comes with a lot of noise (tags, attributes) that the LLM might happily read but we might not want to include.</li>
<li><strong>Structured JSON chunks</strong> give the most control, but is a big step from Quarto slides compared to plain markdown.</li>
</ul>
<p>If we&rsquo;re starting from a Quarto <code>.qmd</code> or <code>.ipynb</code> file, we can:</p>
<ul>
<li>Directly render Quarto slides to markdown via <code>quarto render</code> with <code>-to markdown</code></li>
<li>We&rsquo;re not getting into the nitty gritty of <a href="https://quarto.org/docs/authoring/markdown-basics.html" target="_blank" rel="noopener">markdown vs Quarto</a>
, but Quarto is basically markdown with some fancy extras, which can cause noise for our goal. So we might want to strip slide metadata, speaker notes, and other extras with custom filters or scripts, depending on what we want to keep (and to be honest: how sophisticated we want it to be).</li>
</ul>
<p>Let&rsquo;s say we have a very basic Quarto presentation that talks about &ldquo;The Shiny Side of LLMs&rdquo;, loosely resembling what we&rsquo;ve already talked about in this article, but is written in a sub-optimal way. The file is named <a href="https://github.com/hypebright/the-shiny-side-of-llms/blob/8857ca5ae4f74a86105ce9d1f04df3354eeee866/Quarto/my-presentation.qmd" target="_blank" rel="noopener">&ldquo;my-presentation.qmd&rdquo;</a>
 and lives in a Quarto folder in our working directory.</p>
<blockquote>
<p><strong>About the presentation file</strong></p>
<p>It&rsquo;s not necessary to read this presentation line-by-line, it is just there for demo purposes and for reference if you want to make sense of the output that Claude is going to give later. In subsequent code we will simply refer to it as &ldquo;my-presentation.qmd&rdquo;.</p>
</blockquote>
<details class="code-fold">
<summary>See full Quarto presentation</summary>
<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><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span><span class="lnt">153
</span><span class="lnt">154
</span><span class="lnt">155
</span><span class="lnt">156
</span><span class="lnt">157
</span><span class="lnt">158
</span><span class="lnt">159
</span><span class="lnt">160
</span><span class="lnt">161
</span><span class="lnt">162
</span><span class="lnt">163
</span><span class="lnt">164
</span><span class="lnt">165
</span><span class="lnt">166
</span><span class="lnt">167
</span><span class="lnt">168
</span><span class="lnt">169
</span><span class="lnt">170
</span><span class="lnt">171
</span><span class="lnt">172
</span><span class="lnt">173
</span><span class="lnt">174
</span><span class="lnt">175
</span><span class="lnt">176
</span><span class="lnt">177
</span><span class="lnt">178
</span><span class="lnt">179
</span><span class="lnt">180
</span><span class="lnt">181
</span><span class="lnt">182
</span><span class="lnt">183
</span><span class="lnt">184
</span><span class="lnt">185
</span><span class="lnt">186
</span><span class="lnt">187
</span><span class="lnt">188
</span><span class="lnt">189
</span><span class="lnt">190
</span><span class="lnt">191
</span><span class="lnt">192
</span><span class="lnt">193
</span><span class="lnt">194
</span><span class="lnt">195
</span><span class="lnt">196
</span><span class="lnt">197
</span><span class="lnt">198
</span><span class="lnt">199
</span><span class="lnt">200
</span><span class="lnt">201
</span><span class="lnt">202
</span><span class="lnt">203
</span><span class="lnt">204
</span><span class="lnt">205
</span><span class="lnt">206
</span><span class="lnt">207
</span><span class="lnt">208
</span><span class="lnt">209
</span><span class="lnt">210
</span><span class="lnt">211
</span><span class="lnt">212
</span><span class="lnt">213
</span><span class="lnt">214
</span><span class="lnt">215
</span><span class="lnt">216
</span><span class="lnt">217
</span><span class="lnt">218
</span><span class="lnt">219
</span><span class="lnt">220
</span><span class="lnt">221
</span><span class="lnt">222
</span><span class="lnt">223
</span><span class="lnt">224
</span><span class="lnt">225
</span><span class="lnt">226
</span><span class="lnt">227
</span><span class="lnt">228
</span><span class="lnt">229
</span><span class="lnt">230
</span><span class="lnt">231
</span><span class="lnt">232
</span><span class="lnt">233
</span><span class="lnt">234
</span><span class="lnt">235
</span><span class="lnt">236
</span><span class="lnt">237
</span><span class="lnt">238
</span><span class="lnt">239
</span><span class="lnt">240
</span><span class="lnt">241
</span><span class="lnt">242
</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="o">---</span>
</span></span><span class="line"><span class="cl"><span class="n">title</span><span class="o">:</span> <span class="s">&#34;The Shiny Side of LLMs&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">author</span><span class="o">:</span> <span class="s">&#34;Veerle Eeftink - van Leemput&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">format</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">  <span class="n">revealjs</span><span class="o">:</span> 
</span></span><span class="line"><span class="cl">    <span class="n">theme</span><span class="o">:</span> <span class="n">[default]</span>
</span></span><span class="line"><span class="cl"><span class="n">include</span><span class="o">-</span><span class="kr">in</span><span class="o">-</span><span class="n">header</span><span class="o">:</span> 
</span></span><span class="line"><span class="cl">  <span class="n">text</span><span class="o">:</span> <span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="o">&lt;</span><span class="n">link</span> <span class="n">href</span><span class="o">=</span><span class="s">&#34;https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css&#34;</span> <span class="n">rel</span><span class="o">=</span><span class="s">&#34;stylesheet&#34;</span> <span class="n">integrity</span><span class="o">=</span><span class="s">&#34;sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC&#34;</span> <span class="n">crossorigin</span><span class="o">=</span><span class="s">&#34;anonymous&#34;</span><span class="o">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="n">highlight</span><span class="o">-</span><span class="n">style</span><span class="o">:</span> <span class="s">&#34;nord&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">---</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Hey 👋</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">In</span> <span class="n">this</span> <span class="n">demo</span> <span class="n">presentation</span> <span class="n">we</span> <span class="n">will</span> <span class="n">explore</span> <span class="n">how</span> <span class="n">to</span> <span class="n">use</span> <span class="n">LLMs</span> <span class="kr">in</span> <span class="n">Shiny</span> <span class="n">applications.</span> <span class="n">Because</span> <span class="n">everybody</span> <span class="n">is</span> <span class="n">doing</span> <span class="n">it</span><span class="p">,</span> <span class="n">right</span><span class="o">?</span> 😉
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># What do you need?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">-</span> <span class="n">The</span> <span class="n">means</span> <span class="n">to</span> <span class="n">authenticate.</span> <span class="n">Depending</span> <span class="n">on</span> <span class="n">your</span> <span class="n">provider</span><span class="p">,</span> <span class="n">you</span> <span class="n">might</span> <span class="n">need</span> <span class="n">an</span> <span class="n">API</span> <span class="nf">key </span><span class="p">(</span><span class="n">e.g.</span> <span class="n">Anthropic</span><span class="p">,</span> <span class="n">OpenAI</span><span class="p">),</span> <span class="n">a</span> <span class="nf">token </span><span class="p">(</span><span class="n">e.g.</span> <span class="n">Hugging</span> <span class="n">Face</span><span class="p">),</span> <span class="n">or</span> <span class="n">other</span> <span class="n">authentication</span> <span class="n">methods.</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span> <span class="n">Put</span> <span class="n">those</span> <span class="n">credentials</span> <span class="kr">in</span> <span class="n">a</span> <span class="n">suitable</span> <span class="nf">place </span><span class="p">(</span><span class="kr">for</span> <span class="n">R</span><span class="o">:</span> <span class="n">`.Renviron`</span><span class="p">,</span> <span class="kr">for</span> <span class="n">Python</span><span class="o">:</span> <span class="n">`.env`</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span> <span class="n">Install</span> <span class="n">the</span> <span class="n">necessary</span> <span class="n">packages</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">  <span class="o">-</span> <span class="n">For</span> <span class="n">Python</span><span class="o">:</span> <span class="n">`chatlas`</span><span class="p">,</span> <span class="n">`shiny`</span>
</span></span><span class="line"><span class="cl">  <span class="o">-</span> <span class="n">For</span> <span class="n">R</span><span class="o">:</span> <span class="n">`ellmer`</span><span class="p">,</span> <span class="n">`shiny`</span><span class="p">,</span> <span class="n">`shinychat`</span><span class="p">,</span> <span class="n">and</span> <span class="n">optionally</span> <span class="n">`bslib`</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span> <span class="n">An</span> <span class="n">creative</span> <span class="n">idea</span> <span class="kr">for</span> <span class="n">your</span> <span class="n">LLM</span><span class="o">-</span><span class="n">powered</span> <span class="n">app</span><span class="o">!</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># What are we building</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">We</span><span class="s">&#39;re building an app called DeckCheck: it&#39;</span><span class="n">s</span> <span class="n">your</span> <span class="n">favourite</span> <span class="n">Presentation</span> <span class="n">Rehearsal</span> <span class="n">Buddy.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">With</span> <span class="n">DeckCheck</span><span class="p">,</span> <span class="n">you</span> <span class="n">can</span> <span class="n">upload</span> <span class="n">your</span> <span class="n">Quarto</span> <span class="n">presentation</span> <span class="n">and</span> <span class="n">get</span> <span class="n">tailored</span> <span class="n">feedback</span><span class="o">!</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">No</span> <span class="n">more</span> <span class="n">lengthy</span><span class="p">,</span> <span class="n">boring</span> <span class="n">presentations</span> 🤠<span class="n">.
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Where to start?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="m">1</span><span class="n">. Talk</span> <span class="n">to</span> <span class="n">an</span> <span class="n">LLM</span> <span class="n">with</span> <span class="n">the</span> <span class="n">help</span> <span class="n">of</span> <span class="nf">`chatlas` </span><span class="p">(</span><span class="n">Python</span><span class="p">)</span> <span class="n">and</span> <span class="nf">`ellmer` </span><span class="p">(</span><span class="n">R</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="m">2</span><span class="n">. Get</span> <span class="n">a</span> <span class="n">basic</span> <span class="n">`shiny`</span> <span class="n">app</span> <span class="n">ready</span>
</span></span><span class="line"><span class="cl"><span class="m">3</span><span class="n">. Integrate</span> <span class="n">`chatlas`</span><span class="o">/</span><span class="n">`ellmer`</span> <span class="kr">in</span> <span class="n">that</span> <span class="n">`shiny`</span> <span class="n">app</span> <span class="n">using</span> <span class="n">a</span> <span class="n">simple</span> <span class="n">chat</span> <span class="n">interface</span>
</span></span><span class="line"><span class="cl"><span class="m">4</span><span class="n">. Build</span> <span class="n">it</span> <span class="n">out</span> <span class="n">to</span> <span class="n">an</span> <span class="n">app</span> <span class="n">where</span> <span class="n">you</span> <span class="n">can</span> <span class="n">upload</span> <span class="n">slides</span><span class="p">,</span> <span class="n">see</span> <span class="n">metrics</span> <span class="n">about</span> <span class="n">your</span> <span class="n">presentation</span><span class="p">,</span> <span class="n">and</span> <span class="n">more.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Talk to an LLM with chatlas</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">For</span> <span class="n">Python</span><span class="p">,</span> <span class="n">we</span> <span class="n">can</span> <span class="n">use</span> <span class="n">`chatlas`</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">\`\`\`python
</span></span></span><span class="line"><span class="cl"><span class="n">from chatlas import ChatAnthropic
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">chat = ChatAnthropic(
</span></span></span><span class="line"><span class="cl"><span class="n">    model=&#34;claude-sonnet-4-20250514&#34;,
</span></span></span><span class="line"><span class="cl"><span class="n">    system_prompt=&#34;You are a presentation coach for data scientists. You give constructive, focused, and practical feedback on titles, structure, and storytelling.&#34;,
</span></span></span><span class="line"><span class="cl"><span class="n">)
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">chat.chat(&#34;I&#39;m working on a presentation with the title: &#39;The Shiny Side of LLMs&#39;. What&#39;s your feedback just based on that title?&#34;)
</span></span></span><span class="line"><span class="cl"><span class="n">\`\`\`
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">\# Talk to an LLM with ellmer
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">For R, we can use `ellmer`:
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">\`\`\`r
</span></span></span><span class="line"><span class="cl"><span class="n">library(ellmer)
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">chat &lt;- chat_anthropic(
</span></span></span><span class="line"><span class="cl"><span class="n">    model = &#34;claude-sonnet-4-20250514&#34;,
</span></span></span><span class="line"><span class="cl"><span class="n">    system_prompt = &#34;You are a presentation coach for data scientists. You give constructive, focused, and practical feedback on titles, structure, and storytelling.&#34;
</span></span></span><span class="line"><span class="cl"><span class="n">)
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">chat$chat(&#34;I&#39;m working on a presentation with the title: &#39;The Shiny Side of LLMs&#39;. What&#39;s your feedback just based on that title?&#34;)
</span></span></span><span class="line"><span class="cl"><span class="n">\`\`\`
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">\# Why Shiny?
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">-   Shiny is a web application framework for R and Python that allows you to build interactive web applications.
</span></span></span><span class="line"><span class="cl"><span class="n">-   It provides a reactive programming model, making it easy to create dynamic user interfaces and handle user inputs.
</span></span></span><span class="line"><span class="cl"><span class="n">-   Perfect for your LLM-powered app!
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">\# Shiny basics: Python
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">\`\`\`python
</span></span></span><span class="line"><span class="cl"><span class="n">from shiny import App, ui, render
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n"># Define UI
</span></span></span><span class="line"><span class="cl"><span class="n">app_ui = ui.page_fluid(
</span></span></span><span class="line"><span class="cl"><span class="n">    ui.h1(&#34;DeckCheck&#34;),
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">    # Card with content
</span></span></span><span class="line"><span class="cl"><span class="n">    ui.card(
</span></span></span><span class="line"><span class="cl"><span class="n">        ui.card_header(&#34;Welcome&#34;),
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">        ui.p(&#34;This is a basic Shiny for Python application.&#34;),
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">        # Input: text
</span></span></span><span class="line"><span class="cl"><span class="n">        ui.input_text_area(&#34;text&#34;, &#34;What&#39;s your question?&#34;),
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">        # Output: echo text back
</span></span></span><span class="line"><span class="cl"><span class="n">        ui.output_text(&#34;echo&#34;)
</span></span></span><span class="line"><span class="cl"><span class="n">    )
</span></span></span><span class="line"><span class="cl"><span class="n">)
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n"># Define server
</span></span></span><span class="line"><span class="cl"><span class="n">def server(input, output, session):
</span></span></span><span class="line"><span class="cl"><span class="n">    @render.text
</span></span></span><span class="line"><span class="cl"><span class="n">    def echo():
</span></span></span><span class="line"><span class="cl"><span class="n">        return f&#34;You asked: {input.text()}&#34;
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n"># Create app
</span></span></span><span class="line"><span class="cl"><span class="n">app = App(app_ui, server)
</span></span></span><span class="line"><span class="cl"><span class="n">\`\`\`
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">\# Shiny basics: R
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">Tip: combine with `bslib` for fancy looks!
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">\`\`\`r
</span></span></span><span class="line"><span class="cl"><span class="n">library(shiny)
</span></span></span><span class="line"><span class="cl"><span class="n">library(bslib)
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">ui &lt;- page_fluid(
</span></span></span><span class="line"><span class="cl"><span class="n">  theme = bs_theme(bootswatch = &#34;minty&#34;),
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">  # App title
</span></span></span><span class="line"><span class="cl"><span class="n">  h1(&#34;DeckCheck&#34;),
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">  # Create a card
</span></span></span><span class="line"><span class="cl"><span class="n">  card(
</span></span></span><span class="line"><span class="cl"><span class="n">    card_header(&#34;Welcome&#34;),
</span></span></span><span class="line"><span class="cl"><span class="n">    p(&#34;This is a simple Shiny app using bslib for layout and styling.&#34;),
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">    # Input: text
</span></span></span><span class="line"><span class="cl"><span class="n">    textAreaInput(&#34;text&#34;, &#34;What&#39;s your question?&#34;),
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">    # Output: echo the text back
</span></span></span><span class="line"><span class="cl"><span class="n">    textOutput(&#34;echo&#34;)
</span></span></span><span class="line"><span class="cl"><span class="n">  )
</span></span></span><span class="line"><span class="cl"><span class="n">)
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">server &lt;- function(input, output, session) {
</span></span></span><span class="line"><span class="cl"><span class="n">  # Reactive output
</span></span></span><span class="line"><span class="cl"><span class="n">  output$echo &lt;- renderText({
</span></span></span><span class="line"><span class="cl"><span class="n">    paste(&#34;You asked:&#34;, input$text)
</span></span></span><span class="line"><span class="cl"><span class="n">  })
</span></span></span><span class="line"><span class="cl"><span class="n">}
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">shinyApp(ui, server)
</span></span></span><span class="line"><span class="cl"><span class="n">\`\`\`
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">\# Where&#39;s the magic?
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">We&#39;re here for some AI magic, right? So let&#39;s talk to an LLM via our Shiny application!
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">![AI Magic](https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMmZ0cmd1aGZzM3JqdW1zbng0czU2c2RwejNtM25jMW8xdnYzNXVrdCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/OlxeZT285uhFydNetO/giphy.gif)
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">\# Shiny and chatlas
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">We can add a chat component to our UI with `ui.chat_ui` from `shiny`, and use `ChatAnthropic` from `chatlas` to send our messages to Claude. With minimal change, our Shiny for Python app is already LLM-powered!
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">`\`\`python
</span></span></span><span class="line"><span class="cl"><span class="n">from shiny import App, ui
</span></span></span><span class="line"><span class="cl"><span class="n">from chatlas import ChatAnthropic
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n"># Define UI
</span></span></span><span class="line"><span class="cl"><span class="n">app_ui = ui.page_fluid(
</span></span></span><span class="line"><span class="cl"><span class="n">    ui.h1(&#34;DeckCheck&#34;),
</span></span></span><span class="line"><span class="cl"><span class="n">    # Card with chat component
</span></span></span><span class="line"><span class="cl"><span class="n">    ui.card(
</span></span></span><span class="line"><span class="cl"><span class="n">        ui.card_header(&#34;Get started&#34;),
</span></span></span><span class="line"><span class="cl"><span class="n">        ui.p(&#34;Ask me anything about your presentation 💡&#34;),
</span></span></span><span class="line"><span class="cl"><span class="n">        # Chat component
</span></span></span><span class="line"><span class="cl"><span class="n">        ui.chat_ui(id=&#34;my_chat&#34;),
</span></span></span><span class="line"><span class="cl"><span class="n">    ),
</span></span></span><span class="line"><span class="cl"><span class="n">)
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n"># Define server
</span></span></span><span class="line"><span class="cl"><span class="n">def server(input, output, session):
</span></span></span><span class="line"><span class="cl"><span class="n">    chat_component = ui.Chat(id=&#34;my_chat&#34;)
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">    chat = ChatAnthropic(
</span></span></span><span class="line"><span class="cl"><span class="n">        model=&#34;claude-sonnet-4-20250514&#34;,
</span></span></span><span class="line"><span class="cl"><span class="n">        system_prompt=&#34;You are a presentation coach for data scientists. You give constructive, focused, and practical feedback on titles, structure, and storytelling.&#34;,
</span></span></span><span class="line"><span class="cl"><span class="n">    )
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">    @chat_component.on_user_submit
</span></span></span><span class="line"><span class="cl"><span class="n">    async def handle_user_input(user_input: str):
</span></span></span><span class="line"><span class="cl"><span class="n">        response = await chat.stream_async(user_input)
</span></span></span><span class="line"><span class="cl"><span class="n">        await chat_component.append_message_stream(response)
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n"># Create app
</span></span></span><span class="line"><span class="cl"><span class="n">app = App(app_ui, server)
</span></span></span><span class="line"><span class="cl"><span class="n">\`\`\`
</span></span></span><span class="line"><span class="cl"><span class="n">
</span></span></span><span class="line"><span class="cl"><span class="n">\# Shiny, shinychat and ellmer We can add a chat component to our UI with `chat_mod_ui` from `shinychat`, and use `chat_anthropic` from `ellmer`</span> <span class="n">to</span> <span class="n">send</span> <span class="n">our</span> <span class="n">messages</span> <span class="n">to</span> <span class="n">Claude.</span> <span class="n">With</span> <span class="n">minimal</span> <span class="n">change</span><span class="p">,</span> <span class="n">our</span> <span class="n">Shiny</span> <span class="kr">for</span> <span class="n">R</span> <span class="n">app</span> <span class="n">is</span> <span class="n">already</span> <span class="n">LLM</span><span class="o">-</span><span class="n">powered</span><span class="o">!</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">\`\`\`r</span>
</span></span><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">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">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="nf">page_fluid</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">theme</span> <span class="o">=</span> <span class="nf">bs_theme</span><span class="p">(</span><span class="n">bootswatch</span> <span class="o">=</span> <span class="s">&#34;minty&#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"># App title</span>
</span></span><span class="line"><span class="cl">  <span class="nf">h1</span><span class="p">(</span><span class="s">&#34;DeckCheck&#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 card</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;Get started&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="nf">p</span><span class="p">(</span><span class="s">&#34;Ask me anything about your presentation 💡&#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"># Chat component</span>
</span></span><span class="line"><span class="cl">    <span class="nf">chat_mod_ui</span><span class="p">(</span><span class="s">&#34;chat_component&#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</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="s">&#34;You are a presentation coach for data scientists. 
</span></span></span><span class="line"><span class="cl"><span class="s">  You give constructive, focused, and practical feedback on titles, structure, and storytelling.&#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="nf">chat_mod_server</span><span class="p">(</span><span class="s">&#34;chat_component&#34;</span><span class="p">,</span> <span class="n">chat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span> <span class="n">server</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">\`\`\`</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">\</span><span class="c1"># User experience</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">When</span> <span class="n">using</span> <span class="n">an</span> <span class="n">LLM</span> <span class="kr">in</span> <span class="n">a</span> <span class="n">Shiny</span> <span class="n">application</span><span class="p">,</span> <span class="n">it</span> <span class="n">is</span> <span class="n">important</span> <span class="n">to</span> <span class="n">consider</span> <span class="n">the</span> <span class="n">user</span> <span class="n">experience.</span> <span class="n">The</span> <span class="n">LLM</span> <span class="n">should</span> <span class="n">enhance</span> <span class="n">the</span> <span class="n">application</span><span class="p">,</span> <span class="n">not</span> <span class="n">overwhelm</span> <span class="n">it.</span> <span class="n">This</span> <span class="n">means</span> <span class="n">that</span> <span class="n">the</span> <span class="n">LLM</span><span class="s">&#39;s responses should be concise and relevant to the user&#39;</span><span class="n">s</span> <span class="n">query.</span> <span class="n">Additionally</span><span class="p">,</span> <span class="n">it</span> <span class="n">is</span> <span class="n">crucial</span> <span class="n">to</span> <span class="n">handle</span> <span class="n">errors</span> <span class="n">gracefully</span><span class="p">,</span> <span class="n">providing</span> <span class="n">users</span> <span class="n">with</span> <span class="n">helpful</span> <span class="n">feedback</span> <span class="kr">if</span> <span class="n">the</span> <span class="n">LLM</span> <span class="n">cannot</span> <span class="n">generate</span> <span class="n">a</span> <span class="n">response</span> <span class="n">or</span> <span class="kr">if</span> <span class="n">there</span> <span class="n">are</span> <span class="n">issues</span> <span class="n">with</span> <span class="n">the</span> <span class="n">API.</span> <span class="n">Therefore</span><span class="p">,</span> <span class="n">implementing</span> <span class="n">proper</span> <span class="n">error</span> <span class="n">handling</span> <span class="n">and</span> <span class="n">fallback</span> <span class="n">mechanisms</span> <span class="n">is</span> <span class="n">essential.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">\</span><span class="c1"># Up next</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">Of</span> <span class="n">course</span> <span class="n">we</span><span class="s">&#39;</span><span class="err">re not ready yet. Next steps include:</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="o">-</span>   <span class="n">Adding</span> <span class="n">a</span> <span class="n">file</span> <span class="n">upload</span> <span class="n">button</span>
</span></span><span class="line"><span class="cl"><span class="c1">## Reading the content of the presentation</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>   <span class="n">Crafting</span> <span class="n">our</span> <span class="n">prompt</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>   <span class="n">Extract</span> <span class="n">information</span> <span class="kr">in</span> <span class="n">a</span> <span class="n">structured</span> <span class="n">way</span>
</span></span><span class="line"><span class="cl"><span class="o">-</span>   <span class="n">Display</span> <span class="n">that</span> <span class="n">information</span> <span class="kr">in</span> <span class="n">a</span> <span class="n">visually</span> <span class="n">appealing</span> <span class="nf">manner </span><span class="p">(</span><span class="n">value</span> <span class="n">boxes</span><span class="p">,</span> <span class="n">use</span> <span class="n">of</span> <span class="n">colours</span><span class="p">,</span> <span class="n">icons</span><span class="p">,</span> <span class="n">functionality</span> <span class="n">to</span> <span class="n">export</span> <span class="n">the</span> <span class="n">feedback...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">\</span><span class="c1"># Thank you!</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<p>This presentation would be suitable for a lightning talk (a maximum of 10 minutes), and for an audience that is not yet familiar with <code>shiny</code> and <code>chatlas</code>/<code>ellmer</code>.</p>
<p>To turn this <code>.qmd</code> file programmatically into markdown (<code>.md</code>) we would need to call quarto from our system. This can be done via the CLI or, in the case of R, via the <a href="https://quarto-dev.github.io/quarto-r/" target="_blank" rel="noopener"><code>quarto</code> package</a>
. Note that our simple presentation doesn&rsquo;t make much use of the fancy stuff you can do with Quarto, so our <code>.md</code> file will almost look exactly the same as our <code>.qmd</code> file. But keep in mind we&rsquo;re building for a wider audience!</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">
<p>We can run quarto from the CLI with <code>subprocess</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></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">subprocess</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">([</span><span class="s2">&#34;quarto&#34;</span><span class="p">,</span> <span class="s2">&#34;render&#34;</span><span class="p">,</span> <span class="s2">&#34;my-presentation.qmd&#34;</span><span class="p">,</span> <span class="s2">&#34;--to&#34;</span><span class="p">,</span> <span class="s2">&#34;markdown&#34;</span><span class="p">])</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-7-2">
<p>Using the <code>quarto</code> package:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="n">quarto</span><span class="o">::</span><span class="nf">quarto_render</span><span class="p">(</span><span class="s">&#34;my-presentation.qmd&#34;</span><span class="p">,</span> <span class="n">output_format</span> <span class="o">=</span> <span class="s">&#34;markdown&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>or as a sytem command:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">system</span><span class="p">(</span><span class="s">&#34;quarto render my-presentation.qmd --to markdown&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<p>Now our system prompt can look something like this:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><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-markdown" data-lang="markdown"><span class="line"><span class="cl">You are a presentation coach for data scientists that analyses presentation slide decks written in Markdown. 
</span></span><span class="line"><span class="cl">You extract key information, evaluate quality, and return structured feedback that is constructive, focused and practical.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The presentation you are helping with is a 10-minute lightning talk at posit::conf(2025).  
</span></span><span class="line"><span class="cl">The audience is Python and R users who are curious about AI and large language models, 
</span></span><span class="line"><span class="cl">but not all of them have a deep technical background. 
</span></span><span class="line"><span class="cl">The talk uses Shiny as a way to demo and explore LLMs in practice. 
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">You extract the following information:
</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">You score the presentation on the following categories (from 1–10), and give a concise explanation:
</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">Always return your answer as a JSON object with the following structure:
</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><details class="code-fold">
<summary>Show full prompt</summary>
<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></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="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">You are a presentation coach for data scientists that analyses presentation slide decks written in Markdown. 
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract key information, evaluate quality, and return structured feedback that is constructive, focused and practical.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">The presentation you are helping with is a 10-minute lightning talk at posit::conf(2025).  
</span></span></span><span class="line"><span class="cl"><span class="s2">The audience is Python and R users who are curious about AI and large language models, 
</span></span></span><span class="line"><span class="cl"><span class="s2">but not all of them have a deep technical background. 
</span></span></span><span class="line"><span class="cl"><span class="s2">The talk uses Shiny as a way to demo and explore LLMs in practice. 
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract the following information:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">1. The presentation title
</span></span></span><span class="line"><span class="cl"><span class="s2">2. Total number of slides
</span></span></span><span class="line"><span class="cl"><span class="s2">3. Percentage of slides containing code blocks
</span></span></span><span class="line"><span class="cl"><span class="s2">4. Percentage of slides containing images
</span></span></span><span class="line"><span class="cl"><span class="s2">5. Estimated presentation length (in minutes, assuming ~1 minute per text slide and 2–3 minutes per code or image-heavy slide)
</span></span></span><span class="line"><span class="cl"><span class="s2">6. Tone (a brief description)
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You score the presentation on the following categories (from 1–10), and give a concise explanation:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">1. Clarity of content: evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?
</span></span></span><span class="line"><span class="cl"><span class="s2">2. Relevance for intended audience: assess how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?
</span></span></span><span class="line"><span class="cl"><span class="s2">3. Visual design: judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?
</span></span></span><span class="line"><span class="cl"><span class="s2">4. Engagement: estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?
</span></span></span><span class="line"><span class="cl"><span class="s2">5. Pacing: analyze the distribution of content across slides. Are some slides too dense or too light? 
</span></span></span><span class="line"><span class="cl"><span class="s2">6. Structure: review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?
</span></span></span><span class="line"><span class="cl"><span class="s2">7. consistency: evaluatue whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?
</span></span></span><span class="line"><span class="cl"><span class="s2">8. Accessibility: consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Always return your answer as a JSON object with the following structure:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">{
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;presentation_title&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;total_slides&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;percent_with_code&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;percent_with_images&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;estimated_duration_minutes&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;tone&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;clarity&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;relevance&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;visual_design&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;engagement&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;pacing&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;structure&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;concistency&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;accessibility&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  }
</span></span></span><span class="line"><span class="cl"><span class="s2">}
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<p>And our user prompt:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">Here are the slides in Markdown: ...
</span></span></code></pre></td></tr></table>
</div>
</div><h1 id="storing-prompts-and-dynamic-data">Storing prompts and dynamic data
</h1>
<p>As you know by know, you need to provide very specific instructions in your prompt if you&rsquo;re looking for good outcomes. And the more you specify, the lengthier your prompt becomes. Eventually this is going to be very annoying to put in your code directly. Therefore, it&rsquo;s a good idea to store your prompt somewhere. We determined markdown is the way to go for inputting slide content, but the same motivation applies to our prompt. Besides that, markdown allows us to structure our prompt more clearly too: headers, bullet points, numbered lists&hellip; That&rsquo;s nice for the LLM and for you!</p>
<p>The recommended file naming convention is to use <code>prompt.md</code> if you only have one prompt, and use informative names like <code>prompt-analyse-slides.md</code> if you have multiple. For clarity, we&rsquo;ll go with the latter even though we only have one prompt (so far).</p>
<p>Since your prompt(s) are very important for the outcome of your LLM-powered app, it&rsquo;s also a good idea to track these files using git. You don&rsquo;t want to lose that precious carefully prompt that worked so well before you decided to change the whole thing.</p>
<p>Prompt(s) don&rsquo;t have to be static: they can be dynamic too. We want our app to improve Quarto presentations world-wide: many users, many Quarto slides, many audiences and presentation lengths. Our prompt needs to be able to work with this dynamic data, so our prompt needs to change depending on those variables. Luckily, we can use the <code>interpolate_file()</code> or <code>interpolate()</code> function, which, as the names suggest, can interpolate variables into a prompt template stored in a file, or in a simple string. To do so, we use the <code>{ x }</code> syntax, making our <code>prompt-analyse-slides.md</code> file look like this:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><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-markdown" data-lang="markdown"><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The presentation you are helping with is a {{ length }}-minute {{ type }} at {{ event }}.  
</span></span><span class="line"><span class="cl">The audience is {{ audience }}. 
</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><details class="code-fold">
<summary>Show full prompt</summary>
<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></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="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">You are a presentation coach for data scientists that analyses presentation slide decks written in Markdown. 
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract key information, evaluate quality, and return structured feedback that is constructive, focused and practical.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">The presentation you are helping with is a {{ length }}-minute {{ type }} at {{ event }}.  
</span></span></span><span class="line"><span class="cl"><span class="s2">The audience is {{ audience }}. 
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract the following information:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">1. The presentation title
</span></span></span><span class="line"><span class="cl"><span class="s2">2. Total number of slides
</span></span></span><span class="line"><span class="cl"><span class="s2">3. Percentage of slides containing code blocks
</span></span></span><span class="line"><span class="cl"><span class="s2">4. Percentage of slides containing images
</span></span></span><span class="line"><span class="cl"><span class="s2">5. Estimated presentation length (in minutes, assuming ~1 minute per text slide and 2–3 minutes per code or image-heavy slide)
</span></span></span><span class="line"><span class="cl"><span class="s2">6. Tone (a brief description)
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You score the presentation on the following categories (from 1–10), and give a concise explanation:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">1. Clarity of content: evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?
</span></span></span><span class="line"><span class="cl"><span class="s2">2. Relevance for intended audience: assess how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?
</span></span></span><span class="line"><span class="cl"><span class="s2">3. Visual design: judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?
</span></span></span><span class="line"><span class="cl"><span class="s2">4. Engagement: estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?
</span></span></span><span class="line"><span class="cl"><span class="s2">5. Pacing: analyze the distribution of content across slides. Are some slides too dense or too light? 
</span></span></span><span class="line"><span class="cl"><span class="s2">6. Structure: review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?
</span></span></span><span class="line"><span class="cl"><span class="s2">7. consistency: evaluatue whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?
</span></span></span><span class="line"><span class="cl"><span class="s2">8. Accessibility: consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Always return your answer as a JSON object with the following structure:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">{
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;presentation_title&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;total_slides&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;percent_with_code&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;percent_with_images&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;estimated_duration_minutes&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;tone&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;clarity&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;relevance&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;visual_design&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;engagement&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;pacing&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;structure&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;concistency&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;accessibility&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  }
</span></span></span><span class="line"><span class="cl"><span class="s2">}
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<p>Our user prompt is going directly into the <code>chat</code> method and will be:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">Here are the slides in Markdown: {{ markdown }}
</span></span></code></pre></td></tr></table>
</div>
</div><p>Note: if your user prompt is long you might want to put this in a separate prompt file as well.</p>
<blockquote>
<p><strong>Note</strong></p>
<p>You might wonder why we don&rsquo;t we put the Markdown content in our system prompt. The reason is that we don&rsquo;t want to overload our system prompt with big chunks of data like our Markdown slides. As mentioned previously, the system prompt sets the role, the rules, and provides task instructions. It is stable context. The Markdown slides are the actual content to analyse, and they change from one run to the other. This fits better in a user prompt. It&rsquo;s also good practice to keep the system prompt clean so it is reusable across runs.</p>
</blockquote>
<p>Those are prompts we can work with! So let&rsquo;s chat away:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-8" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-8-1">Python</a></li>
<li><a href="#tabset-8-2">R</a></li>
</ul>
<div id="tabset-8-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><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-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">ChatAnthropic</span><span class="p">,</span> <span class="n">interpolate_file</span><span class="p">,</span> <span class="n">interpolate</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">subprocess</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"># Get Quarto presentation and convert to plain Markdown</span>
</span></span><span class="line"><span class="cl"><span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="s2">&#34;quarto&#34;</span><span class="p">,</span> <span class="s2">&#34;render&#34;</span><span class="p">,</span> <span class="s2">&#34;./Quarto/my-presentation.qmd&#34;</span><span class="p">,</span> <span class="s2">&#34;--to&#34;</span><span class="p">,</span> <span class="s2">&#34;markdown&#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"># Dynamic data</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Audience, length in minutes, type, and event</span>
</span></span><span class="line"><span class="cl"><span class="n">audience_content</span> <span class="o">=</span> <span class="s2">&#34;Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">length_content</span> <span class="o">=</span> <span class="s2">&#34;10&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">type_content</span> <span class="o">=</span> <span class="s2">&#34;lightning talk&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">event_content</span> <span class="o">=</span> <span class="s2">&#34;posit::conf(2025)&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Read the generated Markdown file containing our slides</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./Quarto/docs/my-presentation.md&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_content</span> <span class="o">=</span> <span class="n">markdown_file</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#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"># Define prompt file</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./prompts/prompt-analyse-slides.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"># Create system prompt</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt</span> <span class="o">=</span> <span class="n">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">variables</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;audience&#34;</span><span class="p">:</span> <span class="n">audience_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;length&#34;</span><span class="p">:</span> <span class="n">length_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="n">type_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;event&#34;</span><span class="p">:</span> <span class="n">event_content</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"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">ChatAnthropic</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;claude-sonnet-4-20250514&#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="n">system_prompt</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"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Since all the instructions are in the system prompt, we can just</span>
</span></span><span class="line"><span class="cl"><span class="c1"># provide the Markdown content as a message</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="n">interpolate</span><span class="p">(</span><span class="s2">&#34;Here are the slides in Markdown: {{ markdown_content }}&#34;</span><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show output</summary>
<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="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  {                                                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;presentation_title&#34;: &#34;The Shiny Side of LLMs&#34;,                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;total_slides&#34;: 16,                                                              
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;percent_with_code&#34;: 44,                                                         
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;percent_with_images&#34;: 6,                                                        
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;estimated_duration_minutes&#34;: 22,                                                
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;tone&#34;: &#34;Casual, enthusiastic, and approachable with emojis and informal languag 
</span></span></span><span class="line"><span class="cl"><span class="s2"> aimed at making technical concepts accessible&#34;,                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;clarity&#34;: {                                                                     
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 7,                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Content is generally clear with good step-by-step progressio 
</span></span></span><span class="line"><span class="cl"><span class="s2"> but some technical concepts could benefit from more explanation for non-technical  
</span></span></span><span class="line"><span class="cl"><span class="s2"> audience members. Code examples are well-structured but may be overwhelming for    
</span></span></span><span class="line"><span class="cl"><span class="s2"> beginners.&#34;                                                                        
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;relevance&#34;: {                                                                   
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 8,                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Well-targeted for the audience - provides both Python and R  
</span></span></span><span class="line"><span class="cl"><span class="s2"> examples, acknowledges varying technical backgrounds, and focuses on practical     
</span></span></span><span class="line"><span class="cl"><span class="s2"> implementation rather than deep theory. The DeckCheck example is relatable to the  
</span></span></span><span class="line"><span class="cl"><span class="s2"> conference audience.&#34;                                                              
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;visual_design&#34;: {                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 5,                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Heavy reliance on text and code blocks with minimal visual   
</span></span></span><span class="line"><span class="cl"><span class="s2"> elements. Only one image (GIF) used. Code blocks dominate several slides, making   
</span></span></span><span class="line"><span class="cl"><span class="s2"> them potentially overwhelming. Layout appears basic without clear visual hierarchy 
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;engagement&#34;: {                                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 6,                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Uses emojis and casual language to maintain interest, includ 
</span></span></span><span class="line"><span class="cl"><span class="s2"> a relatable example (DeckCheck), and has one engaging GIF. However, lacks          
</span></span></span><span class="line"><span class="cl"><span class="s2"> interactive elements beyond code examples and could benefit from more storytelling 
</span></span></span><span class="line"><span class="cl"><span class="s2"> elements.&#34;                                                                         
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;pacing&#34;: {                                                                      
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 4,                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Uneven pacing with several code-heavy slides that would      
</span></span></span><span class="line"><span class="cl"><span class="s2"> require significant time to explain, while some slides have minimal content. The   
</span></span></span><span class="line"><span class="cl"><span class="s2"> estimated 22-minute duration exceeds the 10-minute lightning talk format           
</span></span></span><span class="line"><span class="cl"><span class="s2"> considerably.&#34;                                                                     
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;structure&#34;: {                                                                   
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 8,                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Clear logical progression from introduction to requirements, 
</span></span></span><span class="line"><span class="cl"><span class="s2"> basic concepts, implementation examples, and next steps. Good parallel structure   
</span></span></span><span class="line"><span class="cl"><span class="s2"> showing both Python and R approaches. Solid beginning, middle, and end.&#34;           
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;concistency&#34;: {                                                                 
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 7,                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Maintains consistent casual tone and formatting throughout.  
</span></span></span><span class="line"><span class="cl"><span class="s2"> Code examples follow similar patterns for both languages. Some inconsistency in    
</span></span></span><span class="line"><span class="cl"><span class="s2"> slide density and the single image feels somewhat disconnected from the overall    
</span></span></span><span class="line"><span class="cl"><span class="s2"> style.&#34;                                                                            
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;accessibility&#34;: {                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 6,                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Code blocks may be challenging for those with visual         
</span></span></span><span class="line"><span class="cl"><span class="s2"> impairments due to syntax highlighting and smaller fonts. The casual tone and emoj 
</span></span></span><span class="line"><span class="cl"><span class="s2"> help with cognitive accessibility, but the technical density could be overwhelming 
</span></span></span><span class="line"><span class="cl"><span class="s2"> for some audience members.&#34;                                                        
</span></span></span><span class="line"><span class="cl"><span class="s2">   }                                                                                
</span></span></span><span class="line"><span class="cl"><span class="s2"> }
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
</div>
<div id="tabset-8-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span></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></span><span class="line"><span class="cl"><span class="c1"># Get Quarto presentation and convert to plain Markdown</span>
</span></span><span class="line"><span class="cl"><span class="n">quarto</span><span class="o">::</span><span class="nf">quarto_render</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;./Quarto/my-presentation.qmd&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">output_format</span> <span class="o">=</span> <span class="s">&#34;markdown&#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"># Dynamic data</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Audience, length in minutes, type, and event</span>
</span></span><span class="line"><span class="cl"><span class="n">audience_content</span> <span class="o">&lt;-</span> <span class="s">&#34;Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">length_content</span> <span class="o">&lt;-</span> <span class="s">&#34;10&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">type_content</span> <span class="o">&lt;-</span> <span class="s">&#34;lightning talk&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">event_content</span> <span class="o">&lt;-</span> <span class="s">&#34;posit::conf(2025)&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Read the generated Markdown file containing our slides</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./Quarto/docs/my-presentation.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_content</span> <span class="o">&lt;-</span> <span class="nf">readChar</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">,</span> <span class="nf">file.size</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define prompt file</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./prompts/prompt-analyse-slides.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create system prompt</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt</span> <span class="o">&lt;-</span> <span class="nf">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">path</span> <span class="o">=</span> <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">audience</span> <span class="o">=</span> <span class="n">audience_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">length</span> <span class="o">=</span> <span class="n">length_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">type</span> <span class="o">=</span> <span class="n">type_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span> <span class="o">=</span> <span class="n">event_content</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"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="n">system_prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">params</span> <span class="o">=</span> <span class="nf">params</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">temperature</span> <span class="o">=</span> <span class="m">0.8</span> <span class="c1"># default is 1</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"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Since all the instructions are in the system prompt, we can just</span>
</span></span><span class="line"><span class="cl"><span class="c1"># provide the Markdown content as a message</span>
</span></span><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="nf">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Here are the slides in Markdown: {{ markdown_content }}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show output</summary>
<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-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;presentation_title&#34;: &#34;The Shiny Side of LLMs&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;total_slides&#34;: 16,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;percent_with_code&#34;: 37.5,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;percent_with_images&#34;: 6.25,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;estimated_duration_minutes&#34;: 14,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;tone&#34;: &#34;Casual, friendly, and enthusiastic with emoji use and conversational </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; language that makes technical content approachable&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;clarity&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 7,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Content is generally clear with good progression from basics </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; to implementation. However, some technical details could benefit from more </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; explanation, and the system prompts in code examples could be better integrated </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; into the narrative.&#34;</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;   &#34;relevance&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 8,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Excellent match for the audience - shows practical </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; integration of LLMs with familiar tools (Shiny), provides both Python and R </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; examples, and focuses on a relatable use case (presentation feedback) that </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; resonates with the posit::conf audience.&#34;</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;   &#34;visual_design&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 6,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Basic markdown structure is clean but lacks visual hierarchy </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; and design elements. Code blocks are well-formatted, but slides could benefit from </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; more visual breaks, bullet points, and consistent formatting. The single GIF adds </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; some visual interest.&#34;</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;   &#34;engagement&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 7,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Good use of humor, emojis, and relatable scenarios (boring </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; presentations). The progression from simple to complex keeps interest, and the </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; practical demo concept is engaging. Could benefit from more interactive elements or</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; storytelling.&#34;</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;   &#34;pacing&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 6,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Some slides are too dense (UX slide, code-heavy slides) while</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; others are very light (Thank you slide). The progression is logical but could be </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; more evenly distributed. Code slides may take longer than estimated.&#34;</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;   &#34;structure&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 8,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Clear logical flow from introduction to requirements, basic </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; concepts, implementation, and next steps. Good parallel structure showing both </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Python and R implementations. Strong opening and clear progression toward building </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; the demo app.&#34;</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;   &#34;concistency&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 7,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Generally consistent tone and approach throughout. Code </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; formatting is consistent between languages. However, some slides vary significantly</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; in content density, and the casual tone occasionally conflicts with more technical </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; sections.&#34;</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;   &#34;accessibility&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 5,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Basic markdown structure is accessible, but lacks </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; considerations for screen readers (no alt text for the GIF), may have contrast </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; issues depending on theme, and code blocks could be challenging for some users. </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Font sizes and spacing depend on presentation software.&#34;</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; }</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
</div>
</div>
<blockquote>
<p><strong>Inconsistencies in the output</strong></p>
<p>Did you already notice that some meta-data, like the percentage of slides with code and/or images is inconsistent? For example, the &ldquo;percent_with_code&rdquo; is 40, but other times it&rsquo;s 37.5 or 45. It seems like a broken calculator! To understand why this happens you can read <a href="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-2/../../../blog/shiny/shiny-side-of-llms-part-1/">part 1</a>
 again, where we talk about why LLMs are really good at some tasks, and others, well&hellip; Not so much. That doesn&rsquo;t mean it&rsquo;s completely out of our hands. We can help the LLM with a &ldquo;tool&rdquo;. Keep on reading to learn more about that concept.</p>
</blockquote>
<p>It&rsquo;s not too bad for a first try and it serves as a basis we can depart from. We can go back and forth many times, aka do some &ldquo;prompt engineering&rdquo;, to make sure we are getting results that are usable. The prompt that we used mostly returns a broad analysis (makes sense, we asked it to), but if we want our users to take action we need to provide specific improvements. So we could extend our prompt with &ldquo;provide specific and actionable improvements&rdquo; and provide specific instructions like &ldquo;keep each suggestion concise and mention the slide number(s) if applicable&rdquo;:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">You score the presentation on the following categories (from 1–10), and give a concise explanation:
</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">For each of the above scoring categories, provide specific and actionable improvements. Follow these instructions:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Keep each suggestion concise and mention the slide number(s) if applicable.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Do not invent issues and only suggest improvements when the content would clearly benefit from them.
</span></span><span class="line"><span class="cl"><span class="k">-</span> For each catogory, estimate what the new score would be if these improvements are implemented.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Return the improvement and new score as part of the response for that category.
</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><details class="code-fold">
<summary>Show full prompt</summary>
<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></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="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">You are a presentation coach for data scientists that analyses presentation slide decks written in Markdown. 
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract key information, evaluate quality, and return structured feedback that is constructive, focused and practical.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">The presentation you are helping with is a {{ length }}-minute {{ type }} at {{ event }}.  
</span></span></span><span class="line"><span class="cl"><span class="s2">The audience is {{ audience }}. 
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract the following information:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">1. The presentation title
</span></span></span><span class="line"><span class="cl"><span class="s2">2. Total number of slides
</span></span></span><span class="line"><span class="cl"><span class="s2">3. Percentage of slides containing code blocks
</span></span></span><span class="line"><span class="cl"><span class="s2">4. Percentage of slides containing images
</span></span></span><span class="line"><span class="cl"><span class="s2">5. Estimated presentation length (in minutes, assuming ~1 minute per text slide and 2–3 minutes per code or image-heavy slide)
</span></span></span><span class="line"><span class="cl"><span class="s2">6. Tone (a brief description)
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You score the presentation on the following categories (from 1–10), and give a concise explanation:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">1. Clarity of content: evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?
</span></span></span><span class="line"><span class="cl"><span class="s2">2. Relevance for intended audience: assess how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?
</span></span></span><span class="line"><span class="cl"><span class="s2">3. Visual design: judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?
</span></span></span><span class="line"><span class="cl"><span class="s2">4. Engagement: estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?
</span></span></span><span class="line"><span class="cl"><span class="s2">5. Pacing: analyze the distribution of content across slides. Are some slides too dense or too light? 
</span></span></span><span class="line"><span class="cl"><span class="s2">6. Structure: review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?
</span></span></span><span class="line"><span class="cl"><span class="s2">7. consistency: evaluatue whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?
</span></span></span><span class="line"><span class="cl"><span class="s2">8. Accessibility: consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">For each of the above scoring categories, provide specific and actionable improvements. Follow these instructions:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">- Keep each suggestion concise and mention the slide number(s) if applicable.
</span></span></span><span class="line"><span class="cl"><span class="s2">- Do not invent issues and only suggest improvements when the content would clearly benefit from them.
</span></span></span><span class="line"><span class="cl"><span class="s2">- For each catogory, estimate what the new score would be if these improvements are implemented.
</span></span></span><span class="line"><span class="cl"><span class="s2">- Return the improvement and new score as part of the response for that category.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Always return your answer as a JSON object with the following structure:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">{
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;presentation_title&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;total_slides&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;percent_with_code&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;percent_with_images&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;estimated_duration_minutes&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;tone&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;clarity&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;improvements&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score_after_improvements&#34;: 0
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;relevance&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;improvements&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score_after_improvements&#34;: 0
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;visual_design&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;improvements&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score_after_improvements&#34;: 0
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;engagement&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;improvements&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score_after_improvements&#34;: 0
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;pacing&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;improvements&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score_after_improvements&#34;: 0
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;structure&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;improvements&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score_after_improvements&#34;: 0
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;consistency&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;improvements&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score_after_improvements&#34;: 0
</span></span></span><span class="line"><span class="cl"><span class="s2">  },
</span></span></span><span class="line"><span class="cl"><span class="s2">  &#34;accessibility&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score&#34;: 0,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;justification&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;improvements&#34;: &#34;&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;score_after_improvements&#34;: 0
</span></span></span><span class="line"><span class="cl"><span class="s2">  }
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-9" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-9-1">Python</a></li>
<li><a href="#tabset-9-2">R</a></li>
</ul>
<div id="tabset-9-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><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="c1"># Define prompt file</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./prompts/prompt-analyse-slides-with-improvements.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"># Create system prompt</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt</span> <span class="o">=</span> <span class="n">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">variables</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;audience&#34;</span><span class="p">:</span> <span class="n">audience_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;length&#34;</span><span class="p">:</span> <span class="n">length_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="n">type_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;event&#34;</span><span class="p">:</span> <span class="n">event_content</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"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">ChatAnthropic</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;claude-sonnet-4-20250514&#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="n">system_prompt</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"># Start conversation with the chat</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="n">interpolate</span><span class="p">(</span><span class="s2">&#34;Here are the slides in Markdown: {{ markdown_content }}&#34;</span><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show complete code</summary>
<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></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">ChatAnthropic</span><span class="p">,</span> <span class="n">interpolate_file</span><span class="p">,</span> <span class="n">interpolate</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">subprocess</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"># Get Quarto presentation and convert to plain Markdown</span>
</span></span><span class="line"><span class="cl"><span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="s2">&#34;quarto&#34;</span><span class="p">,</span> <span class="s2">&#34;render&#34;</span><span class="p">,</span> <span class="s2">&#34;./Quarto/my-presentation.qmd&#34;</span><span class="p">,</span> <span class="s2">&#34;--to&#34;</span><span class="p">,</span> <span class="s2">&#34;markdown&#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"># Dynamic data</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Audience, length in minutes, type, and event</span>
</span></span><span class="line"><span class="cl"><span class="n">audience_content</span> <span class="o">=</span> <span class="s2">&#34;Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">length_content</span> <span class="o">=</span> <span class="s2">&#34;10&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">type_content</span> <span class="o">=</span> <span class="s2">&#34;lightning talk&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">event_content</span> <span class="o">=</span> <span class="s2">&#34;posit::conf(2025)&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Read the generated Markdown file containing our slides</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./Quarto/docs/my-presentation.md&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_content</span> <span class="o">=</span> <span class="n">markdown_file</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#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"># Define prompt file</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./prompts/prompt-analyse-slides-with-improvements.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"># Create system prompt</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt</span> <span class="o">=</span> <span class="n">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">variables</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;audience&#34;</span><span class="p">:</span> <span class="n">audience_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;length&#34;</span><span class="p">:</span> <span class="n">length_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="n">type_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;event&#34;</span><span class="p">:</span> <span class="n">event_content</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"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">ChatAnthropic</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;claude-sonnet-4-20250514&#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="n">system_prompt</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"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Since all the instructions are in the system prompt, we can just</span>
</span></span><span class="line"><span class="cl"><span class="c1"># provide the Markdown content as a message</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="n">interpolate</span><span class="p">(</span><span class="s2">&#34;Here are the slides in Markdown: {{ markdown_content }}&#34;</span><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<details class="code-fold">
<summary>Show output</summary>
<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></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="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">  {                                                                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;presentation_title&#34;: &#34;The Shiny Side of LLMs&#34;,                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;total_slides&#34;: 16,                                                                              
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;percent_with_code&#34;: 43.75,                                                                      
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;percent_with_images&#34;: 6.25,                                                                     
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;estimated_duration_minutes&#34;: 23,                                                                
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;tone&#34;: &#34;Casual, enthusiastic, and developer-friendly with emojis and informal language&#34;,        
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;clarity&#34;: {                                                                                     
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 7,                                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Content is generally clear with good explanations of technical concepts, but 
</span></span></span><span class="line"><span class="cl"><span class="s2"> some slides lack necessary context or detail for the intended audience&#34;,                           
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;improvements&#34;: &#34;Define &#39;chatlas&#39; and &#39;ellmer&#39; when first introduced (slides 5-6). Add brief   
</span></span></span><span class="line"><span class="cl"><span class="s2"> explanation of what &#39;system_prompt&#39; does. Explain what &#39;reactive programming model&#39; means in       
</span></span></span><span class="line"><span class="cl"><span class="s2"> practical terms (slide 7).&#34;,                                                                       
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score_after_improvements&#34;: 8                                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;relevance&#34;: {                                                                                   
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 8,                                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Very relevant for Python and R users interested in AI/LLMs. Code examples ar 
</span></span></span><span class="line"><span class="cl"><span class="s2"> appropriate and the practical focus matches audience needs&#34;,                                       
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;improvements&#34;: &#34;Add a brief comparison of when to choose Python vs R approach (slide 4).      
</span></span></span><span class="line"><span class="cl"><span class="s2"> Include mention of cost considerations for API usage in slide 2.&#34;,                                 
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score_after_improvements&#34;: 9                                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;visual_design&#34;: {                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 5,                                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Heavy text density on many slides, particularly slides with code blocks.     
</span></span></span><span class="line"><span class="cl"><span class="s2"> Limited visual elements beyond one GIF&#34;,                                                           
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;improvements&#34;: &#34;Break up text-heavy slides (2, 14) into bullet points or multiple slides. Add 
</span></span></span><span class="line"><span class="cl"><span class="s2"> diagrams showing app architecture (slides 4, 7). Use consistent formatting for code blocks and     
</span></span></span><span class="line"><span class="cl"><span class="s2"> ensure proper syntax highlighting.&#34;,                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score_after_improvements&#34;: 7                                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;engagement&#34;: {                                                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 6,                                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Casual tone and emojis add personality, and the practical demo concept is    
</span></span></span><span class="line"><span class="cl"><span class="s2"> engaging, but lacks interactive elements or storytelling&#34;,                                         
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;improvements&#34;: &#34;Add a brief demo video or screenshots of the final app. Include a &#39;What you&#39;l 
</span></span></span><span class="line"><span class="cl"><span class="s2"> learn today&#39; slide early on. Add audience poll question about their LLM experience (slide 2).&#34;,    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score_after_improvements&#34;: 8                                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;pacing&#34;: {                                                                                      
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 4,                                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Presentation is too long for 10-minute slot (estimated 23 minutes) with unev 
</span></span></span><span class="line"><span class="cl"><span class="s2"> distribution - some slides very dense, others very light&#34;,                                         
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;improvements&#34;: &#34;Combine slides 5-6 into one comparison slide. Merge slides 8-9 into single    
</span></span></span><span class="line"><span class="cl"><span class="s2"> Python/R comparison. Remove or significantly shorten slide 14. Condense setup requirements (slide  
</span></span></span><span class="line"><span class="cl"><span class="s2"> 2).&#34;,                                                                                              
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score_after_improvements&#34;: 7                                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;structure&#34;: {                                                                                   
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 7,                                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Good logical flow from introduction to implementation, but missing clear     
</span></span></span><span class="line"><span class="cl"><span class="s2"> conclusion and call-to-action&#34;,                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;improvements&#34;: &#34;Add agenda/outline slide after slide 1. Include key takeaways and resources   
</span></span></span><span class="line"><span class="cl"><span class="s2"> slide before &#39;Thank you&#39;. Make slide 15 more specific about next steps.&#34;,                          
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score_after_improvements&#34;: 8                                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;consistency&#34;: {                                                                                 
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 8,                                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Generally consistent tone and formatting throughout, with good parallel      
</span></span></span><span class="line"><span class="cl"><span class="s2"> structure between Python and R examples&#34;,                                                          
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;improvements&#34;: &#34;Ensure consistent code block formatting and indentation. Standardize bullet   
</span></span></span><span class="line"><span class="cl"><span class="s2"> point styles across all slides.&#34;,                                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score_after_improvements&#34;: 9                                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">   },                                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">   &#34;accessibility&#34;: {                                                                               
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score&#34;: 6,                                                                                    
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;justification&#34;: &#34;Code examples may have small font sizes, and some slides are text-heavy whic 
</span></span></span><span class="line"><span class="cl"><span class="s2"> could be challenging for some viewers&#34;,                                                            
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;improvements&#34;: &#34;Ensure code font size is at least 14pt. Add alt text description for the GIF  
</span></span></span><span class="line"><span class="cl"><span class="s2"> (slide 11). Break up dense paragraphs with more white space and bullet points.&#34;,                   
</span></span></span><span class="line"><span class="cl"><span class="s2">     &#34;score_after_improvements&#34;: 8                                                                  
</span></span></span><span class="line"><span class="cl"><span class="s2">   }                                                                                                
</span></span></span><span class="line"><span class="cl"><span class="s2"> }
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span> 
</span></span></code></pre></td></tr></table>
</div>
</div></details>
</div>
<div id="tabset-9-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># Define prompt file</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./prompts/prompt-analyse-slides-with-improvements.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create system prompt</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt</span> <span class="o">&lt;-</span> <span class="nf">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">path</span> <span class="o">=</span> <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">audience</span> <span class="o">=</span> <span class="n">audience_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">length</span> <span class="o">=</span> <span class="n">length_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">type</span> <span class="o">=</span> <span class="n">type_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span> <span class="o">=</span> <span class="n">event_content</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"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="n">system_prompt</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"># Start conversation with the chat</span>
</span></span><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="nf">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Here are the slides in Markdown: {{ markdown_content }}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show complete code</summary>
<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="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></span><span class="line"><span class="cl"><span class="c1"># Get Quarto presentation and convert to plain Markdown</span>
</span></span><span class="line"><span class="cl"><span class="n">quarto</span><span class="o">::</span><span class="nf">quarto_render</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;./Quarto/my-presentation.qmd&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">output_format</span> <span class="o">=</span> <span class="s">&#34;markdown&#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"># Dynamic data</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Audience, length in minutes, type, and event</span>
</span></span><span class="line"><span class="cl"><span class="n">audience_content</span> <span class="o">&lt;-</span> <span class="s">&#34;Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">length_content</span> <span class="o">&lt;-</span> <span class="s">&#34;10&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">type_content</span> <span class="o">&lt;-</span> <span class="s">&#34;lightning talk&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">event_content</span> <span class="o">&lt;-</span> <span class="s">&#34;posit::conf(2025)&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Read the generated Markdown file containing our slides</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./Quarto/docs/my-presentation.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_content</span> <span class="o">&lt;-</span> <span class="nf">readChar</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">,</span> <span class="nf">file.size</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define prompt file</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./prompts/prompt-analyse-slides-with-improvements.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create system prompt</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt</span> <span class="o">&lt;-</span> <span class="nf">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">path</span> <span class="o">=</span> <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">audience</span> <span class="o">=</span> <span class="n">audience_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">length</span> <span class="o">=</span> <span class="n">length_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">type</span> <span class="o">=</span> <span class="n">type_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span> <span class="o">=</span> <span class="n">event_content</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"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="n">system_prompt</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"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Since all the instructions are in the system prompt, we can just</span>
</span></span><span class="line"><span class="cl"><span class="c1"># provide the Markdown content as a message</span>
</span></span><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="nf">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Here are the slides in Markdown: {{ markdown_content }}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">))</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<details class="code-fold">
<summary>Show output</summary>
<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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;presentation_title&#34;: &#34;The Shiny Side of LLMs&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;total_slides&#34;: 16,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;percent_with_code&#34;: 37.5,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;percent_with_images&#34;: 6.25,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;estimated_duration_minutes&#34;: 14,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;tone&#34;: &#34;Casual, friendly, and practical with a playful touch (emojis, informal </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; language, and humor)&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;clarity&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 7,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Content is generally clear with good code examples and </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; explanations. However, some technical terms could use more definition for the </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; mixed-technical audience.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;improvements&#34;: &#34;Define key terms like &#39;reactive programming&#39; (slide 7), </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; explain what API keys/tokens are (slide 2), and clarify what &#39;system_prompt&#39; does </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; in the code examples (slides 5-6).&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score_after_improvements&#34;: 8</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;   &#34;relevance&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 8,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Excellent match for the audience - Python and R users curious</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; about AI/LLMs. Provides practical, hands-on examples in both languages and focuses </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; on implementation rather than deep theory.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;improvements&#34;: &#34;Add a brief slide explaining what LLMs are and their basic </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; capabilities for audience members completely new to AI (slide 3 area).&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score_after_improvements&#34;: 9</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;   &#34;visual_design&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 5,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Heavy reliance on text and code blocks. Most slides are </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; text-dense with minimal visual elements. Only one image used, and code blocks </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; dominate several slides.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;improvements&#34;: &#34;Add visual diagrams showing the Shiny-LLM integration flow </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; (slide 4), include screenshots of the final app (slide 3), reduce text density on </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; slides 13 and 15, and add icons or visual elements to break up text-heavy slides.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score_after_improvements&#34;: 7</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;   &#34;engagement&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 6,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Casual tone with emojis and humor helps engagement, but heavy</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; code blocks and text-dense slides could lose audience attention. The practical </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; &#39;build along&#39; approach is engaging.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;improvements&#34;: &#34;Add interactive moments like &#39;raise your hand if you&#39;ve used </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Shiny before&#39; (slide 7), include a live demo or video clip of the app in action </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; (slide 11), and break up slide 13 with bullet points or visual elements.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score_after_improvements&#34;: 8</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;   &#34;pacing&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 6,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Some slides are too dense (slides 8, 9, 11, 12, 13) while </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; others are very light (slides 1, 16). The code-heavy slides will take longer to </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; present than estimated.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;improvements&#34;: &#34;Split slide 11 into two slides (one for Python UI, one for </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; server), break slide 13 into key points with visuals, and add more content to the </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; thank you slide (slide 16) with contact info or resources.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score_after_improvements&#34;: 8</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;   &#34;structure&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 8,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Clear logical flow from introduction to requirements to </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; implementation to next steps. Good progression from basic concepts to integration. </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Clear beginning, middle, and end.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;improvements&#34;: &#34;Add a brief agenda slide after slide 1 to preview the journey,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; and include a &#39;key takeaways&#39; slide before the thank you slide.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score_after_improvements&#34;: 9</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;   &#34;concistency&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 7,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Generally consistent tone and formatting. Code blocks are </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; well-formatted. Some inconsistency in slide density and the mix of casual/technical</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; language.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;improvements&#34;: &#34;Standardize slide titles (some are questions, some </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; statements), ensure consistent bullet point formatting across slides, and maintain </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; consistent emoji usage throughout.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score_after_improvements&#34;: 8</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;   &#34;accessibility&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score&#34;: 6,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;justification&#34;: &#34;Code blocks may be challenging to read for some viewers. </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Heavy text density on several slides could be overwhelming. No obvious color </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; contrast issues mentioned.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;improvements&#34;: &#34;Increase font size in code blocks (slides 5-6, 8-9, 11-12), </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; add alt text descriptions for the GIF image (slide 10), reduce text density on </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; slide 13, and ensure sufficient white space between elements.&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;     &#34;score_after_improvements&#34;: 8</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; }</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
</div>
</div>
<p>And of course Claude believes that all the suggested improvements increase the relevant scores. However, don&rsquo;t be surprised if the numbers are off. Remember from <a href="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-2/../../../blog/shiny/shiny-side-of-llms-part-1/">part 1</a>
 that it&rsquo;s just a model that predicts a next token. It&rsquo;s good to do some sanity checks.</p>
<h1 id="ensuring-structured-and-consistent-output">Ensuring structured and consistent output
</h1>
<p>As mentioned previously, specifying an output format makes your outcomes better. That&rsquo;s why we included: <em>&ldquo;Please extract the following information&hellip; Return your answer as a JSON object with the following structure&hellip;&rdquo;</em> in our prompt. Formats like JSON, YAML, and Markdown tables are common, and JSON tends to be a safe bet for both Python and R. It&rsquo;s clean, structured, and easy to parse.</p>
<p>And even though we&rsquo;re explicitly guiding the model toward structured JSON output, we don&rsquo;t always get what we expect: the model might wrap its output in a code block (e.g., ```json), or forget a comma, or produce something that looks like JSON but isn&rsquo;t quite valid. That&rsquo;s weird, right? It&rsquo;s because specifying the format in the prompt is essentially giving the model strong instructions in plain language. You&rsquo;re still relying on the model to follow your instructions correctly and generate the output in that format. This works reasonably well, but it&rsquo;s still a natural language generation task under the hood. And as we learned in part I: it&rsquo;s all just predicting one token after the other. If the probability of a space instead of a comma is higher&hellip; Well, goodbye parsable JSON.</p>
<p>So we need something smarter. That&rsquo;s where something called &ldquo;structured output&rdquo; comes in. Structured output refers to LLMs that support structured schemas as part of the API or function call and not just as instructions in the prompt. These tell the model not just what to output, but how to constrain the generation itself. The model generates a structure directly (like a JSON object or typed dictionary), and the output is guaranteed to match that structure.</p>
<p>The difference is:</p>
<ul>
<li>Prompting: &ldquo;Please follow this format&rdquo; (model might comply)</li>
<li>Structured output: &ldquo;You must generate output that fits this exact shape&rdquo; (enforced)</li>
</ul>
<p>With <code>chatlas</code> and <code>ellmer</code> we can make use of this structured data feature. Bonus: <code>chatlas</code> will convert the response into a structured Python dictionary, and <code>ellmer</code> into an R data structure so it&rsquo;s immediately ready to use!</p>
<p>To return structured data we need to change two things: call <code>chat_structured()</code> and provide a specification. In this specification we can specify the fields, their type, and add a description for each of them. Optionally, to prevent duplication, we can remove the field descriptions from our prompt, and refer to the &ldquo;data model&rdquo; instead:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Always return the result as a JSON object that conforms to the provided data model.
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show full prompt</summary>
<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></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="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">You are a presentation coach for data scientists that analyses presentation slide decks written in Markdown. 
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract key information, evaluate quality, and return structured feedback that is constructive, focused and practical.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">The presentation you are helping with is a {{ length }}-minute {{ type }} at {{ event }}.  
</span></span></span><span class="line"><span class="cl"><span class="s2">The audience is {{ audience }}. 
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract the following information:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">1. The presentation title
</span></span></span><span class="line"><span class="cl"><span class="s2">2. Total number of slides
</span></span></span><span class="line"><span class="cl"><span class="s2">3. Percentage of slides containing code blocks
</span></span></span><span class="line"><span class="cl"><span class="s2">4. Percentage of slides containing images
</span></span></span><span class="line"><span class="cl"><span class="s2">5. Estimated presentation length (in minutes, assuming ~1 minute per text slide and 2–3 minutes per code or image-heavy slide)
</span></span></span><span class="line"><span class="cl"><span class="s2">6. Tone (a brief description)
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You score the presentation on the following categories (from 1–10), and give a concise explanation:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">1. Clarity of content: evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?
</span></span></span><span class="line"><span class="cl"><span class="s2">2. Relevance for intended audience: assess how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?
</span></span></span><span class="line"><span class="cl"><span class="s2">3. Visual design: judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?
</span></span></span><span class="line"><span class="cl"><span class="s2">4. Engagement: estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?
</span></span></span><span class="line"><span class="cl"><span class="s2">5. Pacing: analyze the distribution of content across slides. Are some slides too dense or too light? 
</span></span></span><span class="line"><span class="cl"><span class="s2">6. Structure: review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?
</span></span></span><span class="line"><span class="cl"><span class="s2">7. consistency: evaluatue whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?
</span></span></span><span class="line"><span class="cl"><span class="s2">8. Accessibility: consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">For each of the above scoring categories, provide specific and actionable improvements. Follow these instructions:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">- Keep each suggestion concise and mention the slide number(s) if applicable.
</span></span></span><span class="line"><span class="cl"><span class="s2">- Do not invent issues and only suggest improvements when the content would clearly benefit from them.
</span></span></span><span class="line"><span class="cl"><span class="s2">- For each catogory, estimate what the new score would be if these improvements are implemented.
</span></span></span><span class="line"><span class="cl"><span class="s2">- Return the improvement and new score as part of the response for that category.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Always return the result as a JSON object that conforms to the provided data model.
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<blockquote>
<p><strong>Double work or Don&rsquo;t Repeat Yourself (DRY)?</strong></p>
<p>This approach assumes the model actually has access to the data model definition, either via a tool, API schema, or injected example. Without that, results may be inconsistent. It seems that Claude&rsquo;s results are quite good this way, but it may vary for other models, so it&rsquo;s good to try different things.</p>
<p>For educational purposes it&rsquo;s nice to see that the LLM is getting the required information from the provided data model, and not from the system prompt.</p>
<p>Having both the description in the prompt and in a data model is an option to, especially when:</p>
<ul>
<li>You want strong guidance and fallback validation.</li>
<li>You&rsquo;re building for reliability or multi-model use (not all models treat schemas equally).</li>
<li>You&rsquo;re iterating and want to troubleshoot what part is helping.</li>
</ul>
</blockquote>
<p>We&rsquo;ll come back to why the Don&rsquo;t-Repeat-Yourself (DRY) principle might be worthwhile for the token count and cost too.</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">
<p>To use structured output, you give the model some input and a <a href="https://docs.pydantic.dev/latest/concepts/models/" target="_blank" rel="noopener">Pydantic model</a>
 that describes the kind of data you want. The Pydantic model is like a form with specific fields and types, like <code>title: string</code> or <code>duration: integer</code>.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><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></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"># Define data structure to extract from the input</span>
</span></span><span class="line"><span class="cl"><span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mi">10</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">PercentType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">float</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mf">100.0</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">MinutesType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">SlideCount</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</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="k">class</span> <span class="nc">ScoringCategory</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">score</span><span class="p">:</span> <span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Score from 1–10.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">justification</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Brief explanation of the score.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">improvements</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Concise, actionable improvements, mentioning slide numbers if applicable.&#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">score_after_improvements</span><span class="p">:</span> <span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Estimated score after suggested improvements.&#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="k">class</span> <span class="nc">DeckAnalysis</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">presentation_title</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;The presentation title.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">total_slides</span><span class="p">:</span> <span class="n">SlideCount</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_code</span><span class="p">:</span> <span class="n">PercentType</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_images</span><span class="p">:</span> <span class="n">PercentType</span>
</span></span><span class="line"><span class="cl">    <span class="n">estimated_duration_minutes</span><span class="p">:</span> <span class="n">MinutesType</span>
</span></span><span class="line"><span class="cl">    <span class="n">tone</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Brief description of the tone of the presentation.&#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">clarity</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?&#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">relevance</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Assess how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?&#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">visual_design</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?&#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">engagement</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?&#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">pacing</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Analyze the distribution of content across slides. Are some slides too dense or too light? &#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">structure</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?&#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">concistency</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>  <span class="c1"># spelling kept as-is</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">description</span><span class="o">=</span><span class="s2">&#34;Evaluatue whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?&#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">accessibility</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?&#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"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">ChatAnthropic</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;claude-sonnet-4-20250514&#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="n">system_prompt</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"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Since all the instructions are in the system prompt, we can just</span>
</span></span><span class="line"><span class="cl"><span class="c1"># provide the Markdown content as a message</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">chat_structured</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">interpolate</span><span class="p">(</span><span class="s2">&#34;Here are the slides in Markdown: {{ markdown_content }}&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_model</span><span class="o">=</span><span class="n">DeckAnalysis</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><details class="code-fold">
<summary>Show complete code</summary>
<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><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</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">ChatAnthropic</span><span class="p">,</span> <span class="n">interpolate_file</span><span class="p">,</span> <span class="n">interpolate</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">subprocess</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 class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span><span class="p">,</span> <span class="n">Field</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Annotated</span><span class="p">,</span> <span class="n">Optional</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Get Quarto presentation and convert to plain Markdown</span>
</span></span><span class="line"><span class="cl"><span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="s2">&#34;quarto&#34;</span><span class="p">,</span> <span class="s2">&#34;render&#34;</span><span class="p">,</span> <span class="s2">&#34;./Quarto/my-presentation.qmd&#34;</span><span class="p">,</span> <span class="s2">&#34;--to&#34;</span><span class="p">,</span> <span class="s2">&#34;markdown&#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"># Dynamic data</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Audience, length in minutes, type, and event</span>
</span></span><span class="line"><span class="cl"><span class="n">audience_content</span> <span class="o">=</span> <span class="s2">&#34;Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">length_content</span> <span class="o">=</span> <span class="s2">&#34;10&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">type_content</span> <span class="o">=</span> <span class="s2">&#34;lightning talk&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">event_content</span> <span class="o">=</span> <span class="s2">&#34;posit::conf(2025)&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Read the generated Markdown file containing our slides</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./Quarto/docs/my-presentation.md&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_content</span> <span class="o">=</span> <span class="n">markdown_file</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#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"># Define prompt file</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./prompts/prompt-analyse-slides-structured.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"># Create system prompt</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt</span> <span class="o">=</span> <span class="n">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">variables</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;audience&#34;</span><span class="p">:</span> <span class="n">audience_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;length&#34;</span><span class="p">:</span> <span class="n">length_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="n">type_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;event&#34;</span><span class="p">:</span> <span class="n">event_content</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"># Define data structure to extract from the input</span>
</span></span><span class="line"><span class="cl"><span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mi">10</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">PercentType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">float</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mf">100.0</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">MinutesType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">SlideCount</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</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="k">class</span> <span class="nc">ScoringCategory</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">score</span><span class="p">:</span> <span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Score from 1–10.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">justification</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Brief explanation of the score.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">improvements</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Concise, actionable improvements, mentioning slide numbers if applicable.&#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">score_after_improvements</span><span class="p">:</span> <span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Estimated score after suggested improvements.&#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="k">class</span> <span class="nc">DeckAnalysis</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">presentation_title</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;The presentation title.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">total_slides</span><span class="p">:</span> <span class="n">SlideCount</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_code</span><span class="p">:</span> <span class="n">PercentType</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_images</span><span class="p">:</span> <span class="n">PercentType</span>
</span></span><span class="line"><span class="cl">    <span class="n">estimated_duration_minutes</span><span class="p">:</span> <span class="n">MinutesType</span>
</span></span><span class="line"><span class="cl">    <span class="n">tone</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Brief description of the tone of the presentation.&#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">clarity</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?&#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">relevance</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Assess how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?&#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">visual_design</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?&#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">engagement</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?&#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">pacing</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Analyze the distribution of content across slides. Are some slides too dense or too light? &#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">structure</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?&#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">concistency</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>  <span class="c1"># spelling kept as-is</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">description</span><span class="o">=</span><span class="s2">&#34;Evaluatue whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?&#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">accessibility</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?&#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"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">ChatAnthropic</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;claude-sonnet-4-20250514&#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="n">system_prompt</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"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Since all the instructions are in the system prompt, we can just</span>
</span></span><span class="line"><span class="cl"><span class="c1"># provide the Markdown content as a message</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">chat_structured</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">interpolate</span><span class="p">(</span><span class="s2">&#34;Here are the slides in Markdown: {{ markdown_content }}&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_model</span><span class="o">=</span><span class="n">DeckAnalysis</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></details>
<details class="code-fold">
<summary>Show output</summary>
<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></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="p">{</span><span class="s1">&#39;presentation_title&#39;</span><span class="p">:</span> <span class="s1">&#39;The Shiny Side of LLMs&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;total_slides&#39;</span><span class="p">:</span> <span class="mi">16</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;percent_with_code&#39;</span><span class="p">:</span> <span class="mf">31.25</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;percent_with_images&#39;</span><span class="p">:</span> <span class="mf">6.25</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;estimated_duration_minutes&#39;</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;tone&#39;</span><span class="p">:</span> <span class="s1">&#39;Casual and friendly with a teaching approach, using emojis and humor to make technical content approachable&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;clarity&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Good step-by-step approach and clear explanations, but heavy code blocks may confuse non-technical audience members. Key concepts are well-defined.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s2">&#34;Simplify code examples on slides 5-6, 8-9, 11-12 to show only essential lines with comments. Add brief explanations of technical terms like &#39;reactive programming&#39; on slide 7.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;relevance&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Highly relevant to the posit::conf audience with practical Python and R examples. Addresses real need for AI integration in data science workflows.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s1">&#39;Add a brief mention of common use cases data scientists might have for LLM-powered apps beyond presentation feedback (slide 3 or 14).&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;visual_design&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Very text-heavy with large code blocks that will be difficult to read in a presentation setting. Limited visual hierarchy and only one image/GIF.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s1">&#39;Replace full code blocks on slides 5-6, 8-9, 11-12 with key snippets only. Add diagrams showing the app architecture on slide 4. Include screenshots of DeckCheck app on slide 3.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">7</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;engagement&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">7</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Good use of humor, emojis, and relatable examples (DeckCheck concept). The magic GIF adds personality. However, code-heavy sections may lose audience attention.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s1">&#39;Add interactive elements or polls. Include a brief live demo or recorded demo on slide 10 instead of the GIF. Show before/after examples of presentation feedback.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;pacing&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Uneven distribution with slides 5-6, 8-9, 11-12 being too dense for a 10-minute talk. Some slides like slide 1 and 15 are very light.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s1">&#39;Condense setup information from slides 2-4 into 2 slides. Reduce code examples to key snippets. Combine slides 1 and 2 into a single introduction slide.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">7</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;structure&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">7</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Logical flow from introduction through setup, basic concepts, and implementation. Clear progression from simple to complex. Good use of the DeckCheck example throughout.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s1">&#39;Add an agenda slide after slide 1. Create clearer section breaks between setup (slides 2-4), concepts (slides 5-7), and implementation (slides 8-12).&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;concistency&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Consistent tone, formatting, and use of the DeckCheck example throughout. Parallel structure for Python and R examples works well.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;accessibility&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Code blocks will be difficult to read for audience members, especially those with visual challenges. Font sizes in code examples are likely too small for presentation setting.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s1">&#39;Use larger fonts and fewer lines in code examples (slides 5-6, 8-9, 11-12). Add high contrast between text and background. Include alt text descriptions for the GIF on slide 10.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">7</span><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<p>The code above defines a set of Pydantic models that describe the expected structure of our output.<code>ScoringCategory</code> is a reusable model containing a score (an integer constrained between 0 and 10), a justification string, optional improvement suggestions, and an estimated score after implementing the improvements. Numeric fields in the top-level model, <code>DeckAnalysis</code>, are validated using <code>Annotated</code> types with constraints defined via <code>Field(...)</code> . For example, to ensure percentages are between 0.0 and 100.0, and counts are non-negative. The <code>DeckAnalysis</code> model nests eight <code>ScoringCategory</code> models. It saves some duplication. The top-level model is given to the <code>chat_structured()</code> method: <code>chat.chat_structured(interpolate(&quot;Here are the slides in Markdown: {{ markdown_content }}&quot;), data_model=DeckAnalysis)</code> .</p>
</div>
<div id="tabset-10-2">
<p>To use structured output, you use <code>$chat_structured()</code> instead of the <code>$chat()</code>method. You&rsquo;ll also need to define a type specification that describes the structure of the data that you want.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><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></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"># Reusable scoring category</span>
</span></span><span class="line"><span class="cl"><span class="n">type_scoring_category</span> <span class="o">&lt;-</span> <span class="nf">type_object</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">score</span> <span class="o">=</span> <span class="nf">type_integer</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;Score from 1 to 10.&#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">justification</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Brief explanation of the score.&#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">improvements</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Concise, actionable improvements, mentioning slide numbers if applicable.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">required</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">score_after_improvements</span> <span class="o">=</span> <span class="nf">type_integer</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;Estimated score after suggested improvements.&#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></span><span class="line"><span class="cl"><span class="c1"># Top-level deck analysis object</span>
</span></span><span class="line"><span class="cl"><span class="n">type_deck_analysis</span> <span class="o">&lt;-</span> <span class="nf">type_object</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">presentation_title</span> <span class="o">=</span> <span class="nf">type_string</span><span class="p">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">&#34;The presentation title.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">total_slides</span> <span class="o">=</span> <span class="nf">type_integer</span><span class="p">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Total number of slides.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">percent_with_code</span> <span class="o">=</span> <span class="nf">type_number</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;Percentage of slides containing code blocks (0–100).&#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">percent_with_images</span> <span class="o">=</span> <span class="nf">type_number</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;Percentage of slides containing images (0–100).&#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">estimated_duration_minutes</span> <span class="o">=</span> <span class="nf">type_integer</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;Estimated presentation length in minutes, assuming ~1 minute per text slide and 2–3 minutes per code or image-heavy slide.&#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">tone</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Brief description of the presentation tone (e.g., informal, technical, playful).&#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">clarity</span> <span class="o">=</span> <span class="nf">type_array</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;Evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">relevance</span> <span class="o">=</span> <span class="nf">type_array</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;Asses how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">visual_design</span> <span class="o">=</span> <span class="nf">type_array</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;Judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">engagement</span> <span class="o">=</span> <span class="nf">type_array</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;Estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">pacing</span> <span class="o">=</span> <span class="nf">type_array</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;Analyze the distribution of content across slides. Are some slides too dense or too light? &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">structure</span> <span class="o">=</span> <span class="nf">type_array</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;Review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">concistency</span> <span class="o">=</span> <span class="nf">type_array</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;Evaluate whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">accessibility</span> <span class="o">=</span> <span class="nf">type_array</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;Consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</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"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="n">system_prompt</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"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Since all the instructions are in the system prompt, we can just</span>
</span></span><span class="line"><span class="cl"><span class="c1"># provide the Markdown content as a message</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">$</span><span class="nf">chat_structured</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nf">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;Here are the slides in Markdown: {{ markdown_content }}&#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">type</span> <span class="o">=</span> <span class="n">type_deck_analysis</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show complete code</summary>
<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><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</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></span><span class="line"><span class="cl"><span class="c1"># Get Quarto presentation and convert to plain Markdown</span>
</span></span><span class="line"><span class="cl"><span class="n">quarto</span><span class="o">::</span><span class="nf">quarto_render</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;./Quarto/my-presentation.qmd&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">output_format</span> <span class="o">=</span> <span class="s">&#34;markdown&#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"># Dynamic data</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Audience, length in minutes, type, and event</span>
</span></span><span class="line"><span class="cl"><span class="n">audience_content</span> <span class="o">&lt;-</span> <span class="s">&#34;Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">length_content</span> <span class="o">&lt;-</span> <span class="s">&#34;10&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">type_content</span> <span class="o">&lt;-</span> <span class="s">&#34;lightning talk&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">event_content</span> <span class="o">&lt;-</span> <span class="s">&#34;posit::conf(2025)&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Read the generated Markdown file containing our slides</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./Quarto/docs/my-presentation.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_content</span> <span class="o">&lt;-</span> <span class="nf">readChar</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">,</span> <span class="nf">file.size</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define prompt file</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./prompts/prompt-analyse-slides-structured.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create system prompt</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt</span> <span class="o">&lt;-</span> <span class="nf">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">path</span> <span class="o">=</span> <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">audience</span> <span class="o">=</span> <span class="n">audience_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">length</span> <span class="o">=</span> <span class="n">length_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">type</span> <span class="o">=</span> <span class="n">type_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span> <span class="o">=</span> <span class="n">event_content</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"># Reusable scoring category</span>
</span></span><span class="line"><span class="cl"><span class="n">type_scoring_category</span> <span class="o">&lt;-</span> <span class="nf">type_object</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">score</span> <span class="o">=</span> <span class="nf">type_integer</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;Score from 1 to 10.&#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">justification</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Brief explanation of the score.&#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">improvements</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Concise, actionable improvements, mentioning slide numbers if applicable.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">required</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">score_after_improvements</span> <span class="o">=</span> <span class="nf">type_integer</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;Estimated score after suggested improvements.&#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></span><span class="line"><span class="cl"><span class="c1"># Top-level deck analysis object</span>
</span></span><span class="line"><span class="cl"><span class="n">type_deck_analysis</span> <span class="o">&lt;-</span> <span class="nf">type_object</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">presentation_title</span> <span class="o">=</span> <span class="nf">type_string</span><span class="p">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">&#34;The presentation title.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">total_slides</span> <span class="o">=</span> <span class="nf">type_integer</span><span class="p">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Total number of slides.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">percent_with_code</span> <span class="o">=</span> <span class="nf">type_number</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;Percentage of slides containing code blocks (0–100).&#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">percent_with_images</span> <span class="o">=</span> <span class="nf">type_number</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;Percentage of slides containing images (0–100).&#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">estimated_duration_minutes</span> <span class="o">=</span> <span class="nf">type_integer</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;Estimated presentation length in minutes, assuming ~1 minute per text slide and 2–3 minutes per code or image-heavy slide.&#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">tone</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Brief description of the presentation tone (e.g., informal, technical, playful).&#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">clarity</span> <span class="o">=</span> <span class="nf">type_array</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;Evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">relevance</span> <span class="o">=</span> <span class="nf">type_array</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;Asses how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">visual_design</span> <span class="o">=</span> <span class="nf">type_array</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;Judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">engagement</span> <span class="o">=</span> <span class="nf">type_array</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;Estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">pacing</span> <span class="o">=</span> <span class="nf">type_array</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;Analyze the distribution of content across slides. Are some slides too dense or too light? &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">structure</span> <span class="o">=</span> <span class="nf">type_array</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;Review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">concistency</span> <span class="o">=</span> <span class="nf">type_array</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;Evaluate whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">accessibility</span> <span class="o">=</span> <span class="nf">type_array</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;Consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</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"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="n">system_prompt</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"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Since all the instructions are in the system prompt, we can just</span>
</span></span><span class="line"><span class="cl"><span class="c1"># provide the Markdown content as a message</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">$</span><span class="nf">chat_structured</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nf">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;Here are the slides in Markdown: {{ markdown_content }}&#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">type</span> <span class="o">=</span> <span class="n">type_deck_analysis</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<details class="code-fold">
<summary>Show output</summary>
<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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; $presentation_title</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] &#34;The Shiny Side of LLMs&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $total_slides</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] 16</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $percent_with_code</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] 56.25</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $percent_with_images</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] 6.25</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $estimated_duration_minutes</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] 12</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $tone</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] &#34;Informal, friendly, and technical with playful elements&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $clarity</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     7</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Content is generally clear with good step-by-step explanations, but some technical concepts could benefit from more context for non-technical audience members.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Add brief explanations of what Shiny and LLMs are in slides 2-3. Define technical terms like &#39;reactive programming&#39; in slide 7. Provide more context about API keys and authentication in slide 2.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        8</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $relevance</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     8</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Content is highly relevant to the Python and R user audience at posit::conf, with practical examples in both languages. Good mix of beginner-friendly and intermediate concepts.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Add a brief slide explaining why LLMs are relevant for data scientists specifically. Include more context about when to use LLM-powered apps in data science workflows.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        9</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $visual_design</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     6</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Code blocks are well-formatted, but slides are text-heavy with minimal visual hierarchy. Only one image used throughout the presentation.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Add visual diagrams showing the app architecture in slides 4-5. Use bullet points and icons to break up text in slides 2 and 15. Consider adding screenshots of the actual app interface.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        8</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $engagement</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     7</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Good use of emojis and casual tone keeps it engaging. The magic GIF adds personality, but more interactive elements could enhance engagement.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Add a live demo or screenshots of the working app. Include audience interaction prompts in slides 11-12. Add visual progress indicators showing the app development journey.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        8</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $pacing</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     6</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Some slides are code-heavy (slides 8-9, 12-13) while others are very light (slide 3). The transition from basics to implementation could be smoother.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Split the heavy code slides (8-9, 12-13) into smaller chunks. Add transition slides between major sections. Balance slide 14 with more concrete examples.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        7</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $structure</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     8</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Clear logical flow from introduction to requirements to implementation. Good progression from basic concepts to advanced integration.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Add an agenda slide after slide 1. Include a recap slide before the conclusion. Make the transition from slide 14 to 15 smoother with a bridge sentence.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        9</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $concistency</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     7</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Generally consistent formatting and tone, but code highlighting varies between Python and R sections. Some inconsistency in slide structure.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Ensure consistent code block styling across Python and R examples. Standardize the format for requirement lists in slide 2. Use consistent header styles throughout.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        8</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $accessibility</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     7</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Good contrast in code blocks and readable font sizes. However, the single GIF lacks alt text description and some code blocks are quite dense.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Add alt text description for the magic GIF in slide 11. Break up dense code blocks in slides 8-9, 12-13. Use larger font sizes for code comments.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        8</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<p>You can also specify the full schema that you want to get back from the LLM as a JSON schema. This could be handy if you&rsquo;re not keen on manually converting everything using the <code>type_*()</code>functions (or if an LLM doesn&rsquo;t provide you with the correct output after asking 6 times). Your friend: <code>type_from_schema()</code>.</p>
<blockquote>
<p><strong>Tip</strong></p>
<p><code>type_object()</code> returns a named list in R. If you want to extract a data frame from a single prompt (e.g. reading information from a markdown table), you can wrap <code>type_object()</code> in <code>type_array()</code> and create an array of objects. We do something similar here for the scoring categories. It can be hard to wrap your head around as an R user. The <code>ellmer</code> documentation contains some more <a href="https://ellmer.tidyverse.org/articles/structured-data.html#data-frames" target="_blank" rel="noopener">good examples</a>
.</p>
</blockquote>
</div>
</div>
<h1 id="when-llms-guess-tools-know">When LLMs guess, tools know
</h1>
<p>As you&rsquo;ve seen in our conversation with Claude so far, LLMs struggle with tasks like counting slides. This is because they don&rsquo;t actually run code or parse content structurally like a proper parser would. LLMs aren&rsquo;t doing magic, they are simply predicting the next word based on patterns in text. So when you ask an LLM, &ldquo;how many slides contain code?&rdquo;, it&rsquo;s making an educated guess based on the wording of the slides. It does not actually scan them line-by-line and checking for code blocks. The result: inconsistent answers. In our example, there are 16 slides, 6 with code, and 1 with an image (aka 37.5% code slides and 6.25% image slides). The LLM gets close, but its answers vary and we&rsquo;re not aiming for &ldquo;close enough.&rdquo; The solution: build something on top of the LLM that does this reliably for us. Hello something fancy called &ldquo;tool calling&rdquo;!</p>
<p>Everyone talks about tool calling these days, and for a good reason: it&rsquo;s that extra bit of power you give to an LLM. While it sounds complicated, it&rsquo;s easy to explain: you let your code handle tasks that the LLM struggles with, like accurately counting how many slides have code or images, and then the LLM uses those exact numbers. You basically help the LLM with its answer. This help comes in the form of a tool (aka a function) that you&rsquo;ve written and exposed to the model. Of course the LLM needs to know that the tool is there to begin with, and it needs to know how to use it. Luckily <code>chatlas</code> and <code>ellmer</code> can help us doing that.</p>
<p>So how would this work of we want to improve the slide counts for DeckCheck? First, we construct a simple Python or R function that reads our slides, spots code blocks and images, and gives exact percentages. To make our life a little bit easier, we&rsquo;re opting for the HTML version of Quarto slides here, and not Markdown. In this case, HTML is handy because each slide is neatly contained in a <code>&lt;section&gt;</code> tag, and code is clearly marked with a <code>sourceCode</code> class. For an LLM, HTML is noisy (full of CSS, scripts, and other distractions). But for a parsing function like we&rsquo;re going to write, it&rsquo;s straightforward to process. The nice thing about such a function is that you can fully customise it: maybe you only want to count Python or R code chunks only, or ignore very short examples. We call our function <code>calculate_slide_metric</code>:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-11" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-11-1">Python</a></li>
<li><a href="#tabset-11-2">R</a></li>
</ul>
<div id="tabset-11-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><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></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">calculate_slide_metric</span><span class="p">(</span><span class="n">metric</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Union</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">float</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">    Calculates the total number of slides, percentage of slides with code blocks,
</span></span></span><span class="line"><span class="cl"><span class="s2">    and percentage of slides with images in a Quarto presentation HTML file.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    Parameters
</span></span></span><span class="line"><span class="cl"><span class="s2">    ----------
</span></span></span><span class="line"><span class="cl"><span class="s2">    metric : str
</span></span></span><span class="line"><span class="cl"><span class="s2">        The metric to calculate: &#34;total_slides&#34; for total number of slides,
</span></span></span><span class="line"><span class="cl"><span class="s2">        &#34;code&#34; for percentage of slides containing fenced code blocks,
</span></span></span><span class="line"><span class="cl"><span class="s2">        or &#34;images&#34; for percentage of slides containing images.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    Returns
</span></span></span><span class="line"><span class="cl"><span class="s2">    -------
</span></span></span><span class="line"><span class="cl"><span class="s2">    float or int
</span></span></span><span class="line"><span class="cl"><span class="s2">        The calculated metric value.
</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="n">html_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./Quarto/docs/my-presentation.html&#34;</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">html_file</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">FileNotFoundError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;HTML file </span><span class="si">{</span><span class="n">html_file</span><span class="si">}</span><span class="s2"> does not exist.&#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"># Read HTML file</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="n">html_file</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#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">html_content</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Split on &lt;section&gt; tags to get individual slides</span>
</span></span><span class="line"><span class="cl">    <span class="n">slides</span> <span class="o">=</span> <span class="n">html_content</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;&lt;section&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">total_slides</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">slides</span><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="n">metric</span> <span class="o">==</span> <span class="s2">&#34;total_slides&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">total_slides</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="n">metric</span> <span class="o">==</span> <span class="s2">&#34;code&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">slides_with_code</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="s1">&#39;class=&#34;sourceCode&#34;&#39;</span> <span class="ow">in</span> <span class="n">slide</span> <span class="k">for</span> <span class="n">slide</span> <span class="ow">in</span> <span class="n">slides</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="nb">round</span><span class="p">((</span><span class="n">slides_with_code</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="n">metric</span> <span class="o">==</span> <span class="s2">&#34;images&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">slides_with_image</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="s2">&#34;&lt;img&#34;</span> <span class="ow">in</span> <span class="n">slide</span> <span class="k">for</span> <span class="n">slide</span> <span class="ow">in</span> <span class="n">slides</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="nb">round</span><span class="p">((</span><span class="n">slides_with_image</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;Unknown metric: choose &#39;total_slides&#39;, &#39;code&#39;, or &#39;images&#39;&#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">result</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-11-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><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></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">#&#39; Calculates the total number of slides, percentage of slides with code blocks,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; and percentage of slides with images in a Quarto presentation HTML file.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; @param metric The metric to calculate: &#34;total_slides&#34; for total number of slides,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; &#34;code&#34; for percentage of slides containing fenced code blocks, or &#34;images&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; for percentage of slides containing images.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; @return The calculated metric value.</span>
</span></span><span class="line"><span class="cl"><span class="n">calculate_slide_metric</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">metric</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">html_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./Quarto/docs/my-presentation.html&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">file.exists</span><span class="p">(</span><span class="n">html_file</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">stop</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;HTML file does not exist. Please render your Quarto presentation first.&#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="c1"># Read HTML file</span>
</span></span><span class="line"><span class="cl">  <span class="n">html_content</span> <span class="o">&lt;-</span> <span class="nf">readChar</span><span class="p">(</span><span class="n">html_file</span><span class="p">,</span> <span class="nf">file.size</span><span class="p">(</span><span class="n">html_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Split on &lt;section&gt; tags to get individual slides</span>
</span></span><span class="line"><span class="cl">  <span class="n">slides</span> <span class="o">&lt;-</span> <span class="nf">unlist</span><span class="p">(</span><span class="nf">strsplit</span><span class="p">(</span><span class="n">html_content</span><span class="p">,</span> <span class="s">&#34;&lt;section&#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">total_slides</span> <span class="o">&lt;-</span> <span class="nf">length</span><span class="p">(</span><span class="n">slides</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">if</span> <span class="p">(</span><span class="n">metric</span> <span class="o">==</span> <span class="s">&#34;total_slides&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">&lt;-</span> <span class="n">total_slides</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">metric</span> <span class="o">==</span> <span class="s">&#34;code&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Count slides where we see the &#34;sourceCode&#34; class</span>
</span></span><span class="line"><span class="cl">    <span class="n">slides_with_code</span> <span class="o">&lt;-</span> <span class="nf">sum</span><span class="p">(</span><span class="nf">grepl</span><span class="p">(</span><span class="s">&#39;class=&#34;sourceCode&#34;&#39;</span><span class="p">,</span> <span class="n">slides</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">&lt;-</span> <span class="nf">round</span><span class="p">((</span><span class="n">slides_with_code</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="m">100</span><span class="p">,</span> <span class="m">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">metric</span> <span class="o">==</span> <span class="s">&#34;images&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Count slides with image tag</span>
</span></span><span class="line"><span class="cl">    <span class="n">slides_with_image</span> <span class="o">&lt;-</span> <span class="nf">sum</span><span class="p">(</span><span class="nf">grepl</span><span class="p">(</span><span class="s">&#39;&lt;img&#39;</span><span class="p">,</span> <span class="n">slides</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">&lt;-</span> <span class="nf">round</span><span class="p">((</span><span class="n">slides_with_image</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="m">100</span><span class="p">,</span> <span class="m">2</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="nf">stop</span><span class="p">(</span><span class="s">&#34;Unknown metric: choose &#39;total_slides&#39;, &#39;code&#39;, or &#39;images&#39;&#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="kr">return</span><span class="p">(</span><span class="n">result</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>
<p>This function is the tool the LLM can call to accurately count slides. It has one input argument: <code>metric</code>. To let the model know that this tool is available, we register it with <code>register_tool</code> before we start talking to the LLM.</p>
<blockquote>
<p><strong>Who does the work?</strong></p>
<p>Note that the LLM itself is not going to read the HTML file. The underlying Python or R session that execute the function is.</p>
</blockquote>
<p>There&rsquo;s a little catch though: structured data extraction automatically disables tool calling. Hmm&hellip; But we used structured data! Luckily you can work around this by doing a regular <code>chat()</code> and then using <code>chat_structured()</code>. We&rsquo;re basically adding an extra pair of user and assistant turn by dividing one task into two. This means we need to reorganise our system prompt by telling the LLM that there is a first task, and a subsequent task. The first one going to focus on retrieving the meta data, and the second one on extracting scores and improvements. For clarity, the tasks are numbered and have short alias (e.g. &ldquo;Task 1 (counts)&rdquo;). This makes it easier to reference them in our user prompts.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">You can be asked for one of the following tasks. Each has a number and a name:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># Task 1 (counts)
</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">Return only the JSON results, nothing else.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gh"># Task 2 (suggestions)
</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">You bundle your results with the results from the first task. Always return the result as a JSON object that conforms to the provided data model.
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show full prompt</summary>
<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></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="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">You are a presentation coach for data scientists that analyses presentation slide decks written in Markdown. 
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract key information, evaluate quality, and return structured feedback that is constructive, focused and practical.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">The presentation you are helping with is a {{ length }}-minute {{ type }} at {{ event }}.  
</span></span></span><span class="line"><span class="cl"><span class="s2">The audience is {{ audience }}. 
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You can be asked for one of the following tasks. Each has a number and a name:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2"># Task 1 (counts)
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract the following information:
</span></span></span><span class="line"><span class="cl"><span class="s2">- The number of slides
</span></span></span><span class="line"><span class="cl"><span class="s2">- The percentage of slides containing code blocks
</span></span></span><span class="line"><span class="cl"><span class="s2">- The percentage of slides containing images 
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Return only the JSON results, nothing else.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2"># Task 2 (suggestions)
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract the following information:
</span></span></span><span class="line"><span class="cl"><span class="s2">- The presentation title
</span></span></span><span class="line"><span class="cl"><span class="s2">- Estimated presentation length (in minutes, assuming ~1 minute per text slide and 2–3 minutes per code or image-heavy slide)
</span></span></span><span class="line"><span class="cl"><span class="s2">- Tone (a brief description)
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You score the presentation on the following categories (from 1–10), and give a concise explanation:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">1. Clarity of content: evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?
</span></span></span><span class="line"><span class="cl"><span class="s2">2. Relevance for intended audience: assess how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?
</span></span></span><span class="line"><span class="cl"><span class="s2">3. Visual design: judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?
</span></span></span><span class="line"><span class="cl"><span class="s2">4. Engagement: estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?
</span></span></span><span class="line"><span class="cl"><span class="s2">5. Pacing: analyze the distribution of content across slides. Are some slides too dense or too light? 
</span></span></span><span class="line"><span class="cl"><span class="s2">6. Structure: review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?
</span></span></span><span class="line"><span class="cl"><span class="s2">7. consistency: evaluatue whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?
</span></span></span><span class="line"><span class="cl"><span class="s2">8. Accessibility: consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">For each of the above scoring categories, provide specific and actionable improvements. Follow these instructions:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">- Keep each suggestion concise and mention the slide number(s) if applicable.
</span></span></span><span class="line"><span class="cl"><span class="s2">- Do not invent issues and only suggest improvements when the content would clearly benefit from them.
</span></span></span><span class="line"><span class="cl"><span class="s2">- For each catogory, estimate what the new score would be if these improvements are implemented.
</span></span></span><span class="line"><span class="cl"><span class="s2">- Return the improvement and new score as part of the response for that category.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You bundle your results with the results from the first task. Always return the result as a JSON object that conforms to the provided data model.
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<p>Because the <code>chat</code> object stateful (and therefore remembers previous responses), it can use the response from the first prompt for the response of the second one. Handy!</p>
<p>Altogether, this results in the following workflow:</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-12" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-12-1">Python</a></li>
<li><a href="#tabset-12-2">R</a></li>
</ul>
<div id="tabset-12-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><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="c1"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">ChatAnthropic</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;claude-sonnet-4-20250514&#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;Your system prompt&#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"># Register the tool with the chat</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">calculate_slide_metric</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Task 1: regular chat to extract meta-data</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></span><span class="line"><span class="cl">    <span class="n">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Execute Task 1 (counts). Here are the slides in Markdown: ...&#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></span><span class="line"><span class="cl"><span class="c1"># Task 2: structured chat to further analyse the slides</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">chat_structured</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Execute Task 2 (suggestions)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_model</span><span class="o">=</span><span class="n">DeckAnalysis</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><details class="code-fold">
<summary>Show complete code</summary>
<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><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span><span class="lnt">153
</span><span class="lnt">154
</span><span class="lnt">155
</span><span class="lnt">156
</span><span class="lnt">157
</span><span class="lnt">158
</span><span class="lnt">159
</span><span class="lnt">160
</span><span class="lnt">161
</span><span class="lnt">162
</span><span class="lnt">163
</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">ChatAnthropic</span><span class="p">,</span> <span class="n">interpolate_file</span><span class="p">,</span> <span class="n">interpolate</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">subprocess</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 class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span><span class="p">,</span> <span class="n">Field</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Annotated</span><span class="p">,</span> <span class="n">Optional</span><span class="p">,</span> <span class="n">Union</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Get Quarto presentation and convert to plain Markdown + HTML</span>
</span></span><span class="line"><span class="cl"><span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="s2">&#34;quarto&#34;</span><span class="p">,</span> <span class="s2">&#34;render&#34;</span><span class="p">,</span> <span class="s2">&#34;./Quarto/my-presentation.qmd&#34;</span><span class="p">,</span> <span class="s2">&#34;--to&#34;</span><span class="p">,</span> <span class="s2">&#34;markdown,html&#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"># Dynamic data</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Audience, length in minutes, type, and event</span>
</span></span><span class="line"><span class="cl"><span class="n">audience_content</span> <span class="o">=</span> <span class="s2">&#34;Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">length_content</span> <span class="o">=</span> <span class="s2">&#34;10&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">type_content</span> <span class="o">=</span> <span class="s2">&#34;lightning talk&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">event_content</span> <span class="o">=</span> <span class="s2">&#34;posit::conf(2025)&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Read the generated Markdown file containing our slides</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./Quarto/docs/my-presentation.md&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_content</span> <span class="o">=</span> <span class="n">markdown_file</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#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"># Define prompt file</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./prompts/prompt-analyse-slides-structured-tool.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"># Create system prompt</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt</span> <span class="o">=</span> <span class="n">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">variables</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;audience&#34;</span><span class="p">:</span> <span class="n">audience_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;length&#34;</span><span class="p">:</span> <span class="n">length_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="n">type_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;event&#34;</span><span class="p">:</span> <span class="n">event_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;markdown_content&#34;</span><span class="p">:</span> <span class="n">markdown_content</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"># Define data structure to extract from the input</span>
</span></span><span class="line"><span class="cl"><span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mi">10</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">PercentType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">float</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mf">100.0</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">MinutesType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">SlideCount</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</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="k">class</span> <span class="nc">ScoringCategory</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">score</span><span class="p">:</span> <span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Score from 1–10.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">justification</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Brief explanation of the score.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">improvements</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Concise, actionable improvements, mentioning slide numbers if applicable.&#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">score_after_improvements</span><span class="p">:</span> <span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Estimated score after suggested improvements.&#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="k">class</span> <span class="nc">DeckAnalysis</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">presentation_title</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;The presentation title.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">total_slides</span><span class="p">:</span> <span class="n">SlideCount</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_code</span><span class="p">:</span> <span class="n">PercentType</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_images</span><span class="p">:</span> <span class="n">PercentType</span>
</span></span><span class="line"><span class="cl">    <span class="n">estimated_duration_minutes</span><span class="p">:</span> <span class="n">MinutesType</span>
</span></span><span class="line"><span class="cl">    <span class="n">tone</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Brief description of the tone of the presentation.&#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">clarity</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?&#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">relevance</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Assess how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?&#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">visual_design</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?&#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">engagement</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?&#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">pacing</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Analyze the distribution of content across slides. Are some slides too dense or too light? &#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">structure</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?&#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">concistency</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>  <span class="c1"># spelling kept as-is</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">description</span><span class="o">=</span><span class="s2">&#34;Evaluatue whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?&#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">accessibility</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?&#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"># Define a tool to calculate some metrics</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Start with a function:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">calculate_slide_metric</span><span class="p">(</span><span class="n">metric</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Union</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">float</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">    Calculates the total number of slides, percentage of slides with code blocks,
</span></span></span><span class="line"><span class="cl"><span class="s2">    and percentage of slides with images in a Quarto presentation HTML file.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    Parameters
</span></span></span><span class="line"><span class="cl"><span class="s2">    ----------
</span></span></span><span class="line"><span class="cl"><span class="s2">    metric : str
</span></span></span><span class="line"><span class="cl"><span class="s2">        The metric to calculate: &#34;total_slides&#34; for total number of slides,
</span></span></span><span class="line"><span class="cl"><span class="s2">        &#34;code&#34; for percentage of slides containing fenced code blocks,
</span></span></span><span class="line"><span class="cl"><span class="s2">        or &#34;images&#34; for percentage of slides containing images.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    Returns
</span></span></span><span class="line"><span class="cl"><span class="s2">    -------
</span></span></span><span class="line"><span class="cl"><span class="s2">    float or int
</span></span></span><span class="line"><span class="cl"><span class="s2">        The calculated metric value.
</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="n">html_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./Quarto/docs/my-presentation.html&#34;</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">html_file</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">FileNotFoundError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;HTML file </span><span class="si">{</span><span class="n">html_file</span><span class="si">}</span><span class="s2"> does not exist.&#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"># Read HTML file</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="n">html_file</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#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">html_content</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Split on &lt;section&gt; tags to get individual slides</span>
</span></span><span class="line"><span class="cl">    <span class="n">slides</span> <span class="o">=</span> <span class="n">html_content</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;&lt;section&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">total_slides</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">slides</span><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="n">metric</span> <span class="o">==</span> <span class="s2">&#34;total_slides&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">total_slides</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="n">metric</span> <span class="o">==</span> <span class="s2">&#34;code&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">slides_with_code</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="s1">&#39;class=&#34;sourceCode&#34;&#39;</span> <span class="ow">in</span> <span class="n">slide</span> <span class="k">for</span> <span class="n">slide</span> <span class="ow">in</span> <span class="n">slides</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="nb">round</span><span class="p">((</span><span class="n">slides_with_code</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="n">metric</span> <span class="o">==</span> <span class="s2">&#34;images&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">slides_with_image</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="s2">&#34;&lt;img&#34;</span> <span class="ow">in</span> <span class="n">slide</span> <span class="k">for</span> <span class="n">slide</span> <span class="ow">in</span> <span class="n">slides</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="nb">round</span><span class="p">((</span><span class="n">slides_with_image</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;Unknown metric: choose &#39;total_slides&#39;, &#39;code&#39;, or &#39;images&#39;&#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">result</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="n">ChatAnthropic</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;claude-sonnet-4-20250514&#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="n">system_prompt</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"># Register the tool with the chat</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">calculate_slide_metric</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Task 1: regular chat to extract meta-data</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></span><span class="line"><span class="cl">    <span class="n">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Execute Task 1 (counts). Here are the slides in Markdown: {{ markdown_content }}&#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></span><span class="line"><span class="cl"><span class="c1"># Task 2: structured chat to further analyse the slides</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">chat_structured</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Execute Task 2 (suggestions)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">data_model</span><span class="o">=</span><span class="n">DeckAnalysis</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></details>
<p>We can clearly see that our tool was being called 3 times for our 3 metrics, just like it should:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</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"># 🔧 tool request (toolu_01JSsxrB8Jj9yDY35pg1g1bc)                      </span>
</span></span><span class="line"><span class="cl"> <span class="n">calculate_slide_metric</span><span class="p">(</span><span class="n">metric</span><span class="o">=</span><span class="n">total_slides</span><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></span><span class="line"><span class="cl"> <span class="c1"># ✅ tool result (toolu_01JSsxrB8Jj9yDY35pg1g1bc)                        </span>
</span></span><span class="line"><span class="cl"> <span class="mi">16</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="c1"># 🔧 tool request (toolu_019635a7mRAotqTFNx2TfCsm)                      </span>
</span></span><span class="line"><span class="cl"> <span class="n">calculate_slide_metric</span><span class="p">(</span><span class="n">metric</span><span class="o">=</span><span class="n">code</span><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></span><span class="line"><span class="cl"> <span class="c1"># ✅ tool result (toolu_019635a7mRAotqTFNx2TfCsm)                        </span>
</span></span><span class="line"><span class="cl"> <span class="mf">37.5</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="c1"># 🔧 tool request (toolu_01Kv6nPiUeZmABADZFeN8Qgi)                      </span>
</span></span><span class="line"><span class="cl"> <span class="n">calculate_slide_metric</span><span class="p">(</span><span class="n">metric</span><span class="o">=</span><span class="n">images</span><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></span><span class="line"><span class="cl"> <span class="c1"># ✅ tool result (toolu_01Kv6nPiUeZmABADZFeN8Qgi)                       </span>
</span></span><span class="line"><span class="cl"> <span class="mf">6.25</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="p">{</span>                                                                       
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;total_slides&#34;</span><span class="p">:</span> <span class="mi">16</span><span class="p">,</span>                                                   
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;percentage_slides_with_code&#34;</span><span class="p">:</span> <span class="mf">37.5</span><span class="p">,</span>                                  
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;percentage_slides_with_images&#34;</span><span class="p">:</span> <span class="mf">6.25</span>                                 
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show the rest of the output</summary>
<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></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="p">{</span><span class="s1">&#39;presentation_title&#39;</span><span class="p">:</span> <span class="s1">&#39;The Shiny Side of LLMs&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;total_slides&#39;</span><span class="p">:</span> <span class="mi">16</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;percent_with_code&#39;</span><span class="p">:</span> <span class="mf">37.5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;percent_with_images&#39;</span><span class="p">:</span> <span class="mf">6.25</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;estimated_duration_minutes&#39;</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;tone&#39;</span><span class="p">:</span> <span class="s1">&#39;Conversational and friendly with technical depth, using emojis and casual language to make complex topics approachable&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;clarity&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Clear explanations with step-by-step progression from basics to implementation. Code examples are well-structured and concepts are explained in accessible terms.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s2">&#34;Add brief explanations of what chatlas/ellmer are on slides 5-6. Define &#39;reactive programming&#39; on slide 7 for non-technical audience members.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">9</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;relevance&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">9</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Highly relevant to the posit::conf audience of Python/R users interested in AI. Content matches the technical level and practical focus expected.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s1">&#39;Consider adding a brief mention of when NOT to use LLMs in presentations on slide 14 to provide balanced perspective.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">9</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;visual_design&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Slides are text-heavy with large code blocks. Only one visual element (GIF on slide 11). Layout is clean but could benefit from more visual variety.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s1">&#39;Add diagrams showing app architecture on slides 4 or 8. Break up dense text on slide 14 into bullet points. Consider visual examples of the DeckCheck app interface.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;engagement&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">7</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Good use of emojis, casual tone, and interactive elements. The GIF adds humor and the practical demo concept is engaging.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s1">&#39;Add a live demo screenshot or mockup of the DeckCheck app on slide 3. Include audience interaction prompts on slides 2-3 asking about their presentation challenges.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;pacing&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">7</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Generally well-paced but some slides are dense (slides 8-9, 12-13) while others are light (slide 11). Code-heavy slides may take longer to explain.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s1">&#39;Split slide 14 into two slides - one for UX principles, one for error handling. Reduce code on slides 8-9 to focus on key concepts only.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;structure&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Clear logical flow from introduction to requirements to implementation to next steps. Good narrative arc building toward a complete solution.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s2">&#34;Add a brief agenda/outline slide after slide 1 to preview the journey. Include a summary slide before &#39;Thank you&#39; recapping key takeaways.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">9</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;concistency&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Consistent formatting, tone, and code style throughout. Good parallel structure between Python and R examples.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s1">&#39;Ensure consistent capitalization in slide titles (slide 11 uses title case while others use sentence case). Standardize code comment styles across examples.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">9</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="s1">&#39;accessibility&#39;</span><span class="p">:</span> <span class="p">{</span><span class="s1">&#39;score&#39;</span><span class="p">:</span> <span class="mi">7</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;justification&#39;</span><span class="p">:</span> <span class="s1">&#39;Text appears readable but code blocks are quite dense. Good contrast likely but some slides have significant cognitive load.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;improvements&#39;</span><span class="p">:</span> <span class="s1">&#39;Increase font size in code blocks on slides 5-6, 8-9, 12-13. Add alt text description for the GIF on slide 11. Use consistent heading hierarchy.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;score_after_improvements&#39;</span><span class="p">:</span> <span class="mi">8</span><span class="p">}}</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
</div>
<div id="tabset-12-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="n">system_prompt</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 the chat</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">calculate_slide_metric</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Task 1: regular chat to extract meta-data</span>
</span></span><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></span><span class="line"><span class="cl">  <span class="nf">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;Execute Task 1 (counts). Here are the slides in Markdown: {{ markdown_content }}&#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></span><span class="line"><span class="cl"><span class="c1"># Task 2: structured chat to further analyse the slides</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">$</span><span class="nf">chat_structured</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Execute Task 2 (suggestions)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">type</span> <span class="o">=</span> <span class="n">type_deck_analysis</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show complete code</summary>
<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><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span><span class="lnt">153
</span><span class="lnt">154
</span><span class="lnt">155
</span><span class="lnt">156
</span><span class="lnt">157
</span><span class="lnt">158
</span><span class="lnt">159
</span><span class="lnt">160
</span><span class="lnt">161
</span><span class="lnt">162
</span><span class="lnt">163
</span><span class="lnt">164
</span><span class="lnt">165
</span><span class="lnt">166
</span><span class="lnt">167
</span><span class="lnt">168
</span><span class="lnt">169
</span><span class="lnt">170
</span><span class="lnt">171
</span><span class="lnt">172
</span><span class="lnt">173
</span><span class="lnt">174
</span><span class="lnt">175
</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></span><span class="line"><span class="cl"><span class="c1"># Get Quarto presentation and convert to plain Markdown + HTML</span>
</span></span><span class="line"><span class="cl"><span class="n">quarto</span><span class="o">::</span><span class="nf">quarto_render</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;./Quarto/my-presentation.qmd&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">output_format</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;markdown&#34;</span><span class="p">,</span> <span class="s">&#34;html&#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"># Dynamic data</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Audience, length in minutes, type, and event</span>
</span></span><span class="line"><span class="cl"><span class="n">audience_content</span> <span class="o">&lt;-</span> <span class="s">&#34;Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">length_content</span> <span class="o">&lt;-</span> <span class="s">&#34;10&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">type_content</span> <span class="o">&lt;-</span> <span class="s">&#34;lightning talk&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">event_content</span> <span class="o">&lt;-</span> <span class="s">&#34;posit::conf(2025)&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Read the generated Markdown file containing our slides</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./Quarto/docs/my-presentation.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_content</span> <span class="o">&lt;-</span> <span class="nf">readChar</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">,</span> <span class="nf">file.size</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define prompt file</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./prompts/prompt-analyse-slides-structured-tool.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create system prompt</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt</span> <span class="o">&lt;-</span> <span class="nf">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">path</span> <span class="o">=</span> <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">audience</span> <span class="o">=</span> <span class="n">audience_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">length</span> <span class="o">=</span> <span class="n">length_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">type</span> <span class="o">=</span> <span class="n">type_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span> <span class="o">=</span> <span class="n">event_content</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"># Reusable scoring category</span>
</span></span><span class="line"><span class="cl"><span class="n">type_scoring_category</span> <span class="o">&lt;-</span> <span class="nf">type_object</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">score</span> <span class="o">=</span> <span class="nf">type_integer</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;Score from 1 to 10.&#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">justification</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Brief explanation of the score.&#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">improvements</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Concise, actionable improvements, mentioning slide numbers if applicable.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">required</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">score_after_improvements</span> <span class="o">=</span> <span class="nf">type_integer</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;Estimated score after suggested improvements.&#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></span><span class="line"><span class="cl"><span class="c1"># Top-level deck analysis object</span>
</span></span><span class="line"><span class="cl"><span class="n">type_deck_analysis</span> <span class="o">&lt;-</span> <span class="nf">type_object</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">presentation_title</span> <span class="o">=</span> <span class="nf">type_string</span><span class="p">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">&#34;The presentation title.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">total_slides</span> <span class="o">=</span> <span class="nf">type_integer</span><span class="p">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Total number of slides.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">percent_with_code</span> <span class="o">=</span> <span class="nf">type_number</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;Percentage of slides containing code blocks (0–100).&#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">percent_with_images</span> <span class="o">=</span> <span class="nf">type_number</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;Percentage of slides containing images (0–100).&#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">estimated_duration_minutes</span> <span class="o">=</span> <span class="nf">type_integer</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;Estimated presentation length in minutes, assuming ~1 minute per text slide and 2–3 minutes per code or image-heavy slide.&#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">tone</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Brief description of the presentation tone (e.g., informal, technical, playful).&#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">clarity</span> <span class="o">=</span> <span class="nf">type_array</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;Evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">relevance</span> <span class="o">=</span> <span class="nf">type_array</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;Asses how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">visual_design</span> <span class="o">=</span> <span class="nf">type_array</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;Judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">engagement</span> <span class="o">=</span> <span class="nf">type_array</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;Estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">pacing</span> <span class="o">=</span> <span class="nf">type_array</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;Analyze the distribution of content across slides. Are some slides too dense or too light? &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">structure</span> <span class="o">=</span> <span class="nf">type_array</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;Review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">concistency</span> <span class="o">=</span> <span class="nf">type_array</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;Evaluate whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">accessibility</span> <span class="o">=</span> <span class="nf">type_array</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;Consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</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"># Define a tool to calculate some metrics</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Start with a function:</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; Calculates the total number of slides, percentage of slides with code blocks,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; and percentage of slides with images in a Quarto presentation HTML file.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; @param metric The metric to calculate: &#34;total_slides&#34; for total number of slides,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; &#34;code&#34; for percentage of slides containing fenced code blocks, or &#34;images&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; for percentage of slides containing images.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; @return The calculated metric value.</span>
</span></span><span class="line"><span class="cl"><span class="n">calculate_slide_metric</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">metric</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">html_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./Quarto/docs/my-presentation.html&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">file.exists</span><span class="p">(</span><span class="n">html_file</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">stop</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;HTML file does not exist. Please render your Quarto presentation first.&#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="c1"># Read HTML file</span>
</span></span><span class="line"><span class="cl">  <span class="n">html_content</span> <span class="o">&lt;-</span> <span class="nf">readChar</span><span class="p">(</span><span class="n">html_file</span><span class="p">,</span> <span class="nf">file.size</span><span class="p">(</span><span class="n">html_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Split on &lt;section&gt; tags to get individual slides</span>
</span></span><span class="line"><span class="cl">  <span class="n">slides</span> <span class="o">&lt;-</span> <span class="nf">unlist</span><span class="p">(</span><span class="nf">strsplit</span><span class="p">(</span><span class="n">html_content</span><span class="p">,</span> <span class="s">&#34;&lt;section&#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">total_slides</span> <span class="o">&lt;-</span> <span class="nf">length</span><span class="p">(</span><span class="n">slides</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">if</span> <span class="p">(</span><span class="n">metric</span> <span class="o">==</span> <span class="s">&#34;total_slides&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">&lt;-</span> <span class="n">total_slides</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">metric</span> <span class="o">==</span> <span class="s">&#34;code&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Count slides where we see the &#34;sourceCode&#34; class</span>
</span></span><span class="line"><span class="cl">    <span class="n">slides_with_code</span> <span class="o">&lt;-</span> <span class="nf">sum</span><span class="p">(</span><span class="nf">grepl</span><span class="p">(</span><span class="s">&#39;class=&#34;sourceCode&#34;&#39;</span><span class="p">,</span> <span class="n">slides</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">&lt;-</span> <span class="nf">round</span><span class="p">((</span><span class="n">slides_with_code</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="m">100</span><span class="p">,</span> <span class="m">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">metric</span> <span class="o">==</span> <span class="s">&#34;images&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Count slides with image tag</span>
</span></span><span class="line"><span class="cl">    <span class="n">slides_with_image</span> <span class="o">&lt;-</span> <span class="nf">sum</span><span class="p">(</span><span class="nf">grepl</span><span class="p">(</span><span class="s">&#39;&lt;img&#39;</span><span class="p">,</span> <span class="n">slides</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">&lt;-</span> <span class="nf">round</span><span class="p">((</span><span class="n">slides_with_image</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="m">100</span><span class="p">,</span> <span class="m">2</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="nf">stop</span><span class="p">(</span><span class="s">&#34;Unknown metric: choose &#39;total_slides&#39;, &#39;code&#39;, or &#39;images&#39;&#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="kr">return</span><span class="p">(</span><span class="n">result</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"># Optionally, to avoid manual work:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># create_tool_def(calculate_slide_metric)</span>
</span></span><span class="line"><span class="cl"><span class="n">calculate_slide_metric</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="n">calculate_slide_metric</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Returns the calculated metric value&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">metric</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">&#39;The metric to calculate: &#34;total_slides&#34; for total number of slides, 
</span></span></span><span class="line"><span class="cl"><span class="s">      &#34;code&#34; for percentage of slides containing fenced code blocks, or &#34;images&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">      for percentage of slides containing images.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">required</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="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="n">system_prompt</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 the chat</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">calculate_slide_metric</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Task 1: regular chat to extract meta-data</span>
</span></span><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></span><span class="line"><span class="cl">  <span class="nf">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;Execute Task 1 (counts). Here are the slides in Markdown: {{ markdown_content }}&#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></span><span class="line"><span class="cl"><span class="c1"># Task 2: structured chat to further analyse the slides</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">$</span><span class="nf">chat_structured</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Execute Task 2 (suggestions)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">type</span> <span class="o">=</span> <span class="n">type_deck_analysis</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<p>We can clearly see that our tool was being called 3 times for our 3 metrics, just like it should:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; ◯ [tool call] calculate_slide_metric(metric = &#34;total_slides&#34;)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ● #&gt; 16</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ◯ [tool call] calculate_slide_metric(metric = &#34;code&#34;)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ● #&gt; 37.5</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ◯ [tool call] calculate_slide_metrimetric = &#34;images&#34;)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ● #&gt; 6.25</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ```json</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;   &#34;total_slides&#34;: 16,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;percentage_slides_with_code&#34;: 37.5,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;percentage_slides_with_images&#34;: 6.25</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; ```</span>
</span></span></code></pre></td></tr></table>
</div>
</div><details class="code-fold">
<summary>Show the rest of the output</summary>
<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><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="c1">#&gt; ◯ [tool call] calculate_slide_metric(metric = &#34;total_slides&#34;)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ● #&gt; 16</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ◯ [tool call] calculate_slide_metric(metric = &#34;code&#34;)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ● #&gt; 37.5</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ◯ [tool call] calculate_slide_metric(metric = &#34;images&#34;)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ● #&gt; 6.25</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; ```json</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;   &#34;total_slides&#34;: 16,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;percentage_slides_with_code&#34;: 37.5,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   &#34;percentage_slides_with_images&#34;: 6.25</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; ```</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; $presentation_title</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] &#34;The Shiny Side of LLMs&#34;</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; $total_slides</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] 16</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; $percent_with_code</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] 37.5</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; $percent_with_images</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] 6.25</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; $estimated_duration_minutes</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] 12</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; $tone</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [1] &#34;Informal and conversational with technical depth&#34;</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; $clarity</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     8</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                  justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Content is well-explained with clear examples and step-by-step progression. Technical concepts are introduced appropriately.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                                                                      improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Add brief definition of LLMs on slide 2 for audience members new to the topic. Consider adding expected output examples on slides 5-6 to show what the LLM responses look like.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        9</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; $relevance</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     9</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                                                justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Highly relevant for the posit::conf audience - focuses on practical implementation in both R and Python with Shiny, which is core to the conference theme.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                             improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Content is already well-targeted for the audience. No significant improvements needed.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        9</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; $visual_design</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     6</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                              justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Layout is clean but quite text-heavy. Code blocks are well-formatted, but some slides like slide 8 and 14 contain large amounts of text.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                                                                                                                            improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Break up slide 8 (Shiny basics: Python) into 2 slides - separate UI and server logic. Split slide 14 (User experience) into bullet points or multiple slides. Add more visual elements like diagrams or screenshots of the final app.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        8</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; $engagement</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     7</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                                           justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Good use of emojis and conversational tone. The magic GIF on slide 10 adds personality. However, mostly text-based with limited interactive elements.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                                                                     improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Add a live demo screenshot or video on slide 15 showing the final DeckCheck app in action. Consider adding a simple architecture diagram on slide 4 to visualize the workflow.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        8</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; $pacing</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     7</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                        justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Good balance overall, but some slides are content-heavy (slides 8, 9, 12, 13) which may feel rushed in a 10-minute lightning talk.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                                                                                           improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Simplify code examples on slides 8-9 by showing only key parts. Break slide 12-13 into smaller, focused slides. Consider removing some implementation details to focus on the core concept and demo.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        8</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; $structure</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     8</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                         justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Clear logical flow from introduction to implementation to next steps. Good progression from basic concepts to complete integration.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                                                          improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Add a brief agenda or roadmap slide after slide 1 to set expectations. Include a quick recap slide before the &#39;Up next&#39; section to summarize what was accomplished.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        9</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; $concistency</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     8</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                          justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Consistent formatting for code blocks and good parallel structure between R and Python examples. Tone remains consistent throughout.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                                             improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Ensure slide titles follow consistent capitalization (slide 10 &#39;Where&#39;s the magic?&#39; vs others). Standardize bullet point formatting across all slides.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        9</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; $accessibility</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1     7</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                                justification</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Code is readable with good contrast. However, some slides have dense text that may be challenging to read quickly during a lightning talk.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;                                                                                                                                                 improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1 Increase font size for code blocks on slides 5-6, 8-9, 12-13. Reduce text density on slide 14. Ensure the GIF on slide 10 has alt text for screen readers.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;   score_after_improvements</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; 1                        8</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
</div>
</div>
<p>So how does the LLM know that it needs to use a tool? We didn&rsquo;t specifically tell it to use it. We just registered it and let the LLM figure it out for itself. How does that work? The first step in the process is turning a vague, human-friendly request into something a tool can actually run. In our case, our prompt contained this:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><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-markdown" data-lang="markdown"><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">You extract the following information:
</span></span><span class="line"><span class="cl"><span class="k">-</span> The number of slides
</span></span><span class="line"><span class="cl"><span class="k">-</span> The percentage of slides containing code blocks
</span></span><span class="line"><span class="cl"><span class="k">-</span> The percentage of slides containing images 
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Return only the JSON results, nothing else.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...
</span></span></code></pre></td></tr></table>
</div>
</div><p>When the LLM sees &ldquo;The percentage of slides containing images,&rdquo; it doesn&rsquo;t just guess. It translates that phrase into structured parameters for the registered tool: <code>calculate_slide_metric</code>. The LLM knows it needs to set <code>metric</code> to <code>&quot;images&quot;</code> or <code>&quot;code&quot;</code>, depending on the request. Once it has those details, it calls the tool with this parameter and responds with the result in JSON (and nothing else, as we told it to: LLMs love to talk).</p>
<p>This is where tool calling really shines. It&rsquo;s not just about running code: it&rsquo;s about structuring messy, open-ended requests into something that can be understood by our code. You could ask &ldquo;Show me how code-heavy my slides are,&rdquo; &ldquo;Can you tell me how many images are in this presentation?&rdquo; or &ldquo;What percentage of slides have images?&rdquo; and the LLM would know to call the same tool. The wording changes, but the intent is the same, and the LLM can map all those variations to the right parameters. It&rsquo;s pretty clever!</p>
<p>The parameters make it even more powerful. If we created a tool with no parameters (e.g. one that only ever returned the percentage of image slides), it could only answer that exact question. But by designing it with parameters, it&rsquo;s way more flexible. The same <code>calculate_slide_metric</code> function can handle requests for total slides, code slides, image slides, or anything else we choose to add later. And because the LLM can chain tool calls, aka run the tool more than once in a conversation, it can check for the 3 metrics all at once and bundle the results together in one tidy JSON.</p>
<h1 id="from-tools-to-agents">From tools to agents
</h1>
<p>Right now, DeckCheck&rsquo;s tool does one (or, ok, technically speaking, three) things: it calculates the metrics you&rsquo;ve defined. That&rsquo;s it. But tool calling can go much further. Imagine that, instead of passing parameters to code you&rsquo;ve already written, the LLM could actually write its own code to analyse your slides. Creepy, right? The LLM is just&hellip; doing its own thing. This is sometimes called &ldquo;agentic AI&rdquo;, where the LLM can formulate and execute coding tasks to learn about <em>and alter</em> the state of the world.</p>
<p>Sounds cool, but what if the LLM decides to do something that&rsquo;s harmful? Imagine it dropping database tables, overwriting files, or sending data somewhere it shouldn&rsquo;t. Without limits, arbitrary code execution can be as risky as it is powerful. That&rsquo;s why you need to take some safety measures. For example, if you let the LLM write SQL statements, you can limit it so it only allows read-only SQL executing. This is exactly what <a href="https://github.com/posit-dev/querychat" target="_blank" rel="noopener">querychat</a>
 does. Or, instead of giving the LLM complete freedom, you can add a human back into the loop by asking explicit user approval before anything gets executed. An example of such a project is the new (experimental) agent called <a href="https://open-vsx.org/extension/posit/databot" target="_blank" rel="noopener">Databot</a>
. Databot is there to assist you in your data analysis, making you faster and more efficient. But it doesn&rsquo;t mean it can replace you: to use these tools effectively and safely, <a href="https://posit.co/blog/databot-is-not-a-flotation-device/" target="_blank" rel="noopener">your human skills are still needed</a>
. Curious to learn more about Databot? Check out the <a href="https://posit.co/blog/introducing-databot/" target="_blank" rel="noopener">official announcement</a>
!</p>
<p>This is also where MCP (<a href="https://modelcontextprotocol.io/docs/getting-started/intro" target="_blank" rel="noopener">Model Context Protocol</a>
) comes in. You have data or tools (an R/Python function, an API, a database, spreadsheet, whatever). You want an LLM to use them. But the LLM doesn&rsquo;t know what you have or how to access it. The problem is, the LLM doesn&rsquo;t automatically know what you have or how to use it. MCP solves that by providing a standard way to describe these tools and make them available to the model. It&rsquo;s like handing the LLM a clearly written &ldquo;menu&rdquo; of what&rsquo;s on offer and how to order it. That&rsquo;s why MCP is often described as &ldquo;a USB-C port for LLM applications&rdquo;: it standardises how models can plug into all sorts of tools and data sources. It opens a world of possibilities. But the details of MCP are a whole topic on their own, so we&rsquo;ll leave that for another time. If you got curious about MCP, you can check out the relevant section in the <a href="https://posit-dev.github.io/chatlas/misc/mcp-tools.html" target="_blank" rel="noopener"><code>chatlas</code> documentation</a>
, or <a href="https://posit-dev.github.io/mcptools/" target="_blank" rel="noopener"><code>mcptools</code></a>
 for some R fun with MCP.</p>
<h1 id="model-parameters">Model parameters
</h1>
<p>We covered the &ldquo;big&rdquo; parts to get a response we can work with: we set a system prompt, crafted a nice detailed input prompt, used structured output, and gave the LLM some extra calculation power with a tool. But still we are getting slightly different results every single time: the suggested improvements are always different and even things like estimated duration vary. Ideally, we want to have as much &ldquo;control&rdquo; as possible over the response an LLM generates. And while we can&rsquo;t 100% control what an LLM does, we can tweak its behaviour by changing (some of) the settings. These settings are also called model parameters.</p>
<blockquote>
<p><strong>A bit playing around the edges</strong></p>
<p>Tweaking model parameters is playing around the edges: it won&rsquo;t magically fix a bad prompt. Writing a good prompt and knowing how to use <code>chatlas</code> or <code>ellmer</code> programmatically already give a solid foundation. Normally default settings for model parameters are the default for a reason: they tend to give the most desired results. But understanding which parameters there are and what kind of effect they have on the LLM&rsquo;s response is a good exercise anyway!</p>
</blockquote>
<p>There are many model parameters, and the ones you can change are different for each provider and model, but generally, these are the most noteworthy ones:</p>
<ul>
<li>
<p><strong>Temperature</strong>: when you ask an LLM the same question you might expect the same answer, just like a calculator always gives the same result for 2 + 2. But an LLM isn&rsquo;t a calculator: it&rsquo;s more like a smart writer. It &ldquo;knows&rdquo; how it should answer your question, but it can phrase it in many ways. That&rsquo;s because its default settings allow a little randomness, a little &ldquo;creativity&rdquo;. The wording, order, or formatting might change, even if the meaning stays the same. This creativity is also called the model&rsquo;s temperature. Typically, the temperature ranges from 0.0 to 1.0. A temperature closer to 1.0 is great for creative and generative tasks, and a temperature closer to 0.0 is best for analytical tasks. Note that a temperature of 0 doesn&rsquo;t mean an LLM will be fully deterministic: probability mechanisms will always cause slight variations in answers. Also note that a temperature of 1 might lead to very creative answers that don&rsquo;t make much sense anymore.</p>
</li>
<li>
<p><strong>Seed</strong>: adds a bit of predictability to a model that&rsquo;s normally unpredictable. As mentioned before, asking the same question twice (especially with a higher temperature) gives you slightly different answers. That&rsquo;s great for creativity, but not always what you want. By setting a seed you make randomness reproducible. It tells the model to &ldquo;start from the same random point&rdquo; every time. This is especially helpful for testing or generating consistent results in production. Note: not all LLM providers support the seed parameter! Looking at the <a href="https://docs.anthropic.com/en/api/messages" target="_blank" rel="noopener">documentation</a>
, Anthropic does not seem to support this parameter in API calls.</p>
</li>
<li>
<p><strong>Top-p</strong>: controls how many possible words the model can choose from when generating a response. It does this by adding up the most likely words until a probability threshold is reached. Let&rsquo;s take our original prompt as an example: &ldquo;I&rsquo;m working on a presentation with the title: &lsquo;The Shiny Side of LLMs&rsquo;. What&rsquo;s your feedback just based on that title?&rdquo;. The model considers several next phrases:</p>
<ul>
<li>&ldquo;is catchy and clear&rdquo; (50%)</li>
<li>&ldquo;could be more specific&rdquo; (25%)</li>
<li>&ldquo;is creative but vague&rdquo; (15%)</li>
<li>&ldquo;needs glitter&rdquo; (7%)</li>
<li>&ldquo;mentions Dalmatians&rdquo; (3%)</li>
</ul>
<p>If top-p = 0.9, only the top three are used (since 50% + 25% + 15% = 90%). The model randomly picks one of those three options, with the probability of each token still guiding the choice (so the 50% token is much more likely to be picked than the 15% one). If top-p = 0.75, the model only considers the first two options. A higher top-p value leads to more variety, while a lower top-p leads to more focussed and &ldquo;safe&rdquo; answers.</p>
<p>It&rsquo;s not recommended to control creativity via both temperature or top-p: pick one. If you start tweaking both parameters they might cancel each other out and it can lead to a weird balance. Changing the temperature is the simpler and more predictive way to control creativity: it&rsquo;s more linear and easier to wrap your head around.</p>
</li>
<li>
<p><strong>Maximum length or maximum output tokens</strong>: the limit on how long the model&rsquo;s response can be. Different models have different maximum values for this parameter. Note that with tokens, you&rsquo;re not counting characters or words. Instead, you&rsquo;re telling the model: &ldquo;Don&rsquo;t generate more than X chunks of language&rdquo;. For example, Claude Sonnet 4 has a <a href="https://docs.anthropic.com/en/docs/about-claude/models/overview#model-comparison-table" target="_blank" rel="noopener">maximum output of 64,000 tokens</a>
 (which is around 48,000 words).</p>
</li>
</ul>
<p>And to come back to some jargon, changing any of these model parameters is called inference tweaking. It&rsquo;s trying to control how creative, long, or focused responses are.</p>
<p>Model parameters are supported by most providers and with <code>chatlas</code> and <code>ellmer</code> you can quickly set them in a provider-agnostic way.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-13" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-13-1">Python</a></li>
<li><a href="#tabset-13-2">R</a></li>
</ul>
<div id="tabset-13-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></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"># Set model parameters (optional)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">set_model_params</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">temperature</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span> <span class="c1"># default is 1</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>By using the <a href="https://posit-dev.github.io/chatlas/reference/Chat.html#set_model_params.qmd" target="_blank" rel="noopener"><code>set_model_params()</code></a>
 method on the chat object, you can quickly set things like <code>temperature</code>, <code>max_tokens</code> or <code>top_p</code>. These model parameters apply to the whole chat. If you want to revert to the default parameters, you can pass <code>None</code> to the relevant parameter.</p>
</div>
<div id="tabset-13-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></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">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">params</span> <span class="o">=</span> <span class="nf">params</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">temperature</span> <span class="o">=</span> <span class="m">0.8</span> <span class="c1"># default is 1</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Adding parameters to our model is easy: we use the <a href="https://ellmer.tidyverse.org/reference/params.html" target="_blank" rel="noopener"><code>params</code> argument</a>
 in <code>chat_anthropic()</code>. We create those parameters with <a href="https://ellmer.tidyverse.org/reference/params.html" target="_blank" rel="noopener"><code>params()</code></a>
. This helper function creates a list of parameters, where parameter names are automatically standardised and included in the correctly place in the API call. You won&rsquo;t have to do a thing. Thanks <code>ellmer</code>!</p>
<p>Note: since <code>ellmer</code> 0.3.0 you can also use <a href="https://ellmer.tidyverse.org/reference/chat-any.html" target="_blank" rel="noopener"><code>chat()</code></a>
 and pass the <code>params</code> argument there.</p>
</div>
</div>
<p>Besides model parameters, which are commonly used across providers, there&rsquo;s also something called <a href="https://posit-dev.github.io/chatlas/get-started/parameters.html#chat-parameters" target="_blank" rel="noopener">chat parameters</a>
. The difference is that the latter are specific to the model provider you&rsquo;re using.</p>
<h1 id="full-workflow">Full workflow
</h1>
<p>A nice prompt, structured output, tool calling, model parameters&hellip; It all led us to the following workflow:</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>
<li><a href="#tabset-14-3">System prompt</a></li>
</ul>
<div id="tabset-14-1">
<blockquote>
<p><strong>Get this code from GitHub</strong></p>
<p>You can grab the code directly from <a href="https://github.com/hypebright/the-shiny-side-of-llms/blob/d1094d2774f9d0c213c7ddf6e17f94da706b1b76/Py/demo/conversation-tool.py" target="_blank" rel="noopener">here</a>
.</p>
</blockquote>
<details class="code-fold">
<summary>See full workflow</summary>
<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><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span><span class="lnt">153
</span><span class="lnt">154
</span><span class="lnt">155
</span><span class="lnt">156
</span><span class="lnt">157
</span><span class="lnt">158
</span><span class="lnt">159
</span><span class="lnt">160
</span><span class="lnt">161
</span><span class="lnt">162
</span><span class="lnt">163
</span><span class="lnt">164
</span><span class="lnt">165
</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">ChatAnthropic</span><span class="p">,</span> <span class="n">interpolate_file</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">subprocess</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 class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span><span class="p">,</span> <span class="n">Field</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Annotated</span><span class="p">,</span> <span class="n">Optional</span><span class="p">,</span> <span class="n">Union</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">ChatAnthropic</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;claude-sonnet-4-20250514&#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;You are a presentation coach for data scientists. You give constructive, focused, and practical feedback on titles, structure, and storytelling.&#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"># Set model parameters (optional)</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">set_model_params</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">temperature</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span>  <span class="c1"># default is 1</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"># Get Quarto presentation and convert to plain Markdown + HTML</span>
</span></span><span class="line"><span class="cl"><span class="n">subprocess</span><span class="o">.</span><span class="n">run</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="s2">&#34;quarto&#34;</span><span class="p">,</span> <span class="s2">&#34;render&#34;</span><span class="p">,</span> <span class="s2">&#34;./my-presentation.qmd&#34;</span><span class="p">,</span> <span class="s2">&#34;--to&#34;</span><span class="p">,</span> <span class="s2">&#34;markdown,html&#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"># Use prompt file</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Step 1: first step of the analysis (meta-data)</span>
</span></span><span class="line"><span class="cl"><span class="n">prompt_file_1</span> <span class="o">=</span> <span class="s2">&#34;./prompts/prompt-analyse-slides-structured-tool-1.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Step 2: second step of the analysis (detailed analysis with improvements)</span>
</span></span><span class="line"><span class="cl"><span class="n">prompt_file_2</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./prompts/prompt-analyse-slides-structured-tool-2.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"># Dynamic data</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Audience, length in minutes, type, and event</span>
</span></span><span class="line"><span class="cl"><span class="n">audience_content</span> <span class="o">=</span> <span class="s2">&#34;Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">length_content</span> <span class="o">=</span> <span class="s2">&#34;10&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">type_content</span> <span class="o">=</span> <span class="s2">&#34;lightning talk&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">event_content</span> <span class="o">=</span> <span class="s2">&#34;posit::conf(2025)&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Read the generated Markdown file containing our slides</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./docs/my-presentation.md&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_content</span> <span class="o">=</span> <span class="n">markdown_file</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#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"># Construct the first prompt</span>
</span></span><span class="line"><span class="cl"><span class="n">prompt_complete_1</span> <span class="o">=</span> <span class="n">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">prompt_file_1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">variables</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;audience&#34;</span><span class="p">:</span> <span class="n">audience_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;length&#34;</span><span class="p">:</span> <span class="n">length_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;type&#34;</span><span class="p">:</span> <span class="n">type_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;event&#34;</span><span class="p">:</span> <span class="n">event_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;markdown&#34;</span><span class="p">:</span> <span class="n">markdown_content</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"># Read the second prompt (no dynamic data)</span>
</span></span><span class="line"><span class="cl"><span class="n">prompt_complete_2</span> <span class="o">=</span> <span class="n">prompt_file_2</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#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"># Define data structure to extract from the input</span>
</span></span><span class="line"><span class="cl"><span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mi">10</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">PercentType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">float</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mf">100.0</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">MinutesType</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">0</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl"><span class="n">SlideCount</span> <span class="o">=</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</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="k">class</span> <span class="nc">ScoringCategory</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">score</span><span class="p">:</span> <span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Score from 1–10.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">justification</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Brief explanation of the score.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">improvements</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Concise, actionable improvements, mentioning slide numbers if applicable.&#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">score_after_improvements</span><span class="p">:</span> <span class="n">ScoreType</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Estimated score after suggested improvements.&#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="k">class</span> <span class="nc">DeckAnalysis</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">presentation_title</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;The presentation title.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">total_slides</span><span class="p">:</span> <span class="n">SlideCount</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_code</span><span class="p">:</span> <span class="n">PercentType</span>
</span></span><span class="line"><span class="cl">    <span class="n">percent_with_images</span><span class="p">:</span> <span class="n">PercentType</span>
</span></span><span class="line"><span class="cl">    <span class="n">estimated_duration_minutes</span><span class="p">:</span> <span class="n">MinutesType</span>
</span></span><span class="line"><span class="cl">    <span class="n">tone</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;Brief description of the tone of the presentation.&#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">clarity</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?&#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">relevance</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Assess how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?&#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">visual_design</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?&#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">engagement</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?&#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">pacing</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Analyze the distribution of content across slides. Are some slides too dense or too light? &#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">structure</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?&#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">concistency</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</span><span class="p">(</span>  <span class="c1"># spelling kept as-is</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">description</span><span class="o">=</span><span class="s2">&#34;Evaluatue whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?&#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">accessibility</span><span class="p">:</span> <span class="n">ScoringCategory</span> <span class="o">=</span> <span class="n">Field</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">description</span><span class="o">=</span><span class="s2">&#34;Consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?&#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"># Define a tool to calculate some metrics</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Start with a function:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">calculate_slide_metric</span><span class="p">(</span><span class="n">metric</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Union</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">float</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">    Calculates the total number of slides, percentage of slides with code blocks,
</span></span></span><span class="line"><span class="cl"><span class="s2">    and percentage of slides with images in a Quarto presentation HTML file.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    Parameters
</span></span></span><span class="line"><span class="cl"><span class="s2">    ----------
</span></span></span><span class="line"><span class="cl"><span class="s2">    metric : str
</span></span></span><span class="line"><span class="cl"><span class="s2">        The metric to calculate: &#34;total_slides&#34; for total number of slides,
</span></span></span><span class="line"><span class="cl"><span class="s2">        &#34;code&#34; for percentage of slides containing fenced code blocks,
</span></span></span><span class="line"><span class="cl"><span class="s2">        or &#34;images&#34; for percentage of slides containing images.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    Returns
</span></span></span><span class="line"><span class="cl"><span class="s2">    -------
</span></span></span><span class="line"><span class="cl"><span class="s2">    float or int
</span></span></span><span class="line"><span class="cl"><span class="s2">        The calculated metric value.
</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="n">html_file</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="s2">&#34;./docs/my-presentation.html&#34;</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">html_file</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">FileNotFoundError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;HTML file </span><span class="si">{</span><span class="n">html_file</span><span class="si">}</span><span class="s2"> does not exist.&#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"># Read HTML file</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="n">html_file</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#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">html_content</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Split on &lt;section&gt; tags to get individual slides</span>
</span></span><span class="line"><span class="cl">    <span class="n">slides</span> <span class="o">=</span> <span class="n">html_content</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;&lt;section&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">total_slides</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">slides</span><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="n">metric</span> <span class="o">==</span> <span class="s2">&#34;total_slides&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">total_slides</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="n">metric</span> <span class="o">==</span> <span class="s2">&#34;code&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">slides_with_code</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="s1">&#39;class=&#34;sourceCode&#34;&#39;</span> <span class="ow">in</span> <span class="n">slide</span> <span class="k">for</span> <span class="n">slide</span> <span class="ow">in</span> <span class="n">slides</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="nb">round</span><span class="p">((</span><span class="n">slides_with_code</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">elif</span> <span class="n">metric</span> <span class="o">==</span> <span class="s2">&#34;images&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">slides_with_image</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="s2">&#34;&lt;img&#34;</span> <span class="ow">in</span> <span class="n">slide</span> <span class="k">for</span> <span class="n">slide</span> <span class="ow">in</span> <span class="n">slides</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="nb">round</span><span class="p">((</span><span class="n">slides_with_image</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="mi">100</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;Unknown metric: choose &#39;total_slides&#39;, &#39;code&#39;, or &#39;images&#39;&#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">result</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">register_tool</span><span class="p">(</span><span class="n">calculate_slide_metric</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Step 1: use regular chat to extract meta-data</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Note that this *should* make use of our tool</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="n">prompt_complete_1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Step 2: use structured chat to further analyse the slides</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">.</span><span class="n">chat_structured</span><span class="p">(</span><span class="n">prompt_complete_2</span><span class="p">,</span> <span class="n">data_model</span><span class="o">=</span><span class="n">DeckAnalysis</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
</div>
<div id="tabset-14-2">
<blockquote>
<p><strong>Get this code from GitHub</strong></p>
<p>You can grab the code directly from <a href="https://github.com/hypebright/the-shiny-side-of-llms/blob/d1094d2774f9d0c213c7ddf6e17f94da706b1b76/R/demo/conversation-tool.R" target="_blank" rel="noopener">here</a>
.</p>
</blockquote>
<details class="code-fold">
<summary>See full workflow</summary>
<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><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span><span class="lnt">153
</span><span class="lnt">154
</span><span class="lnt">155
</span><span class="lnt">156
</span><span class="lnt">157
</span><span class="lnt">158
</span><span class="lnt">159
</span><span class="lnt">160
</span><span class="lnt">161
</span><span class="lnt">162
</span><span class="lnt">163
</span><span class="lnt">164
</span><span class="lnt">165
</span><span class="lnt">166
</span><span class="lnt">167
</span><span class="lnt">168
</span><span class="lnt">169
</span><span class="lnt">170
</span><span class="lnt">171
</span><span class="lnt">172
</span><span class="lnt">173
</span><span class="lnt">174
</span><span class="lnt">175
</span><span class="lnt">176
</span><span class="lnt">177
</span><span class="lnt">178
</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></span><span class="line"><span class="cl"><span class="c1"># Get Quarto presentation and convert to plain Markdown + HTML</span>
</span></span><span class="line"><span class="cl"><span class="n">quarto</span><span class="o">::</span><span class="nf">quarto_render</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;./Quarto/my-presentation.qmd&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">output_format</span> <span class="o">=</span> <span class="nf">c</span><span class="p">(</span><span class="s">&#34;markdown&#34;</span><span class="p">,</span> <span class="s">&#34;html&#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"># Dynamic data</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Audience, length in minutes, type, and event</span>
</span></span><span class="line"><span class="cl"><span class="n">audience_content</span> <span class="o">&lt;-</span> <span class="s">&#34;Python and R users who are curious about AI and large language models, but not all of them have a deep technical background&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">length_content</span> <span class="o">&lt;-</span> <span class="s">&#34;10&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">type_content</span> <span class="o">&lt;-</span> <span class="s">&#34;lightning talk&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">event_content</span> <span class="o">&lt;-</span> <span class="s">&#34;posit::conf(2025)&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Read the generated Markdown file containing our slides</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./Quarto/docs/my-presentation.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">markdown_content</span> <span class="o">&lt;-</span> <span class="nf">readChar</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">,</span> <span class="nf">file.size</span><span class="p">(</span><span class="n">markdown_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define prompt file</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./prompts/prompt-analyse-slides-structured-tool.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create system prompt</span>
</span></span><span class="line"><span class="cl"><span class="n">system_prompt</span> <span class="o">&lt;-</span> <span class="nf">interpolate_file</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">path</span> <span class="o">=</span> <span class="n">system_prompt_file</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">audience</span> <span class="o">=</span> <span class="n">audience_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">length</span> <span class="o">=</span> <span class="n">length_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">type</span> <span class="o">=</span> <span class="n">type_content</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">event</span> <span class="o">=</span> <span class="n">event_content</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"># Reusable scoring category</span>
</span></span><span class="line"><span class="cl"><span class="n">type_scoring_category</span> <span class="o">&lt;-</span> <span class="nf">type_object</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">score</span> <span class="o">=</span> <span class="nf">type_integer</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;Score from 1 to 10.&#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">justification</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Brief explanation of the score.&#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">improvements</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Concise, actionable improvements, mentioning slide numbers if applicable.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">required</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">score_after_improvements</span> <span class="o">=</span> <span class="nf">type_integer</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;Estimated score after suggested improvements.&#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></span><span class="line"><span class="cl"><span class="c1"># Top-level deck analysis object</span>
</span></span><span class="line"><span class="cl"><span class="n">type_deck_analysis</span> <span class="o">&lt;-</span> <span class="nf">type_object</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">presentation_title</span> <span class="o">=</span> <span class="nf">type_string</span><span class="p">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">&#34;The presentation title.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">total_slides</span> <span class="o">=</span> <span class="nf">type_integer</span><span class="p">(</span><span class="n">description</span> <span class="o">=</span> <span class="s">&#34;Total number of slides.&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">percent_with_code</span> <span class="o">=</span> <span class="nf">type_number</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;Percentage of slides containing code blocks (0–100).&#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">percent_with_images</span> <span class="o">=</span> <span class="nf">type_number</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;Percentage of slides containing images (0–100).&#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">estimated_duration_minutes</span> <span class="o">=</span> <span class="nf">type_integer</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;Estimated presentation length in minutes, assuming ~1 minute per text slide and 2–3 minutes per code or image-heavy slide.&#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">tone</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="n">description</span> <span class="o">=</span> <span class="s">&#34;Brief description of the presentation tone (e.g., informal, technical, playful).&#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">clarity</span> <span class="o">=</span> <span class="nf">type_array</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;Evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">relevance</span> <span class="o">=</span> <span class="nf">type_array</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;Asses how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">visual_design</span> <span class="o">=</span> <span class="nf">type_array</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;Judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">engagement</span> <span class="o">=</span> <span class="nf">type_array</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;Estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">pacing</span> <span class="o">=</span> <span class="nf">type_array</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;Analyze the distribution of content across slides. Are some slides too dense or too light? &#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">structure</span> <span class="o">=</span> <span class="nf">type_array</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;Review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">concistency</span> <span class="o">=</span> <span class="nf">type_array</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;Evaluate whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</span>
</span></span><span class="line"><span class="cl">  <span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="n">accessibility</span> <span class="o">=</span> <span class="nf">type_array</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;Consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">type_scoring_category</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"># Define a tool to calculate some metrics</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Start with a function:</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; Calculates the total number of slides, percentage of slides with code blocks,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; and percentage of slides with images in a Quarto presentation HTML file.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; @param metric The metric to calculate: &#34;total_slides&#34; for total number of slides,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; &#34;code&#34; for percentage of slides containing fenced code blocks, or &#34;images&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; for percentage of slides containing images.</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&#39; @return The calculated metric value.</span>
</span></span><span class="line"><span class="cl"><span class="n">calculate_slide_metric</span> <span class="o">&lt;-</span> <span class="kr">function</span><span class="p">(</span><span class="n">metric</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">html_file</span> <span class="o">&lt;-</span> <span class="s">&#34;./Quarto/docs/my-presentation.html&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">if</span> <span class="p">(</span><span class="o">!</span><span class="nf">file.exists</span><span class="p">(</span><span class="n">html_file</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">stop</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="s">&#34;HTML file does not exist. Please render your Quarto presentation first.&#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="c1"># Read HTML file</span>
</span></span><span class="line"><span class="cl">  <span class="n">html_content</span> <span class="o">&lt;-</span> <span class="nf">readChar</span><span class="p">(</span><span class="n">html_file</span><span class="p">,</span> <span class="nf">file.size</span><span class="p">(</span><span class="n">html_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Split on &lt;section&gt; tags to get individual slides</span>
</span></span><span class="line"><span class="cl">  <span class="n">slides</span> <span class="o">&lt;-</span> <span class="nf">unlist</span><span class="p">(</span><span class="nf">strsplit</span><span class="p">(</span><span class="n">html_content</span><span class="p">,</span> <span class="s">&#34;&lt;section&#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">total_slides</span> <span class="o">&lt;-</span> <span class="nf">length</span><span class="p">(</span><span class="n">slides</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">if</span> <span class="p">(</span><span class="n">metric</span> <span class="o">==</span> <span class="s">&#34;total_slides&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">&lt;-</span> <span class="n">total_slides</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">metric</span> <span class="o">==</span> <span class="s">&#34;code&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Count slides where we see the &#34;sourceCode&#34; class</span>
</span></span><span class="line"><span class="cl">    <span class="n">slides_with_code</span> <span class="o">&lt;-</span> <span class="nf">sum</span><span class="p">(</span><span class="nf">grepl</span><span class="p">(</span><span class="s">&#39;class=&#34;sourceCode&#34;&#39;</span><span class="p">,</span> <span class="n">slides</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">&lt;-</span> <span class="nf">round</span><span class="p">((</span><span class="n">slides_with_code</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="m">100</span><span class="p">,</span> <span class="m">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="kr">else</span> <span class="kr">if</span> <span class="p">(</span><span class="n">metric</span> <span class="o">==</span> <span class="s">&#34;images&#34;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Count slides with image tag</span>
</span></span><span class="line"><span class="cl">    <span class="n">slides_with_image</span> <span class="o">&lt;-</span> <span class="nf">sum</span><span class="p">(</span><span class="nf">grepl</span><span class="p">(</span><span class="s">&#39;&lt;img&#39;</span><span class="p">,</span> <span class="n">slides</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">&lt;-</span> <span class="nf">round</span><span class="p">((</span><span class="n">slides_with_image</span> <span class="o">/</span> <span class="n">total_slides</span><span class="p">)</span> <span class="o">*</span> <span class="m">100</span><span class="p">,</span> <span class="m">2</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="nf">stop</span><span class="p">(</span><span class="s">&#34;Unknown metric: choose &#39;total_slides&#39;, &#39;code&#39;, or &#39;images&#39;&#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="kr">return</span><span class="p">(</span><span class="n">result</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"># Optionally, to avoid manual work:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># create_tool_def(calculate_slide_metric)</span>
</span></span><span class="line"><span class="cl"><span class="n">calculate_slide_metric</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="n">calculate_slide_metric</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Returns the calculated metric value&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">metric</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">&#39;The metric to calculate: &#34;total_slides&#34; for total number of slides, 
</span></span></span><span class="line"><span class="cl"><span class="s">      &#34;code&#34; for percentage of slides containing fenced code blocks, or &#34;images&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">      for percentage of slides containing images.&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">required</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="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Initialise chat with Claude Sonnet 4 model</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_anthropic</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="n">model</span> <span class="o">=</span> <span class="s">&#34;claude-sonnet-4-20250514&#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="n">system_prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">params</span> <span class="o">=</span> <span class="nf">params</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">temperature</span> <span class="o">=</span> <span class="m">0.8</span> <span class="c1"># default is 1</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 the chat</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">calculate_slide_metric</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Start conversation with the chat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Task 1: regular chat to extract meta-data</span>
</span></span><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></span><span class="line"><span class="cl">  <span class="nf">interpolate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s">&#34;Execute Task 1 (counts). Here are the slides in Markdown: {{ markdown_content }}&#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></span><span class="line"><span class="cl"><span class="c1"># Task 2: structured chat to further analyse the slides</span>
</span></span><span class="line"><span class="cl"><span class="n">chat</span><span class="o">$</span><span class="nf">chat_structured</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="s">&#34;Execute Task 2 (suggestions)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="n">type</span> <span class="o">=</span> <span class="n">type_deck_analysis</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
</div>
<div id="tabset-14-3">
<details class="code-fold">
<summary>Show full prompt</summary>
<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></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="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">You are a presentation coach for data scientists that analyses presentation slide decks written in Markdown. 
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract key information, evaluate quality, and return structured feedback that is constructive, focused and practical.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">The presentation you are helping with is a {{ length }}-minute {{ type }} at {{ event }}.  
</span></span></span><span class="line"><span class="cl"><span class="s2">The audience is {{ audience }}. 
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You can be asked for one of the following tasks. Each has a number and a name:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2"># Task 1 (counts)
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract the following information:
</span></span></span><span class="line"><span class="cl"><span class="s2">- The number of slides
</span></span></span><span class="line"><span class="cl"><span class="s2">- The percentage of slides containing code blocks
</span></span></span><span class="line"><span class="cl"><span class="s2">- The percentage of slides containing images 
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Return only the JSON results, nothing else.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2"># Task 2 (suggestions)
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You extract the following information:
</span></span></span><span class="line"><span class="cl"><span class="s2">- The presentation title
</span></span></span><span class="line"><span class="cl"><span class="s2">- Estimated presentation length (in minutes, assuming ~1 minute per text slide and 2–3 minutes per code or image-heavy slide)
</span></span></span><span class="line"><span class="cl"><span class="s2">- Tone (a brief description)
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You score the presentation on the following categories (from 1–10), and give a concise explanation:
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">1. Clarity of content: evaluate how clearly the ideas are communicated. Are the explanations easy to understand? Are terms defined when needed? Is the key message clear?
</span></span></span><span class="line"><span class="cl"><span class="s2">2. Relevance for intended audience: assess how well the content matches the audience’s background, needs, and expectations. Are examples, depth of detail, and terminology appropriate for the audience type?
</span></span></span><span class="line"><span class="cl"><span class="s2">3. Visual design: judge the visual effectiveness of the slides. Are they readable, visually balanced, and not overcrowded with text or visuals? Is layout used consistently?
</span></span></span><span class="line"><span class="cl"><span class="s2">4. Engagement: estimate how likely the presentation is to keep attention. Are there moments of interactivity, storytelling, humor, or visual interest that invite focus?
</span></span></span><span class="line"><span class="cl"><span class="s2">5. Pacing: analyze the distribution of content across slides. Are some slides too dense or too light? 
</span></span></span><span class="line"><span class="cl"><span class="s2">6. Structure: review the logical flow of the presentation. Is there a clear beginning, middle, and end? Are transitions between topics smooth? Does the presentation build toward a conclusion?
</span></span></span><span class="line"><span class="cl"><span class="s2">7. consistency: evaluatue whether the presentation is consistent when it comes to formatting, tone, and visual elements. Are there any elements that feel out of place?
</span></span></span><span class="line"><span class="cl"><span class="s2">8. Accessibility: consider how accessible the presentation would be for all viewers, including those with visual or cognitive challenges. Are font sizes readable? Is there sufficient contrast? Are visual elements not overwhelming?
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">For each of the above scoring categories, provide specific and actionable improvements. Follow these instructions:
</span></span></span><span class="line"><span class="cl"><span class="s2">- Keep each suggestion concise and mention the slide number(s) if applicable.
</span></span></span><span class="line"><span class="cl"><span class="s2">- Do not invent issues and only suggest improvements when the content would clearly benefit from them.
</span></span></span><span class="line"><span class="cl"><span class="s2">- For each catogory, estimate what the new score would be if these improvements are implemented.
</span></span></span><span class="line"><span class="cl"><span class="s2">- Return the improvement and new score as part of the response for that category.
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">You bundle your results with the results from the first task. Always return the result as a JSON object that conforms to the provided data model.
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
</div>
</div>
<p>But there&rsquo;s one questions that might still be lingering in your mind: how much does this cost?!</p>
<h1 id="understanding-tokens-and-costs">Understanding tokens and costs
</h1>
<p>Back to our nightmare from the beginning: a bill with a lot of zeros for using an LLM. Luckily you can prevent this pretty easily and it all starts with understanding how and for what you&rsquo;re being charged.</p>
<p>Whenever you talk to an LLM, you&rsquo;re essentially paying for how much you say and how much it says back. Responses are all measured in tokens, those small chunks of text we talked about in Part 1 of this series. For every x amount of tokens, you pay a particular price. For example, at the time of writing, <a href="https://docs.anthropic.com/en/docs/about-claude/pricing" target="_blank" rel="noopener">Claude Sonnet 4</a>
 has a price of 3 USD per million tokens for input, and a price of 15 USD per million tokens for output.</p>
<p>When talking about costs it&rsquo;s important to understand that you pay for both input and output. That means you&rsquo;re billed for what you send and for what the assistant replies with. So if you ask a really long question and get a really long answer, that&rsquo;s going to cost more than a question that requires a yes or no answer.</p>
<p>This is why your prompt and structure matters. If you keep repeating the same context or include an entire Quarto presentation each time you make a call, your token count will add up fast. Remember that the history is send to the LLM with every subsequent question! A deeply nested chat history can cause costs to go up quickly. This is also true for very long system prompts. Therefore, it&rsquo;s often worth trimming what you send, summarising where possible, and being intentional about what the model really needs to see. There&rsquo;s a fine balance between giving as much context as possible for the best possible response, and making sure you&rsquo;re not overdoing it.</p>
<p>Manually keeping track of your tokens would be annoying, so with <code>chatlas</code> or <code>ellmer</code>, you&rsquo;ve got a few handy functions to help estimate token use:</p>
<ul>
<li><code>get_tokens()</code> gives you a raw count of how many tokens you&rsquo;ve used so far.</li>
<li><code>get_cost()</code> gives you an estimate of how much your chat might cost. Note how it says &ldquo;estimate&rdquo;. Model providers can change their pricing overnight, and there might not even be a heads-up. This estimate therefore serves as a rough guide, not the truth.</li>
</ul>
<p>Now for the real thing you want to know: for our full workflow, we pay around 0.06 USD.</p>
<div class="panel-tabset" data-tabset-group="language">
<ul id="tabset-15" class="panel-tabset-tabby">
<li><a data-tabby-default href="#tabset-15-1">Python</a></li>
<li><a href="#tabset-15-2">R</a></li>
</ul>
<div id="tabset-15-1">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></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">get_tokens</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">get_cost</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></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="p">[{</span><span class="s1">&#39;role&#39;</span><span class="p">:</span> <span class="s1">&#39;user&#39;</span><span class="p">,</span> <span class="s1">&#39;tokens&#39;</span><span class="p">:</span> <span class="mi">3611</span><span class="p">,</span> <span class="s1">&#39;tokens_cached&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s1">&#39;tokens_total&#39;</span><span class="p">:</span> <span class="mi">3611</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="s1">&#39;role&#39;</span><span class="p">:</span> <span class="s1">&#39;assistant&#39;</span><span class="p">,</span> <span class="s1">&#39;tokens&#39;</span><span class="p">:</span> <span class="mi">133</span><span class="p">,</span> <span class="s1">&#39;tokens_cached&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s1">&#39;tokens_total&#39;</span><span class="p">:</span> <span class="mi">133</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="s1">&#39;role&#39;</span><span class="p">:</span> <span class="s1">&#39;user&#39;</span><span class="p">,</span> <span class="s1">&#39;tokens&#39;</span><span class="p">:</span> <span class="mi">102</span><span class="p">,</span> <span class="s1">&#39;tokens_cached&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s1">&#39;tokens_total&#39;</span><span class="p">:</span> <span class="mi">3846</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="s1">&#39;role&#39;</span><span class="p">:</span> <span class="s1">&#39;assistant&#39;</span><span class="p">,</span> <span class="s1">&#39;tokens&#39;</span><span class="p">:</span> <span class="mi">52</span><span class="p">,</span> <span class="s1">&#39;tokens_cached&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s1">&#39;tokens_total&#39;</span><span class="p">:</span> <span class="mi">52</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="s1">&#39;role&#39;</span><span class="p">:</span> <span class="s1">&#39;user&#39;</span><span class="p">,</span> <span class="s1">&#39;tokens&#39;</span><span class="p">:</span> <span class="mi">2435</span><span class="p">,</span> <span class="s1">&#39;tokens_cached&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s1">&#39;tokens_total&#39;</span><span class="p">:</span> <span class="mi">6333</span><span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="s1">&#39;role&#39;</span><span class="p">:</span> <span class="s1">&#39;assistant&#39;</span><span class="p">,</span> <span class="s1">&#39;tokens&#39;</span><span class="p">:</span> <span class="mi">947</span><span class="p">,</span> <span class="s1">&#39;tokens_cached&#39;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="s1">&#39;tokens_total&#39;</span><span class="p">:</span> <span class="mi">947</span><span class="p">}]</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></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="mf">0.058350000000000006</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
<div id="tabset-15-2">
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></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">get_tokens</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;1      user   3552         3552</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;2 assistant    148          148</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;3      user    101         3801</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;4 assistant     48           48</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;5      user   1839         5688</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;6 assistant    932          932</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="nf">get_cost</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt;[1] $0.06</span>
</span></span></code></pre></td></tr></table>
</div>
</div></div>
</div>
<blockquote>
<p><strong>Costs differ between runs!</strong></p>
<p>Token counts and cost vary because the answers of the LLM vary too (thanks to the creativity of the model).</p>
</blockquote>
<p>In our case, we might be working with pretty long Quarto presentations. The bigger the presentation, the more tokens our input will have. So far, we only played around with a demo presentation for a 10-minute talk, and basically put everything in one prompt. But we might have subsequent questions to the LLM, too. Overall, there are two strategies that can be used to reduce token count and cost:</p>
<ol>
<li>Batching: instead of sending multiple small calls, combine them into a single, well-structured one when possible. It reduces overhead and often results in fewer total tokens. Keep it DRY to: don&rsquo;t repeat yourself by providing information twice. For example, when using structured output it&rsquo;s not strictly necessary to put all the specifications for the output in the prompt itself too. And it shouldn&rsquo;t be a surprise, but batching is also possible in <code>ellmer</code> with <a href="https://ellmer.tidyverse.org/reference/batch_chat.html" target="_blank" rel="noopener"><code>batch_chat()</code></a>
 and (soon!) in <code>chatlas</code> with a similar function.</li>
<li>Summarising: if you&rsquo;re feeding in the same context repeatedly, have the model summarise it first. Then reuse that summary instead of the full text. It&rsquo;s a small change that can lead to big savings.</li>
</ol>
<p>Every token counts. And if you want to build something that scales, getting a grip on your usage early can save you a lot down the road.</p>
<h1 id="whats-next">What&rsquo;s next
</h1>
<p>The output that we are currently receiving is nice, but it&rsquo;s not something that would amaze our users. For us humans, it&rsquo;s annoying to read information from JSON as well. We need something different. To really get a wow-effect we need to design an appealing user interface. We need to make it easy for users to upload their content, give us additional information (aka our required variables), and analyse the results. Nobody likes a boring static app, so we need to add some shiny interactive features as well. And those shiny interactive features? That&rsquo;s a perfect job for Shiny!</p>
<p>In this part of &ldquo;The Shiny Side of LLMs&rdquo; we built a foundation for our Shiny app: using either <code>chatlas</code> (Python) or <code>ellmer</code> (R) we can now analyse any Quarto presentation. Our current workflow allows us to:</p>
<ul>
<li>Convert a Quarto file to plain markdown (and HTML), which can easily be consumed by an LLM</li>
<li>Call any LLM from any (or ok, most) providers through their API (our chosen model being Claude from Anthropic). Thanks <code>chatlas</code> and <code>ellmer</code>!</li>
<li>Send a carefully crafted prompt that guides the LLM to analyse the markdown content</li>
<li>Let the LLM use a tool to accurately calculate some metrics</li>
<li>Tweak model parameters if we really want to go the extra mile</li>
<li>Receive consistent, structured and useful JSON output that we can use in our Shiny app</li>
</ul>
<p>The only thing left to do? Wrap it in that Shiny interface. See you in the third and last part of &ldquo;The Shiny Side of LLMs&rdquo; series!</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-2/shiny-side-of-llms-header.png" length="401909" type="image/png" />
    </item>
    <item>
      <title>mall 0.2.0</title>
      <link>https://posit-open-source.netlify.app/blog/ai/edgarmall02/</link>
      <pubDate>Tue, 19 Aug 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/ai/edgarmall02/</guid>
      <dc:creator>Edgar Ruiz</dc:creator><description><![CDATA[<p><a href="https://mlverse.github.io/mall/" target="_blank" rel="noopener">mall</a>
 uses Large Language Models (LLM) to run
Natural Language Processing (NLP) operations against your data. This package
is available for both R, and Python. Version 0.2.0 has been released to
<a href="https://cran.r-project.org/web/packages/mall/index.html" target="_blank" rel="noopener">CRAN</a>
 and
<a href="https://pypi.org/project/mlverse-mall/" target="_blank" rel="noopener">PyPi</a>
 respectively.</p>
<p>In R, you can install the latest version with:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-r" data-lang="r"><span class="line"><span class="cl"><span class="nf">install.packages</span><span class="p">(</span><span class="s">&#34;mall&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>In Python, with:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">pip</span> <span class="n">install</span> <span class="n">mlverse</span><span class="o">-</span><span class="n">mall</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This release expands the number of LLM providers you can use with <code>mall</code>. Also,
in Python it introduces the option to run the NLP operations over string vectors,
and in R, it enables support for &lsquo;parallelized&rsquo; requests.</p>
<p>It is also very exciting to announce a brand new cheatsheet for this package. It
is available in print (PDF) and HTML format!</p>
<h2 id="more-llm-providers">More LLM providers
</h2>
<p>The biggest highlight of this release is the the ability to use external LLM
providers such as <a href="https://openai.com/" target="_blank" rel="noopener">OpenAI</a>
, <a href="https://gemini.google.com/" target="_blank" rel="noopener">Gemini</a>

and <a href="https://www.anthropic.com/" target="_blank" rel="noopener">Anthropic</a>
. Instead of writing integration for
each provider one by one, <code>mall</code> uses specialized integration packages to act as
intermediates.</p>
<p>In R, <code>mall</code> uses the <a href="https://ellmer.tidyverse.org/index.html" target="_blank" rel="noopener"><code>ellmer</code></a>
 package
to integrate with <a href="https://ellmer.tidyverse.org/reference/index.html#chatbots" target="_blank" rel="noopener">a variety of LLM providers</a>
.
To access the new feature, first create a chat connection, and then pass that
connection to <code>llm_use()</code>. Here is an example of connecting and using OpenAI:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></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;ellmer&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">library</span><span class="p">(</span><span class="n">mall</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></span><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">&lt;-</span> <span class="nf">chat_openai</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Using model = &#34;gpt-4.1&#34;.</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nf">llm_use</span><span class="p">(</span><span class="n">chat</span><span class="p">,</span> <span class="n">.cache</span> <span class="o">=</span> <span class="s">&#34;_my_cache&#34;</span><span class="p">)</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; ── mall session object </span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; Backend: ellmerLLM session: model:gpt-4.1R session: cache_folder:_my_cache</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>In Python, <code>mall</code> uses <a href="https://posit-dev.github.io/chatlas/" target="_blank" rel="noopener"><code>chatlas</code></a>
 as
the integration point with the LLM. <code>chatlas</code> also integrates with
<a href="https://posit-dev.github.io/chatlas/reference/#chat-model-providers" target="_blank" rel="noopener">several LLM providers</a>
.
To use, first instantiate a <code>chatlas</code> chat connection class, and then pass that
to the <a href="https://pola.rs/" target="_blank" rel="noopener">Polars</a>
 data frame via the <code>&lt;DF&gt;.llm.use()</code> function:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">pip</span> <span class="n">install</span> <span class="n">chatlas</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">mall</span>
</span></span><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></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></span><span class="line"><span class="cl"><span class="n">data</span> <span class="o">=</span> <span class="n">mall</span><span class="o">.</span><span class="n">MallData</span>
</span></span><span class="line"><span class="cl"><span class="n">reviews</span> <span class="o">=</span> <span class="n">data</span><span class="o">.</span><span class="n">reviews</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">reviews</span><span class="o">.</span><span class="n">llm</span><span class="o">.</span><span class="n">use</span><span class="p">(</span><span class="n">chat</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; {&#39;backend&#39;: &#39;chatlas&#39;, &#39;chat&#39;: &lt;Chat OpenAI/gpt-4.1 turns=0 tokens=0/0 $0.0&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; , &#39;_cache&#39;: &#39;_mall_cache&#39;}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Connecting <code>mall</code> to external LLM providers introduces a consideration of cost.
Most providers charge for the use of their API, so there is a potential that a
large table, with long texts, could be an expensive operation.</p>
<h2 id="parallel-requests-r-only">Parallel requests (R only)
</h2>
<p>A new feature introduced in <a href="https://www.tidyverse.org/blog/2025/07/ellmer-0-3-0" target="_blank" rel="noopener"><code>ellmer</code> 0.3.0</a>

enables the access to submit multiple prompts in parallel, rather than in sequence.
This makes it faster, and potentially cheaper, to process a table. If the provider
supports this feature, <code>ellmer</code> is able to leverage it via the
<a href="https://ellmer.tidyverse.org/reference/parallel_chat.html" target="_blank" rel="noopener"><code>parallel_chat()</code></a>

function. Gemini and OpenAI support the feature.</p>
<p>In the new release of <code>mall</code>, the integration with <code>ellmer</code> has been specially
written to take advantage of parallel chat. The internals have been re-written to
submit the NLP-specific instructions as a system message in order
reduce the size of each prompt. Additionally, the cache system has also been
re-tooled to support batched requests.</p>
<h2 id="nlp-operations-without-a-table">NLP operations without a table
</h2>
<p>Since its initial version, <code>mall</code> has provided the ability for R users to perform
the NLP operations over a string vector, in other words, without needing a table.
Starting with the new release, <code>mall</code> also provides this same functionality
in its Python version.</p>
<p><code>mall</code> can process vectors contained in a <code>list</code> object. To use, initialize a
new <code>LLMVec</code> class object with either an Ollama model, or a <code>chatlas</code> <code>Chat</code>
object, and then access the same NLP functions as the Polars extension.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Initialize a Chat object</span>
</span></span><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="n">chat</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="c1"># Pass it to a new LLMVec</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">mall</span> <span class="kn">import</span> <span class="n">LLMVec</span>
</span></span><span class="line"><span class="cl"><span class="n">llm</span> <span class="o">=</span> <span class="n">LLMVec</span><span class="p">(</span><span class="n">chat</span><span class="p">)</span>    
</span></span></code></pre></td></tr></table>
</div>
</div><p>Access the functions via the new LLMVec object, and pass the text to be processed.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">llm</span><span class="o">.</span><span class="n">sentiment</span><span class="p">([</span><span class="s2">&#34;I am happy&#34;</span><span class="p">,</span> <span class="s2">&#34;I am sad&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [&#39;positive&#39;, &#39;negative&#39;]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">llm</span><span class="o">.</span><span class="n">translate</span><span class="p">([</span><span class="s2">&#34;Este es el mejor dia!&#34;</span><span class="p">],</span> <span class="s2">&#34;english&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="c1">#&gt; [&#39;This is the best day!&#39;]</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>For more information visit the reference page: <a href="https://mlverse.github.io/mall/reference/LlmVec.html" target="_blank" rel="noopener">LLMVec</a>
</p>
<h2 id="new-cheatsheet">New cheatsheet
</h2>
<p>The brand new official cheatsheet is now available from Posit:
<a href="https://rstudio.github.io/cheatsheets/nlp-with-llms.pdf" target="_blank" rel="noopener">Natural Language processing using LLMs in R/Python</a>
.
Its mean feature is that one side of the page is dedicated to the R version,
and the other side of the page to the Python version.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/ai/edgarmall02/images/cheatsheet.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<p>An web page version is also availabe in the official cheatsheet site
<a href="https://rstudio.github.io/cheatsheets/html/nlp-with-llms.html" target="_blank" rel="noopener">here</a>
. It takes
advantage of the tab feature that lets you select between R and Python
explanations and examples.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/ai/edgarmall02/images/html-cheatsheet.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/ai/edgarmall02/thumbnail.png" length="690897" type="image/png" />
    </item>
    <item>
      <title>What LLMs Actually Do (and What They Don’t)</title>
      <link>https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-1/</link>
      <pubDate>Thu, 31 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-1/</guid>
      <dc:creator>Veerle Eeftink - Van Leemput</dc:creator><description><![CDATA[<h1 id="welcome-to-the-shiny-side-of-llms">Welcome to the Shiny Side of LLMs!
</h1>
<p>Large language models (LLMs) are everywhere, and we&rsquo;re way beyond denying their existence.
New use cases pop up every single day, from autocompleting code to customer support bots, summarisers, and tutors.
And everybody feels the urge to jump in.
Tools like ChatGPT, Claude, and GitHub Copilot have made us all comfortable using AI.
But when it comes to building something that is tailored, interactive, and actually useful for others, it all starts to feel&hellip; muddy.
Buzzwords start flying: embedding, encoding, RAG, vector databases, streaming tokens&hellip; and suddenly you&rsquo;re wondering if this is still for you.</p>
<p>Spoiler alert: it is, and it&rsquo;s all more accessible than it seems.
Yes, the underlying technology is complex, but that doesn&rsquo;t mean it&rsquo;s out of reach.
For the first time, you have easy access to something that mimics human reasoning.
As a programmer, that means you can describe a problem in plain language and get useful output: code, explanations, ideas, or even full analyses.
And you don&rsquo;t need to train your own models or master deep learning theory to use it.
You just need to understand what&rsquo;s happening under the hood well enough to start building.
In this blog series, we&rsquo;ll demystify all the buzzwords and walk you through:</p>
<ul>
<li>What LLMs really do, conceptually and technically, but explained super simply</li>
<li>How to interact with them while staying in your comfort zone (Python or R)</li>
<li>How to turn those interactions into real, usable tools using Shiny (again: Python or R)</li>
<li>The magic of mixing in your own data such as documents, notes, and anything else you use</li>
</ul>
<p>This isn&rsquo;t just about building a chatbot.
It&rsquo;s about understanding how things work and how you can use an LLM to build your own tools.
A document summariser, a smart search assistant, or a decision helper for your team: the choice is yours.
We&rsquo;ll go step by step.
From &ldquo;zero&rdquo; (I&rsquo;ve only ever typed things into ChatGPT) to &ldquo;hero&rdquo; (I&rsquo;ve built my own LLM-powered app and I understand how it works).</p>
<p>No need to know anything about model hosting, server clusters, or fancy acronyms.
Just you, some Python or R, and a fun idea!</p>
<blockquote>
<p><strong>What can you expect from this first part?</strong></p>
<p>This is Part 1 of &ldquo;The Shiny Side of LLMs&rdquo; series: <em>What LLMs Actually Do (and What They Don&rsquo;t).</em> It is intended to give you some insight into how LLMs work under the hood.</p>
<p>You don&rsquo;t <em>need</em> to know all this to start building things.
But having a basic understanding of the mechanics can help you reason more clearly about what LLMs are (and aren&rsquo;t) good at and avoid getting lost in the hype.</p>
</blockquote>
<h1 id="the-large-the-language-and-the-model">The &ldquo;Large&rdquo;, the &ldquo;Language&rdquo;, and the &ldquo;Model&rdquo;
</h1>
<blockquote>
<p><strong>TL;DR</strong></p>
<p>When we talk about a &ldquo;Large Language Model&rdquo;, we&rsquo;re really talking about a system trained on huge amounts of text, designed to predict and generate human-like language, powered by a model that recognises patterns at scale.</p>
</blockquote>
<p>First things first, what is a Large Language Model?
A good first step is breaking down the term &ldquo;Large Language Model&rdquo;.
Let&rsquo;s do it backwards, starting with the last word: &ldquo;Model&rdquo;.
In this context, a model is a statistical system that learns patterns from data and uses them to make predictions.
Yes, that&rsquo;s right: plain old statistics.</p>
<p>When we&rsquo;re talking about a &ldquo;Language Model&rdquo; we make predictions about a language, consisting of sentences and its words.
It all boils down to the likelihood of a next word, or sequence of words.
A language model is trained to understand the kinds of words that tend to appear together, how sentences flow, and how meaning is built from context.
Early models could only guess a single word at a time and struggled with anything longer or more complex, but modern ones can generate entire paragraphs or documents that stay on topic and sound surprisingly coherent.
Almost like a human.</p>
<p>Bigger is better.
The &ldquo;Large&rdquo; in &ldquo;Large Language Model&rdquo; refers to both the amount of data the model is trained on and the number of parameters it has (the internal settings the model adjusts as it learns).
The &ldquo;larger&rdquo; a model is, the more patterns it can recognise and the more fluent its outputs tend to be.</p>
<p>So what does that mean when you interact with one?
When you ask an LLM something, you prompt it to do something (that&rsquo;s why the input or text you type is also called a prompt).
That something is to predict the most likely sequence of words based on the sequence you have given.</p>
<blockquote>
<p><strong>LLMs and AI</strong></p>
<p>You didn&rsquo;t see much mention of Artificial Intelligence (AI) yet, and that&rsquo;s on purpose.
While LLMs are a specific kind of AI, they&rsquo;re just one part of a much broader field.
But since LLMs are the most visible (and frankly, the most hyped) examples of AI right now, people often use the terms interchangeably.
To keep things focussed, we&rsquo;ll only use the term LLM here.</p>
</blockquote>
<h1 id="how-llms-learn">How LLMs learn
</h1>
<p>We can talk about LLMs like they&rsquo;re the newest thing, but the foundation is still as &ldquo;ancient&rdquo; as statistics.
It&rsquo;s about understanding data: making inferences, finding patterns, estimating (un)certainty.
Machine Learning (ML) is statistics, and ML models learn patterns from data.
Think about algorithms like decision trees, support vector machines, and linear regression that all rely on the same idea: learning from examples.</p>
<p>LLMs learn through a technique called deep learning, which is a subfield of ML.
At the core of deep learning are neural networks: mathematical structures loosely inspired by biological neurons within the brain.
You can think of them as layers of tiny decision-makers, where each layer takes in numbers, transforms them, and passes them on to the next.</p>
<p>Each connection between layers has a weight, aka a number that determines how strongly one &ldquo;neuron&rdquo; influences another.
During training, the model adjusts these weights millions (or billions) of times to reduce the gap between its predictions and the actual data.
This process is called back propagation, and it&rsquo;s basically a giant optimisation loop: the model makes a guess, sees how wrong it was, tweaks the weights, and tries again.
Over time, it gets surprisingly good at spotting patterns in language.</p>
<p><div class="not-prose"><figure>
    <img class="h-auto max-w-full rounded-lg"
      src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-1/shiny-side-of-llms-back-propagation-1.png"
      alt="" 
      loading="lazy"
    >
  </figure></div>
</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-1/shiny-side-of-llms-back-propagation-2.png" alt="A simplified explanation of back propagation in neural networks" />
<figcaption aria-hidden="true">A simplified explanation of back propagation in neural networks</figcaption>
</figure>
<p>The bigger the model, the more opportunities it has to learn how to make good guesses.
That&rsquo;s why the &ldquo;Large&rdquo; in &ldquo;Large Language Model&rdquo; is so important.
In the last few years, model size and model capability has exploded.
This is due to hardware improvements (something has to crunch all that data), better training techniques and something called transformers.
Ah, jargon.</p>
<p>A transformer is a kind of model architecture.
&ldquo;Model architecture&rdquo; sounds difficult, but in simple terms this is a blueprint for how a model processes and learns from data.
It was introduced by <a href="https://arxiv.org/pdf/1706.03762" target="_blank" rel="noopener">Vaswani et al. (2017)</a>
.
Transformers are part of the neural network family, and they&rsquo;re designed to handle huge amounts of data quickly and efficiently, which makes them perfect for crunching lots of text.</p>
<p>What made transformers such a game-changer is a clever idea called attention.
Instead of reading text word by word like older models, transformers look at the whole sentence (or paragraph) at once and figure out which words matter most to each other.
That&rsquo;s how large language models can generate surprisingly accurate, context-aware text.</p>
<p>They also made it possible to train on much bigger chunks of data and pick up more subtle patterns in language.
This is combined with a technique called self-supervised learning, where the model learns to predict missing words in sentences.
If you show it &ldquo;I lifted some ___&rdquo; it learns that &ldquo;weights&rdquo; is more likely than &ldquo;horses&rdquo;.
Because the data itself provides the training signals, we don&rsquo;t need humans to label anything.
That makes training scalable.
It doesn&rsquo;t mean it&rsquo;s totally label-free, though.
The &ldquo;labels&rdquo; (like missing words) just come naturally from the text.</p>
<p>The combo of smart architecture and massive training data is what unlocked the big leap in quality of today&rsquo;s LLMs and made models like ChatGPT possible.
Fun fact: the &ldquo;T&rdquo; in GPT stands for Transformer.
Now you know why that is!</p>
<p>From statistics to transformers, to models we can chat with and help us to get things done faster.
How&rsquo;s that for the &ldquo;ancient&rdquo; statistical techniques?!</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-1/shiny-side-of-llms-stats.png" alt="It all starts with statistics" />
<figcaption aria-hidden="true">It all starts with statistics</figcaption>
</figure>
<p>It all sounds too good to be true: a model that learns on its own, getting &ldquo;smarter&rdquo; with every chunk of input you provide.
And it indeed is, because with this self-supervised learning come challenges too.
As more online text is generated by LLMs, there&rsquo;s a risk the model ends up learning from itself.
It&rsquo;s like copying a copy: input gets blurry.
And in the end LLM are just models that predict something based on the data it has been given.
If that data is garbage, so is the outcome.
If we don&rsquo;t want to end up with boring, soulless, similar, and even incorrect content, we still have to write content ourselves.</p>
<p>Still, one thing is clear: what makes LLMs so powerful is the amount of data they learn from.
And that leads us straight into the magic of scale.</p>
<h1 id="the-magic-of-scale">The magic of scale
</h1>
<p>As LLMs are trained on more and more data, something cool happens: they begin to show emergent abilities (<a href="https://doi.org/10.48550/arXiv.2206.07682" target="_blank" rel="noopener">Wei et al., 2022</a>
).
These are skills the model wasn&rsquo;t directly trained for, yet still manages to do.
Nobody explicitly programmed an LLM to summarise text, translate between languages, or generate code.
But once the model is large enough and has seen enough examples, these abilities just&hellip; emerge.
Magic!</p>
<p>This is different from what we call generative abilities.
&ldquo;Generative&rdquo; simply means the model can generate new text.
All LLMs are generative and can produce new text.
You give them a prompt, and they predict what comes next.
That&rsquo;s their basic job and it&rsquo;s expected behaviour.
Emergent abilities were not in the original job description and the model just suddenly gets good at tasks nobody told it to do.
It&rsquo;s surprising behaviour.</p>
<p>For example, you can ask it to convert a sentence into pirate-speak, even if it was never explicitly trained to do that.
It uses general language patterns it has learned to give it a try, which is known as zero-shot learning.
And if the model struggles to understand what you want, you can help it by giving a few examples in your prompt: few-shot learning.
If you want a list reformatted in a certain way, you can provide some examples and let the LLM finish the rest.</p>
<p>All of this is possible not because the model is &ldquo;intelligent&rdquo; in a human sense, but because it has seen so much text and learned such general patterns that it can apply them in all kinds of situations.
This ability to generalise, or doing more with less instruction, is one of the most powerful outcomes of large-scale learning.</p>
<h1 id="what-llms-arent">What LLMs aren&rsquo;t
</h1>
<p>So you might think LLMs are pretty smart.
After all, they can write essays, answer questions, and even debug your Python and R code.
It&rsquo;s the shiny side of LLMs.
But it isn&rsquo;t all rainbows and unicorns: LLMs aren&rsquo;t smart and they aren&rsquo;t thinking.
LLMs don&rsquo;t reason like humans.
They don&rsquo;t understand what they&rsquo;re saying.
They don&rsquo;t &ldquo;know&rdquo; facts in the way we do.
What they do is predict the next word in a sentence, based on patterns they&rsquo;ve seen in massive amounts of text.</p>
<p>That means an LLM can be confidently wrong.
They might &ldquo;hallucinate&rdquo; an answer that sounds plausible but is completely made up.
The code it generates might look solid, but when you actually run it you notice that it contains packages that don&rsquo;t exist or variable names that were never defined.
They might reflect or even amplify biases in their training data.
They might sound accurate, but miss the mark entirely.
And crucially, you don&rsquo;t really control them.
You can&rsquo;t force an LLM to always give a safe or correct answer.
What you can do is guide it with better prompts, build tools around it that check its output, and design systems that use the model responsibly.</p>
<p>So while LLMs are great at generalising across tasks, that power doesn&rsquo;t come from intelligence.
It comes from scale.
And that is important to remember when building with LLMs.</p>
<h1 id="lets-talk-about-context">Let&rsquo;s talk about context
</h1>
<p>As mentioned earlier, LLMs can understand and generate surprisingly accurate, context-aware text.
And that context deserves some attention (literally: remember transformers!).
It&rsquo;s the context that makes sure an LLM doesn&rsquo;t blurt out random sentences (or, ok, only occasionally do).
But what does &ldquo;context&rdquo; actually mean for a Large Language Model?</p>
<p>It all starts with tokens, which are tiny chunks of text.
A token can be a word, a part of a word, or even just characters or punctuation.
Every time you type something into an LLM, your input is split into these tokens.
Depending on the model, an LLM can &ldquo;see&rdquo; a certain number of tokens at once.
This is called the context window.
Think of it like a sliding window over a long scroll of text.
The model can only see what fits inside that window.
If your input is too long and tokens fall outside of that window: too bad.
An LLM might trims the beginning or the end and hopes you didn&rsquo;t put anything important there.
The exact behaviour varies by provider and how the system around the model is designed.
Some providers implement strategies like summarising or condensing previous conversation history, while others simply drop older messages to make room for new ones.
And in some cases, the system might reject the request outright rather than attempt to process an incomplete prompt.</p>
<p>But wait&hellip; We previously learned LLMs were trained on all of language right?
Yes.
But at generation time, it doesn&rsquo;t remember everything it&rsquo;s ever seen.
It only has this input, right now, and the tokens inside the window.
That&rsquo;s its entire world.
There&rsquo;s no long-term memory (unless you&rsquo;ve built that in, we&rsquo;ll get to that later).
So if you want the model to give you a good answer, what you say really matters.
And that&rsquo;s not the only thing: it also matters how you say it.</p>
<p>Imagine these two prompts:</p>
<pre><code>1. “Create a Shiny for Python app that plots a histogram of random numbers”
2. “Plot a histogram of random numbers. Use Shiny for Python.”
</code></pre>
<p>Same words.
Different order.
Potentially different behaviour.</p>
<p>Language models are super sensitive to the flow of a sentence.
If you put instructions at the start, the model is more likely to follow them.
Bury them at the end, and it might treat it more like a side-note.
Clarity is an LLMs best friend.</p>
<p>So context isn&rsquo;t just about what&rsquo;s in the window.
It&rsquo;s also about where words are and how they&rsquo;re phrased.
Order and surrounding words matter.
In the model&rsquo;s brain, each token looks around (thanks to that attention trick you met earlier) and asks, &ldquo;who&rsquo;s standing next to me, and what story are we telling?&rdquo;
Those neighbouring words:</p>
<ol>
<li><strong>Disambiguate meaning:</strong> words on their own only say so much. &ldquo;Bass&rdquo; is a fish next to &ldquo;river&rdquo;, and a guitar next to &ldquo;band.&rdquo;</li>
<li><strong>Signal tone:</strong> a single word can flip the sentiment. For example, &ldquo;nice&rdquo; after &ldquo;not&rdquo;. On its own, the word &ldquo;nice&rdquo; would be positive.</li>
<li><strong>Set expectations:</strong> since a model is trained on sequences of tokens actually written by humans, some sequences are familiar. So after &ldquo;Once upon a&rdquo; the model is primed for &ldquo;time,&rdquo; not &ldquo;ship&rdquo;.</li>
</ol>
<p>Because the model predicts the next word by weighing everything it can see in the window, every surrounding word tweaks the odds.
Change one word, and the whole probability landscape shifts.
This becomes clear with these different prompts:</p>
<pre><code>1. “Write a short horror story about a clown”
2. “Write a funny short horror story about a clown”
</code></pre>
<p>Can you already sense why &ldquo;prompt engineering&rdquo; became so hot?</p>
<p>Now back to the memory thing.
Out of the box, the model doesn&rsquo;t &ldquo;remember&rdquo; what you said three chats ago.
It doesn&rsquo;t even remember what you said two messages ago, unless you send the whole chat history in the current prompt.
So how big a prompt can get, determines how much an LLM can remember.
This brings us back to the context window.
The maximum number of tokens they can handle at once matter.
It could be 2000 or 128000 tokens.
That all depends on the model.
If your conversation gets longer than the model can hold, the oldest messages get chopped off.
That&rsquo;s why in very long chats, you might notice an LLM starts &ldquo;forgetting&rdquo; early facts.
These facts are not in the context window anymore, so the model can&rsquo;t &ldquo;see&rdquo; them anymore either.</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-1/shiny-side-of-llms-chat.png" alt="Ok, this isn’t a “very long” chat, but you get the idea: whatever is outside the context window is “lost” information" />
<figcaption aria-hidden="true">Ok, this isn’t a “very long” chat, but you get the idea: whatever is outside the context window is “lost” information</figcaption>
</figure>
<p>It is possible for models to have access to persistent memory across conversations, but in that case facts and knowledge have to be explicitly stored somewhere.
This memory is separate from the context window.
Whether and how that memory gets updated depends on the broader system designed by the LLM provider.
This system, the surrounding infrastructure and logic around the model, determines what&rsquo;s worth saving.
Only if something seems important enough the memory will be updated.</p>
<h1 id="from-tokens-to-embedding-to-encoding">From tokens to embedding to encoding
</h1>
<p>Your prompt is divided into tokens, those tiny chunks of text.
But computers don&rsquo;t &ldquo;understand&rdquo; words like we do.
Computers work with numbers.
That&rsquo;s where something fancy called &ldquo;embedding&rdquo; comes in: each token is turned into numbers that represents its meaning.
And these numbers are not a plain &ldquo;4982&rdquo; or &ldquo;2974&rdquo;, but rather a pattern of numbers.
You can compare it with an unique barcode.
To &ldquo;understand&rdquo; your prompt, the model looks at all these number patterns together and uses the earlier mentioned attention to figure out how they relate to each other and what the relationship between the words is.
This process is called encoding.
A well-known model that does this is BERT, which was introduced by Google in 2018 (<a href="https://doi.org/10.48550/arXiv.1810.04805" target="_blank" rel="noopener">Devlin et al., 2019</a>
).</p>
<p>Because of word embeddings and encoding, a prompt like &ldquo;Create a Shiny for Python app that plots a histogram&rdquo; is not just a set of random words.
The model knows that &ldquo;Shiny&rdquo; and &ldquo;Python&rdquo; relate to things like &ldquo;web app&rdquo; or &ldquo;dashboard&rdquo; because their embeddings are close together.
It gives the model directions on the tech stack to use.
It also sees that &ldquo;plot&rdquo; and &ldquo;histogram&rdquo; are both in the prompt, so it understands you want a chart.
And the word &ldquo;create&rdquo; signals that you&rsquo;re asking for code, not just an explanation.
All these relationships, links and signals are taken into account when generating the output token by token.</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-1/shiny-side-of-llms-code-gen.png" alt="From input to output: how an LLM generates code" />
<figcaption aria-hidden="true">From input to output: how an LLM generates code</figcaption>
</figure>
<p>So far, we only talked about generating language, like plain text or some code.
But what about images?
The latest models can take images as input and they spit out a polished (perhaps fake-looking) image when being asked too.
These multimodal models (like GPT-4o or Claude 3) combine both vision and language.
How?
By treating images like language!</p>
<blockquote>
<p><strong>Images and multimodal models</strong></p>
<p>When talking about images, we&rsquo;re taking a small side-step in our LLM journey.
Image tasks are typically handled by specialised models (like diffusion models for image generation) or by multimodal LLMs (like GPT-4o or Gemini) that can process both text and images.
So when we talk about &ldquo;LLMs&rdquo; generating or understanding images, we&rsquo;re really referring to LLMs with extra capabilities.</p>
</blockquote>
<p>Just like text is broken down into tokens, images are also converted into a form the model can understand.
Instead of words, an image is split into patches (tiny chunks of pixels).
You can think of this as &ldquo;tokenizing an image.&rdquo; And just like tokens, patches are turned into numbers that capture the meaning of those pixels, which brings us back to the fancy word: &ldquo;embedding&rdquo;.
These image embeddings are then processed by the model just like text embeddings.
That&rsquo;s how a model can look at an image and describe it or answer questions about it.</p>
<p>Generating new images is also not a problem: in that case embeddings are translated to a visual representation (pixels) and a full image is being built from it.
This is exactly what well-known generative vision models like DALL·E, Midjourney, or Stable Diffusion do: they&rsquo;re trained to turn a sequence of text tokens into a sequence of image tokens, gradually &ldquo;painting&rdquo; a picture that matches the prompt.
If we ask Stable Diffusion for &ldquo;A weightlifting Dalmatian holding a barbell with red plates&rdquo;, it has no problem doing so.
It&rsquo;s all about processing the embeddings together and figure out how every word relates to each other.
In this case, &ldquo;weightlifting&rdquo; modifies what the &ldquo;Dalmatian&rdquo; should look like, and &ldquo;holding&rdquo; links &ldquo;Dalmatian&rdquo; and &ldquo;barbell&rdquo;.</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-1/shiny-side-of-llms-image-gen.png" alt="Generating images works pretty similar to generating language for LLMs with those extra capabilities" />
<figcaption aria-hidden="true">Generating images works pretty similar to generating language for LLMs with those extra capabilities</figcaption>
</figure>
<p>Note that depending on the model you&rsquo;re using, you might need to provide more context in your prompt by saying something like &ldquo;Generate an image that shows&hellip;&rdquo;.
This is especially important for multimodal models, like GPT-4o or Gemini, which can generate text, images, audio, and more.
A prompt like &ldquo;A weightlifting Dalmatian holding a barbell with red plates&rdquo; might seem clear to you and me, but to a multimodal model, it could mean several things: perhaps you want an image, or a description, or a funny meme.
This is different for a model like Stable Diffusion, a text-to-image model, which is trained specifically for image generation.
Naturally it will assume you want an image when you give it a prompt.</p>
<h1 id="more-jargon-rag-and-vector-databases">More jargon: RAG and vector databases
</h1>
<p>LLMs can only generate such &ldquo;human-like&rdquo; language because they are good at embeddings, encodings, and understanding context.
Context is everything.
This context comes from the &ldquo;knowledge&rdquo; an LLM has, or from you.
On their own, LLMs can only use data that has been used during training to generate output.
LLMs only &ldquo;know&rdquo; facts that happened during their training process (and not after), and proprietary information is off limits.
That doesn&rsquo;t mean you can&rsquo;t let an LLM use new facts, or private company info.
You can add it to the context, taking the context window into account.
So what do you do when you want to add so much information that it doesn&rsquo;t fit that window?
Hello, RAG!</p>
<p>It&rsquo;s probably not the first time you encountered the term &ldquo;RAG&rdquo;.
RAG stands for Retrieval-Augmented Generation (<a href="https://doi.org/10.48550/arXiv.2005.11401" target="_blank" rel="noopener">Lewis et al., 2020</a>
).
It means that instead of relying only on what the language model was trained on, it first retrieves relevant information (like from a document or database) and then generates a response using both your prompt and that retrieved info.
This helps the model give more accurate and up-to-date answers.</p>
<p>&ldquo;Retrieving information&rdquo; sounds simple when you just have one document, but imagine you have a big pile of documents.
That&rsquo;s a bit more complicated.
To make retrieval easier, RAG systems usually break documents into smaller chunks, like paragraphs or even sentences.
Each chunk gets turned into a vector and stored in a vector database.
When you ask a question, the system searches for the chunks whose vectors are closest in meaning to your query and uses those to be added to the context.
It&rsquo;s all about making the most of the context window.</p>
<h1 id="so-many-models">So many models
</h1>
<p>We&rsquo;re not talking about one big giant LLM in this blog series.
It&rsquo;s plural: LLMs.
That&rsquo;s because there are a lot of different LLMs out there.
All these models are trained a little differently.
That&rsquo;s important, because how a model is trained (what and how many data it saw, what tasks it practiced, what architecture it uses) shapes what it&rsquo;s good at.
Some models are trained mostly on text (think books, articles, web pages), while others are trained more heavily on code (like GitHub repos).
That&rsquo;s why some models might be better suited as your coding-companion, and others may serve you well as your ghostwriter.</p>
<p>There are many ways to compare LLMs, but here are few things you might want to consider:</p>
<ul>
<li><strong>Context window</strong>: larger windows help with long documents or multi-turn conversations.</li>
<li><strong>Training data</strong>: what kind of data the model was exposed to (e.g. text, code, math problems, dialogue, or some mix).</li>
<li><strong>Instruction tuning</strong>: some models are trained to follow prompts more precisely, often using human feedback (this has a very lengthy name: Reinforcement Learning from Human Feedback, aka RLHF).</li>
<li><strong>Output quality</strong>: ah, there it is! Probably the the most important thing. How good are the model&rsquo;s answers?! This depends on everything above, plus things like formatting ability, reasoning steps, or hallucination rates.</li>
<li><strong>Cost and speed</strong>: we&rsquo;ll come back when actually talking to LLMs, but not all models are priced equally, and speed can vary a lot.</li>
</ul>
<p>Since output quality is probably one of your top priorities, you might wonder how you can quickly get a sense of LLM performance.
You can try out different models, sure.
But as a data-savvy person you&rsquo;re probably keen to turn towards a more objective method.
Luckily, there are tools like <a href="https://llm-stats.com/" target="_blank" rel="noopener">llm-stats.com</a>
, <a href="https://www.vellum.ai/open-llm-leaderboard" target="_blank" rel="noopener">Vellum&rsquo;s leaderboard</a>
, and <a href="https://lmarena.ai/leaderboard" target="_blank" rel="noopener">LM Arena</a>
 that help you with a comparison.
The latter is based on subjective human ratings of output quality across tasks.</p>
<p>Looking at these leaderboards you see all the popular models like GPT (OpenAI), Claude (Anthropic), Gemini (Google), Llama (Meta), DeepSeek, Mistral and Nemotron (NVIDIA).
And all of these models have different versions too!
It might spin your head.
Each model has strengths and weaknesses.
Some are better with long input, some are cheaper, some are open source&hellip; But honestly, don&rsquo;t stress too much about picking the &ldquo;perfect&rdquo; one.
Once you start integrating an LLM into your Shiny app, it&rsquo;s easy to change models later if you need to.
At this stage, it&rsquo;s more important to just get started.</p>
<p>When it comes to personal experience and &ldquo;word on the street&rdquo; from Python and R developers: with Claude you&rsquo;re generally good when it comes to writing code (e.g. as a copilot) and summarising text.
If you&rsquo;re looking for a model with a large context window, Gemini might be what you would be after.
And your friendly neighbourhood LLM could potentially be found in GPT models (like GPT-4o) which generally have a good balance between reasoning, code, and creative tasks.</p>
<h1 id="building-with-llms">Building with LLMs
</h1>
<p>LLMs can be applied in a wide range of natural language tasks.
They&rsquo;re perfect for building chatbots or virtual assistants.
They can generate content, which can be blog posts, emails, or code snippets.
They can handle machine translation across languages, summarise long reports into key points, and perform sentiment analysis to extract opinions or emotional tone from text.
They even can do data analysis for you.</p>
<p>It&rsquo;s worth recognising that the examples above can have multiple use cases: you can use LLMs to increase your own productivity, or you can move from using an LLM to building with one.
As a copilot, an LLM can help you write code, draft content, summarise documents, or generate ideas, which (hopefully) makes you faster and more efficient in your day-to-day work.
But when you start building tools for others, the focus shifts.
Now it&rsquo;s not just about what the model can do, but how you wrap it in a helpful interface.
You need to think about the user&rsquo;s problem, how they interact with your tool, and how to guide their input to get meaningful results.
That&rsquo;s a different kind of challenge.
And this is exactly where something like Shiny shines.
Whether you&rsquo;re using Python or R, Shiny makes it easy to build interactive, user-friendly applications that connect to an LLM behind the scenes.
You can quickly prototype ideas, test interfaces, and deliver real value.
It&rsquo;s the perfect bridge between your data science skills and creating practical tools powered by LLMs.
The only thing you need is a bit of creativity, some coding skills, and a foundational understanding of LLMs.
The latter we tackled already!</p>
<p>So what are we going to build in this blog series?
Imagine you need to present something to your colleagues in the development team, you&rsquo;re a little nervous and would like to have some feedback on the presentation first.
Since you&rsquo;re a data scientist who loves reproducible slides, you&rsquo;ve chosen to build a <a href="https://quarto.org" target="_blank" rel="noopener">Quarto presentation</a>
.
Wouldn&rsquo;t it be wonderful if you could upload your slides in an app and get instant feedback?
Like the perfect &ldquo;Presentation Rehearsal Buddy&rdquo; that analyses your slides and gives you tailored suggestions?
And wouldn&rsquo;t it be great if you can help other data scientists with the same problem?
You can even prevent your colleagues from giving too lengthly and unfocussed presentations.
Also, it&rsquo;s great for polishing your talks at conferences, like <a href="https://posit.co/conference/" target="_blank" rel="noopener">posit::conf(2025)</a>
.
Now that&rsquo;s a cool app!</p>
<p>And it&rsquo;s the kind of thing you can actually start building now you understand what LLMs can and can&rsquo;t do.
But wait&hellip; I can feel your creativity flowing, the app idea growing on you, and I understand you might be dreaming bigger.
Maybe a model trained on your own (sensitive) company data to really tailor presentations to your work?
Amazing!
You probably heard it&rsquo;s not the best idea to send sensitive data to just any public API, so your mind jumps to a local LLM: one that runs on your own machine(s), just for you.
A private model that knows everything about your org&hellip; That&rsquo;s tempting!
And while it sounds wonderful, it&rsquo;s probably not the best place to start.</p>
<p>Running your own LLM comes with lots of complexity: infrastructure, cost, fine-tuning, and security.
It&rsquo;s a bit like buying a race car before learning to drive, then trying to build the track and engine from scratch.
A better place to begin?
Use the best pre-trained models that are already available.</p>
<p>And remember the magic of scale: LLMs show their real power only when trained on massive amounts of data and compute.
That&rsquo;s where those emergent abilities come from.
Why reinvent the wheel when you can just take it for a spin?
There&rsquo;s a reason why the best performing models can&rsquo;t be run on a laptop!</p>
<h1 id="responsible-use-and-regulations">Responsible use and regulations
</h1>
<p>Ready, set, &hellip; disclaimers.
Damn.
This is not an ethics class.
We&rsquo;re here to code.
But we can&rsquo;t get around some disclaimers, and if you&rsquo;re using an LLM, you should take some things into account when it comes to responsible use:</p>
<ul>
<li>Perhaps this goes without saying, but you can&rsquo;t bluntly copy and paste data or (sensitive) information into any LLM. That means your own data, and somebody else&rsquo;s data. Generated or uploaded input (text, data, prompts, images, etc.) could (and will) be used for other purposes, such as the training of AI models. If you don&rsquo;t want things to be out in the open, you shouldn&rsquo;t input it unless you&rsquo;re really sure that data will not be re-used.</li>
<li>You can&rsquo;t trust an LLM completely either. You must always take a critical approach to using the output produced and be aware of the limitations, such as bias, hallucinations and inaccuracies.</li>
<li>You&rsquo;re accountable for the integrity of the content generated by or with the support of an LLM. There&rsquo;s also an important difference between <em>using</em> an LLM for your own tasks and <em>building</em> tools on top of LLMs that others will rely on. In the first case, you&rsquo;re the human in the loop, judging what to trust and what to ignore. But when others interact with your app, you&rsquo;re responsible for the entire experience: how the model is prompted, how outputs are presented, and whether users are warned of uncertainty. In any case, if you use an LLM to make decisions or build an app that others use to act upon, you can&rsquo;t blame the LLM for giving false directions.</li>
</ul>
<p>Many organisations know this too, and it makes them wary of using LLM-based tools.
Especially in regulated environments, there&rsquo;s a natural hesitation to adopt these tools without strong safeguards in place.
If you want your organisation to be open to the idea, you need to think about clear data handling policies, usage boundaries, a transparent explanation of how the model is prompted, what kind of outputs are produced, and where the limitations lie.
The more control and clarity you can offer, the more trust you&rsquo;ll build.</p>
<p>If your organisation already trusts a cloud vendor like AWS, Azure, or Google Cloud with your data, then using an LLM hosted by that same vendor often fits within existing security and compliance frameworks, which might make it easier to justify adoption.</p>
<p>It&rsquo;s also good to be aware of applicable regulations and guidelines.
Know what you can and can&rsquo;t use an LLM for when building your application.
For example, in Europe, there&rsquo;s the <a href="https://digital-strategy.ec.europa.eu/en/policies/regulatory-framework-ai" target="_blank" rel="noopener">AI Act</a>
 (which is the first-ever legal framework on AI).
Generally speaking, legislation is not that fast, but since AI capabilities are quickly expanding it&rsquo;s only a matter of time before rules and guidelines do catch up.
Stay informed!</p>
<p>And with those less fun but necessary warnings we wrap up this introduction to LLMs.
It&rsquo;s not a magic all-mighty black box anymore: you now know exactly what LLMs are and what they are not.</p>
<h1 id="coming-up-talking-to-llms">Coming up: talking to LLMs
</h1>
<p>Our app for polished Quarto presentations is just around the corner, but before we can build it, we need to learn how to actually talk to an LLM.
Programmatically that is.
After all, the magic only happens once you know how to send the right prompts, pass along the right content, and make sense of what comes back.
In our case, that means: extracting a presentation&rsquo;s content, feeding it to the model with smart prompts, and formatting the output into something useful.
Think about value boxes, improvement tips, or even edits to your Quarto file directly.
That&rsquo;s where we&rsquo;re headed.
But first, the next step in our app-building journey: talking to an LLM.</p>
<h1 id="references">References
</h1>
<p>Devlin, J., Chang, M. W., Lee, K., &amp; Toutanova, K.
(2019).
BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding.
<em>arXiv.</em> <a href="https://doi.org/10.48550/arXiv.1810.04805" target="_blank" rel="noopener">https://doi.org/10.48550/arXiv.1810.04805</a>
</p>
<p>Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, N., Küttler, H., Lewis, M., Yih, W., Rocktäschel, T., Riedel, S., &amp; Kiela, D.
(2020).
Retrieval‑augmented generation for knowledge‑intensive NLP tasks.
<em>arXiv</em>.
<a href="https://doi.org/10.48550/arXiv.2005.11401" target="_blank" rel="noopener">https://doi.org/10.48550/arXiv.2005.11401</a>
</p>
<p>Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, Ł., &amp; Polosukhin, I.
(2017).
Attention is all you need.
<em>arXiv</em>.
<a href="https://doi.org/10.48550/arXiv.1706.03762" target="_blank" rel="noopener">https://doi.org/10.48550/arXiv.1706.03762</a>
</p>
<p>Wei, J., Tay, Y., Bommasani, R., Raffel, C., Zoph, B., Borgeaud, S., Yogatama, D., Bosma, M., Zhou, D., Metzler, D., Chi, E. H., Hashimoto, T., Vinyals, O., Liang, P., Dean, J., &amp; Fedus, W.
(2022).
Emergent Abilities of Large Language Models (Version 2).
<em>arXiv.</em> <a href="https://doi.org/10.48550/arXiv.2206.07682" target="_blank" rel="noopener">https://doi.org/10.48550/arXiv.2206.07682</a>
</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/shiny/shiny-side-of-llms-part-1/shiny-side-of-llms-header.png" length="493133" type="image/png" />
    </item>
    <item>
      <title>From One Notebook to Many Reports: Parameterized reports with the `jupyter` engine</title>
      <link>https://posit-open-source.netlify.app/blog/quarto/2025-07-24-parameterized-reports-python/</link>
      <pubDate>Thu, 24 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/quarto/2025-07-24-parameterized-reports-python/</guid>
      <dc:creator>Charlotte Wickham</dc:creator><description><![CDATA[<blockquote>
<p><strong>Based on a talk at SciPy 2025</strong></p>
<p>This post is based on the talk &ldquo;From One Notebook to Many Reports: Automating with Quarto&rdquo; delivered at <a href="https://www.scipy2025.scipy.org" target="_blank" rel="noopener">SciPy 2025</a>
 by Charlotte Wickham.
You can find the slides at <a href="https://cwickham.github.io/one-notebook-many-reports/" target="_blank" rel="noopener">cwickham.github.io/one-notebook-many-reports</a>
 and example code at <a href="https://github.com/cwickham/one-notebook-many-reports" target="_blank" rel="noopener">github.com/cwickham/one-notebook-many-reports</a>
.</p>
</blockquote>
<h2 id="the-problem-repetitive-reporting">The Problem: Repetitive Reporting
</h2>
<p>Would you rather read a generic &ldquo;Climate summary&rdquo; or a &ldquo;Climate summary for <em>exactly where you live</em>&rdquo;? Reports that are personalized to a specific situation increase engagement and connection. But producing many customized reports manually is tedious and error-prone.</p>
<p>Quarto solves this with parameterized reports&mdash;you create a single document template, then render it multiple times with different parameter values to generate customized outputs automatically.</p>
<p>A great example is the customized soil health reports from Washington Soil Health Initiative&rsquo;s <a href="https://washingtonsoilhealthinitiative.com/state-of-the-soils/" target="_blank" rel="noopener">State of the Soils Assessment</a>
, presented at posit::conf(2023) by <a href="https://jadeyryan.com" target="_blank" rel="noopener">Jadey Ryan</a>
 (watch on <a href="https://youtu.be/lbE5uOqfT70?si=C-d5U5Q2VXo1wlDs" target="_blank" rel="noopener">YouTube</a>
). Jadey demonstrated this approach using R and plain text Quarto files (<code>.qmd</code>).</p>
<p>This post shows you how to apply the same principles using Python: we&rsquo;ll walk through converting a Jupyter notebook (<code>.ipynb</code>) into a parameterized report, then automating the generation of multiple customized outputs. Then I&rsquo;ll give you some tips for making your reports look polished.</p>
<h2 id="the-solution-parameterized-reports">The Solution: Parameterized Reports
</h2>
<h3 id="start-with-a-notebook">Start with a notebook
</h3>
<p>As an example, let&rsquo;s start with a Jupyter notebook analyzing climate data for Corvallis, Oregon.</p>
<figure id="corvallis-ipynb">
<img src="https://posit-open-source.netlify.app/blog/quarto/2025-07-24-parameterized-reports-python/corvallis-ipynb.png" data-fig-alt="Screenshot of a Jupyter notebook with code cells and output, including a plot and text summary." alt="corvallis.ipynb" />
<figcaption aria-hidden="true"><a href="https://github.com/cwickham/one-notebook-many-reports/blob/main/01-one-notebook/corvallis.ipynb"><code>corvallis.ipynb</code></a></figcaption>
</figure>
<p>You can see the full notebook, <a href="https://github.com/cwickham/one-notebook-many-reports/blob/main/01-one-notebook/corvallis.ipynb" target="_blank" rel="noopener"><code>corvallis.ipynb</code>, on GitHub</a>
, but here are the key pieces:</p>
<ul>
<li>
<p>The code cells import some data for all of Oregon, and filter it to just rows relevant for Corvallis, then produce a summary sentence and a plot.</p>
</li>
<li>
<p>The document options specify <code>echo: false</code> so no code appears in the final output, and <code>format: typst</code> so the output is a PDF produced via <a href="https://typst.app" target="_blank" rel="noopener">Typst</a>
, a modern alternative to LaTeX.</p>
</li>
</ul>
<p>This single notebook can be rendered with Quarto:</p>
<p><strong>Terminal</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">quarto render corvallis.ipynb 
</span></span></code></pre></td></tr></table>
</div>
</div><p>The result is a PDF file, <code>corvallis.pdf</code>, a simple report with the title &ldquo;Corvallis&rdquo; and a single sentence summary of the climate data, along with a plot highlighting the mean temperature for this year against the last 30 years.</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/quarto/2025-07-24-parameterized-reports-python/corvallis-pdf.png" data-fig-alt="Screenshot of a PDF file with the title &#39;Corvallis&#39; that contains a single sentence summary and a plot." alt="corvallis.pdf" />
<figcaption aria-hidden="true"><code>corvallis.pdf</code></figcaption>
</figure>
<p>Now, imagine we want to create this report for the 50 largest cities in Oregon.
Here&rsquo;s the steps we&rsquo;ll take:</p>
<ol>
<li>Turn hardcoded values into variables</li>
<li>Declare those variables parameters</li>
<li>Render the notebook with different parameter values</li>
<li>Automate rendering with many parameter values</li>
</ol>
<h3 id="1-turn-hardcoded-values-into-variables">1. Turn hardcoded values into variables
</h3>
<p>We want a report for each city.
We&rsquo;ll start by creating a variable, <code>city</code>, which we&rsquo;ll designate a parameter in our next step.
In a new code cell at the top of our notebook, we define the variable:</p>
<p><strong>code</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">city</span> <span class="o">=</span> <span class="s2">&#34;Corvallis&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Then anywhere we previously hardcoded <code>&quot;Corvallis&quot;</code> in the notebook, we replace it with this variable.</p>
<p>The first occurrence is in the title of the document.
Originally, we had a markdown cell defining a level 1 heading:</p>
<p><strong>markdown</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gh"># Corvallis
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>We replace it with a code cell that uses an f-string to produce markdown for a level 1 heading based on the <code>city</code> variable:</p>
<p><strong>code</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">Markdown(f&#34;# {city}&#34;)
</span></span></code></pre></td></tr></table>
</div>
</div><p>In the filtering step the replacement is straightforward, we just change the string to the variable:</p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr>
<td style="text-align: left;"><div width="50.0%" data-layout-align="left">
<p>Before:</p>
<div class="code-with-filename">
<strong>code</strong>
<div class="sourceCode" id="cb1" data-filename="code"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>tmean <span class="op">=</span> tmean_oregon.<span class="bu">filter</span>(</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>    pl.col(<span class="st">&quot;city&quot;</span>) <span class="op">==</span> <span class="st">&quot;Corvallis&quot;</span>,</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>)</span></code></pre></div>
</div>
</div></td>
<td style="text-align: left;"><div width="50.0%" data-layout-align="left">
<p>After:</p>
<div class="code-with-filename">
<strong>code</strong>
<div class="sourceCode" id="cb2" data-filename="code"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>tmean <span class="op">=</span> tmean_oregon.<span class="bu">filter</span>(</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>    pl.col(<span class="st">&quot;city&quot;</span>) <span class="op">==</span> city,</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>)</span></code></pre></div>
</div>
</div></td>
</tr>
</tbody>
</table>
<p>Finally, the plot code (using <a href="https://plotnine.org" target="_blank" rel="noopener">plotnine</a>
), sets the title of the plot to include the city name:</p>
<p><strong>code</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></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="o">...</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span> <span class="n">labs</span><span class="p">(</span><span class="n">title</span> <span class="o">=</span> <span class="s2">&#34;Corvallis, OR&#34;</span><span class="p">,</span> <span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>We can also use an f-string here to include the <code>city</code> variable:</p>
<p><strong>code</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></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="o">...</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span> <span class="n">labs</span><span class="p">(</span><span class="n">title</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">city</span><span class="si">}</span><span class="s2">, OR&#34;</span><span class="p">,</span> <span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Now, we should be able to test our changes by explicitly setting the <code>city</code> variable to something other than &ldquo;Corvallis&rdquo; and re-running the cells.
Since our report is no longer specific to Corvallis, we can rename it <code>climate.ipynb</code>.</p>
<h3 id="2-declare-those-variables-parameters">2. Declare those variables parameters
</h3>
<p>Now we have a variable that represents the parameter, we need to let Quarto know it&rsquo;s a parameter.
Quarto&rsquo;s parameterized reports are implemented using <a href="https://papermill.readthedocs.io/en/latest/" target="_blank" rel="noopener">Papermill</a>
, and inherit Papermill&rsquo;s approach: tag the cell defining the parameter with <code>parameters</code>.</p>
<p>In Jupyter, you can add this tag through the cell toolbar:</p>
<img src="https://posit-open-source.netlify.app/blog/quarto/2025-07-24-parameterized-reports-python/corvallis-add-tag.png" data-fig-alt="Screenshot of a Jupyter notebook cell with a tag &#39;parameters&#39; added to it." />
<p>You can see the updated notebook, now a parameterized notebook, on GitHub: <a href="hhttps://github.com/cwickham/one-notebook-many-reports/blob/main/02-one-parameterized-report/climate.ipynb"><code>climate.ipynb</code></a>
.</p>
<h3 id="3-render-with-different-parameter-values">3. Render with different parameter values
</h3>
<p>If we render <code>climate.ipynb</code>, it will still produce the same report for Corvallis, because we haven&rsquo;t changed the parameter value:</p>
<p><strong>Terminal</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">quarto render climate.ipynb
</span></span></code></pre></td></tr></table>
</div>
</div><p>But we can now pass parameter values to Quarto with the <code>-P</code> flag:</p>
<p><strong>Terminal</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><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-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Generate report for Portland</span>
</span></span><span class="line"><span class="cl">quarto render climate.ipynb -P city:Portland --output-file portland.pdf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Generate report for Eugene  </span>
</span></span><span class="line"><span class="cl">quarto render climate.ipynb -P city:Eugene --output-file eugene.pdf
</span></span></code></pre></td></tr></table>
</div>
</div><p>We&rsquo;ve also added <code>--output-file</code> to ensure each report gets its own filename.</p>
<h3 id="4-automate-rendering-with-many-parameter-values">4. Automate rendering with many parameter values
</h3>
<p>To generate all 50 reports, we need to run <code>quarto render</code> 50 times, each time with a different city as the parameter value.
You could automate this in many ways, but let&rsquo;s use a Python script.
For example, you might have a dataset of cities and their corresponding output filenames:</p>
<p><strong>gen-reports.py</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></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">cities</span> <span class="o">=</span> <span class="n">pl</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;city&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;Portland&#34;</span><span class="p">,</span> <span class="s2">&#34;Cottage Grove&#34;</span><span class="p">,</span> <span class="s2">&#34;St. Helens&#34;</span><span class="p">,</span> <span class="s2">&#34;Eugene&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;output_file&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;portland.pdf&#34;</span><span class="p">,</span> <span class="s2">&#34;cottage_grove.pdf&#34;</span><span class="p">,</span> <span class="s2">&#34;st_helens.pdf&#34;</span><span class="p">,</span> <span class="s2">&#34;eugene.pdf&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>I&rsquo;ve generated a small example above, but in reality you would likely read <code>cities</code> in from a file.
Then you could iterate over the rows of this dataset, rendering the notebook for each city:</p>
<p><strong>gen-reports.py</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></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">quarto</span> <span class="kn">import</span> <span class="n">render</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">cities</span><span class="o">.</span><span class="n">iter_rows</span><span class="p">(</span><span class="n">named</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">render</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;climate.ipynb&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">execute_params</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;city&#34;</span><span class="p">:</span> <span class="n">row</span><span class="p">[</span><span class="s2">&#34;city&#34;</span><span class="p">]},</span>
</span></span><span class="line"><span class="cl">        <span class="n">output_file</span><span class="o">=</span><span class="n">row</span><span class="p">[</span><span class="s2">&#34;output_file&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Run this script once, and you&rsquo;ll get all 50 custom reports!</p>
<p>You can find the complete working example on GitHub: <a href="https://github.com/cwickham/one-notebook-many-reports/tree/main/03-many-reports" target="_blank" rel="noopener">cwickham/one-notebook-many-reports/03-many-reports</a>
.</p>
<h2 id="pretty-reports-brand-and-typst">Pretty Reports: Brand and Typst
</h2>
<p>The steps above to produce parameterized reports apply to any output format supported by Quarto.
However, if you are targeting <code>typst</code> you can take advantage of additional features to create beautiful PDF reports.</p>
<h3 id="brandyml">Brand.yml
</h3>
<p>Quarto supports <a href="https://posit-dev.github.io/brand-yml/" target="_blank" rel="noopener">brand.yml</a>
 a way to specify colors, fonts, and logos:</p>
<p><strong>_brand.yml</strong></p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-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">palette</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">forest-green</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;#2d5a3d&#34;</span><span class="w">     
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">charcoal-grey</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;#555555&#34;</span><span class="w">    
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">foreground</span><span class="p">:</span><span class="w"> </span><span class="l">charcoal-grey    </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="l">forest-green       </span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">typography</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">fonts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">family</span><span class="p">:</span><span class="w"> </span><span class="l">Open Sans</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l">google</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">base</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">family</span><span class="p">:</span><span class="w"> </span><span class="l">Open Sans</span><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">medium</span><span class="p">:</span><span class="w"> </span><span class="l">logo.png</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Quarto will detect the <code>_brand.yml</code> file and apply the colors, fonts and logo to your report.
Colors and fonts in your figures will need to be customized in your code, but that is made much easier with the <a href="https://posit-dev.github.io/brand-yml/pkg/py/" target="_blank" rel="noopener">brand-yml</a>
 Python package which imports your values from <code>_brand.yml</code>.</p>
<p>You can see a full example of using <code>_brand.yml</code> with <code>climate.ipynb</code> at <a href="https://github.com/cwickham/scipy-talk/tree/main/04-branded-reports" target="_blank" rel="noopener">cwickham/one-notebook-many-reports/04-branded-reports</a>
, and learn more about Quarto&rsquo;s support for brand in the <a href="https://quarto.org/docs/authoring/brand.html" target="_blank" rel="noopener">Brand guide</a>
.</p>
<h3 id="typst">Typst
</h3>
<p>Learning a little bit of Typst syntax can take your reports from basic to beautiful.
You can include <a href="https://quarto.org/docs/output-formats/typst.html#raw-typst" target="_blank" rel="noopener">raw Typst syntax</a>
 in your notebooks, or wrap elements in Typst functions using the <a href="https://github.com/christopherkenny/typst-function" target="_blank" rel="noopener">typst-function Quarto extension</a>
.
As an example, you could add a header with the city name and a map of the location:</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/quarto/2025-07-24-parameterized-reports-python/corvallis-pretty-pdf.png" data-fig-alt="The `corvallis.ipynb` notebook rendered by Quarto to `pdf`. The document has dark green header with the city in white text and a map next to it with the location as an orange dot." alt="corvallis.pdf" />
<figcaption aria-hidden="true"><code>corvallis.pdf</code></figcaption>
</figure>
<p>You can see the source for this example at <a href="https://github.com/cwickham/one-notebook-many-reports/tree/main/05-pretty-reports" target="_blank" rel="noopener">cwickham/one-notebook-many-reports/05-pretty-reports</a>
.</p>
<h2 id="jupyter-vs-knitr"><code>jupyter</code> vs <code>knitr</code>
</h2>
<p>The steps for creating a parameterized report above are specific to documents that use the <code>jupyter</code> engine.
With a Jupyter notebook (<code>.ipynb</code>),
or a plain text Quarto document (<code>.qmd</code>) with only Python code cells,
Quarto will default to the <code>jupyter</code> engine.
As described above, the <code>jupyter</code> engine uses cell tags to identify parameters.</p>
<p>If you are working in a <code>.ipynb</code> file, your IDE will likely provide a way to add these tags through the cell toolbar.
If you are working in a <code>.qmd</code> file, you can add tags as a code cell option:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">``<span class="sb">`{python}
</span></span></span><span class="line"><span class="cl"><span class="sb">#| tags: [parameters]
</span></span></span><span class="line"><span class="cl"><span class="sb">city = &#34;Corvallis&#34;
</span></span></span><span class="line"><span class="cl"><span class="sb">`</span>``
</span></span></code></pre></td></tr></table>
</div>
</div><p>With the <code>jupyter</code> engine, parameters can then be accessed directly as variables, e.g. <code>city</code>, in later code cells.</p>
<p>If you are working in a Quarto document (<code>.qmd</code>) with R code cells, Quarto will default to the <code>knitr</code> engine.
With the <code>knitr</code> engine, you set parameters in the document header under <code>params</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></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="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">city</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Corvallis&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>In <code>knitr</code>, parameters are accessed as elements of <code>params</code>, e.g. <code>params$city</code>.</p>
<p>You can read more about setting and using parameters in <a href="https://quarto.org/docs/computations/parameters.html" target="_blank" rel="noopener">Guide &gt; Computations &gt; Parameters</a>
.</p>
<h2 id="wrapping-up">Wrapping Up
</h2>
<p>Parameterized reports turn one notebook into many customized outputs.
You&rsquo;ve seen the process of going from a notebook with a hardcoded value to a parameterized report that can be rendered with different values.
You can then automate the rendering in any way you choose to generate dozens of reports at once.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/quarto/2025-07-24-parameterized-reports-python/thumbnail.png" length="381693" type="image/png" />
    </item>
    <item>
      <title>Great Tables `v0.18.0`: Easy Column Spanners and More!</title>
      <link>https://posit-open-source.netlify.app/blog/great-tables/introduction-0.18.0/</link>
      <pubDate>Fri, 18 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/great-tables/introduction-0.18.0/</guid>
      <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>The development of Great Tables continues! We&rsquo;re excited to announce the release of <code>v0.18.0</code>, which brings several powerful new features. These features make it even easier to create beautiful, informative tables. The key additions in this release include new methods (and a tweak to an existing one):</p>
<ul>
<li><code>~~.GT.tab_spanner_delim()</code>: quick spanner creation</li>
<li><code>~~.GT.fmt_tf()</code>: easy boolean value formatting</li>
<li><code>~~.GT.cols_label_rotate()</code>: enables column label rotation</li>
<li><code>~~.GT.fmt_datetime()</code>: added <code>format_str=</code> parameter for extra customization</li>
</ul>
<p>Let&rsquo;s explore each of these interesting new features!</p>
<h3 id="quick-spanner-creation-with-tab_spanner_delim">Quick spanner creation with <code>tab_spanner_delim()</code>
</h3>
<p>Working with data that has hierarchical column names can be tedious when manually creating spanners. The new <code>~~.GT.tab_spanner_delim()</code> method automates this process by intelligently splitting column names based on a delimiter and creating the appropriate spanner structure.</p>
<p>Here&rsquo;s a practical example using the <code>towny</code> dataset, which contains population data for a collection of municipalities across multiple census years. Let&rsquo;s start by looking at the most populated cities and examining their column structure:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables.data</span> <span class="kn">import</span> <span class="n">towny</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="c1"># Create a smaller version of the `towny` dataset</span>
</span></span><span class="line"><span class="cl"><span class="n">towny_mini</span> <span class="o">=</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">from_pandas</span><span class="p">(</span><span class="n">towny</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">filter</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;csd_type&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;city&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="s2">&#34;population_2021&#34;</span><span class="p">,</span> <span class="n">descending</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">select</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;name&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">cs</span><span class="o">.</span><span class="n">starts_with</span><span class="p">(</span><span class="s2">&#34;population_&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">cs</span><span class="o">.</span><span class="n">starts_with</span><span class="p">(</span><span class="s2">&#34;density_&#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">head</span><span class="p">(</span><span class="mi">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></span><span class="line"><span class="cl"><span class="c1"># Let&#39;s look at the column names</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">towny_mini</span><span class="o">.</span><span class="n">columns</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><pre><code>['name', 'population_1996', 'population_2001', 'population_2006', 'population_2011', 'population_2016', 'population_2021', 'density_1996', 'density_2001', 'density_2006', 'density_2011', 'density_2016', 'density_2021']
</code></pre>
<p>Notice how the column names have a clear hierarchical structure with underscores as delimiters. Let&rsquo;s now create a table that takes advantage of this structure:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">towny_mini</span><span class="p">,</span> <span class="n">rowname_col</span><span class="o">=</span><span class="s2">&#34;name&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">tab_spanner_delim</span><span class="p">(</span><span class="n">delim</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="o">.</span><span class="n">fmt_integer</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">contains</span><span class="p">(</span><span class="s2">&#34;population&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_number</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">contains</span><span class="p">(</span><span class="s2">&#34;density&#34;</span><span class="p">),</span> <span class="n">decimals</span><span class="o">=</span><span class="mi">1</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="n">title</span><span class="o">=</span><span class="s2">&#34;Population and Density Trends from Census Data&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">opt_align_table_header</span><span class="p">(</span><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></code></pre></td></tr></table>
</div>
</div><div id="diagtcmpiq" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
#diagtcmpiq table {
          font-family: -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>#diagtcmpiq thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#diagtcmpiq p { margin: 0; padding: 0; }
#diagtcmpiq .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: 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; }
#diagtcmpiq .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#diagtcmpiq .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; }
#diagtcmpiq .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; }
#diagtcmpiq .gt_heading { background-color: #FFFFFF; text-align: left; 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; }
#diagtcmpiq .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#diagtcmpiq .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; }
#diagtcmpiq .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; }
#diagtcmpiq .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; }
#diagtcmpiq .gt_column_spanner_outer:first-child { padding-left: 0; }
#diagtcmpiq .gt_column_spanner_outer:last-child { padding-right: 0; }
#diagtcmpiq .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%; }
#diagtcmpiq .gt_spanner_row { border-bottom-style: hidden; }
#diagtcmpiq .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; }
#diagtcmpiq .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; }
#diagtcmpiq .gt_from_md&gt; :first-child { margin-top: 0; }
#diagtcmpiq .gt_from_md&gt; :last-child { margin-bottom: 0; }
#diagtcmpiq .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; }
#diagtcmpiq .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; }
#diagtcmpiq .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; }
#diagtcmpiq .gt_row_group_first td { border-top-width: 2px; }
#diagtcmpiq .gt_row_group_first th { border-top-width: 2px; }
#diagtcmpiq .gt_striped { color: #333333; background-color: #F4F4F4; }
#diagtcmpiq .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; }
#diagtcmpiq .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#diagtcmpiq .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#diagtcmpiq .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#diagtcmpiq .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; }
#diagtcmpiq .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#diagtcmpiq .gt_left { text-align: left; }
#diagtcmpiq .gt_center { text-align: center; }
#diagtcmpiq .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#diagtcmpiq .gt_font_normal { font-weight: normal; }
#diagtcmpiq .gt_font_bold { font-weight: bold; }
#diagtcmpiq .gt_font_italic { font-style: italic; }
#diagtcmpiq .gt_super { font-size: 65%; }
#diagtcmpiq .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#diagtcmpiq .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table class="gt_table" data-quarto-postprocess="true" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="13" class="gt_heading gt_title gt_font_normal">Population and Density Trends from Census Data</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="6" id="population" class="gt_center gt_columns_top_border gt_column_spanner_outer" data-quarto-table-cell-role="th" scope="colgroup"><span class="gt_column_spanner">population</span></th>
<th colspan="6" id="density" class="gt_center gt_columns_top_border gt_column_spanner_outer" data-quarto-table-cell-role="th" scope="colgroup"><span class="gt_column_spanner">density</span></th>
</tr>
<tr class="gt_col_headings">
<th id="population_1996" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">1996</th>
<th id="population_2001" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">2001</th>
<th id="population_2006" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">2006</th>
<th id="population_2011" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">2011</th>
<th id="population_2016" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">2016</th>
<th id="population_2021" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">2021</th>
<th id="density_1996" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">1996</th>
<th id="density_2001" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">2001</th>
<th id="density_2006" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">2006</th>
<th id="density_2011" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">2011</th>
<th id="density_2016" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">2016</th>
<th id="density_2021" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">2021</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_left gt_stub" data-quarto-table-cell-role="th">Toronto</td>
<td class="gt_row gt_right">2,385,421</td>
<td class="gt_row gt_right">2,481,494</td>
<td class="gt_row gt_right">2,503,281</td>
<td class="gt_row gt_right">2,615,060</td>
<td class="gt_row gt_right">2,731,571</td>
<td class="gt_row gt_right">2,794,356</td>
<td class="gt_row gt_right">3,779.8</td>
<td class="gt_row gt_right">3,932.0</td>
<td class="gt_row gt_right">3,966.5</td>
<td class="gt_row gt_right">4,143.6</td>
<td class="gt_row gt_right">4,328.3</td>
<td class="gt_row gt_right">4,427.8</td>
</tr>
<tr>
<td class="gt_row gt_left gt_stub" data-quarto-table-cell-role="th">Ottawa</td>
<td class="gt_row gt_right">721,136</td>
<td class="gt_row gt_right">774,072</td>
<td class="gt_row gt_right">812,129</td>
<td class="gt_row gt_right">883,391</td>
<td class="gt_row gt_right">934,243</td>
<td class="gt_row gt_right">1,017,449</td>
<td class="gt_row gt_right">258.6</td>
<td class="gt_row gt_right">277.6</td>
<td class="gt_row gt_right">291.3</td>
<td class="gt_row gt_right">316.8</td>
<td class="gt_row gt_right">335.1</td>
<td class="gt_row gt_right">364.9</td>
</tr>
<tr>
<td class="gt_row gt_left gt_stub" data-quarto-table-cell-role="th">Mississauga</td>
<td class="gt_row gt_right">544,382</td>
<td class="gt_row gt_right">612,925</td>
<td class="gt_row gt_right">668,599</td>
<td class="gt_row gt_right">713,443</td>
<td class="gt_row gt_right">721,599</td>
<td class="gt_row gt_right">717,961</td>
<td class="gt_row gt_right">1,859.6</td>
<td class="gt_row gt_right">2,093.8</td>
<td class="gt_row gt_right">2,283.9</td>
<td class="gt_row gt_right">2,437.1</td>
<td class="gt_row gt_right">2,465.0</td>
<td class="gt_row gt_right">2,452.6</td>
</tr>
<tr>
<td class="gt_row gt_left gt_stub" data-quarto-table-cell-role="th">Brampton</td>
<td class="gt_row gt_right">268,251</td>
<td class="gt_row gt_right">325,428</td>
<td class="gt_row gt_right">433,806</td>
<td class="gt_row gt_right">523,906</td>
<td class="gt_row gt_right">593,638</td>
<td class="gt_row gt_right">656,480</td>
<td class="gt_row gt_right">1,008.9</td>
<td class="gt_row gt_right">1,223.9</td>
<td class="gt_row gt_right">1,631.5</td>
<td class="gt_row gt_right">1,970.4</td>
<td class="gt_row gt_right">2,232.7</td>
<td class="gt_row gt_right">2,469.0</td>
</tr>
<tr>
<td class="gt_row gt_left gt_stub" data-quarto-table-cell-role="th">Hamilton</td>
<td class="gt_row gt_right">467,799</td>
<td class="gt_row gt_right">490,268</td>
<td class="gt_row gt_right">504,559</td>
<td class="gt_row gt_right">519,949</td>
<td class="gt_row gt_right">536,917</td>
<td class="gt_row gt_right">569,353</td>
<td class="gt_row gt_right">418.3</td>
<td class="gt_row gt_right">438.4</td>
<td class="gt_row gt_right">451.2</td>
<td class="gt_row gt_right">464.9</td>
<td class="gt_row gt_right">480.1</td>
<td class="gt_row gt_right">509.1</td>
</tr>
</tbody>
</table>
</div>
<p>The <code>~~.GT.tab_spanner_delim()</code> method recognizes the underscore delimiter and creates a hierarchical structure: <code>&quot;population&quot;</code> and <code>&quot;density&quot;</code> become top-level spanners, with the years (<code>1996</code>, <code>2001</code>, <code>2021</code>) as the final column labels. This creates a clean, organized appearance that clearly groups related metrics together. And, this one method can be used instead of a combination of <code>~~.GT.cols_label()</code> and <code>~~.GT.tab_spanner()</code> (which requires a separate invocation per spanner added).</p>
<h3 id="beautiful-boolean-formatting-with-fmt_tf">Beautiful boolean formatting with <code>fmt_tf()</code>
</h3>
<p>Boolean data is common in analytical tables, but raw <code>True</code>/<code>False</code> values can look unprofessional. The new <code>~~.GT.fmt_tf()</code> method provides elegant ways to display boolean data using symbols, words, or custom formatting.</p>
<p>Here&rsquo;s a simple example showing different <code>tf_style=</code> options:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span></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></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></span><span class="line"><span class="cl"><span class="c1"># Create a simple DF with boolean data</span>
</span></span><span class="line"><span class="cl"><span class="n">bool_df</span> <span class="o">=</span> <span class="n">pl</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;feature&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;Premium Sound&#34;</span><span class="p">,</span> <span class="s2">&#34;Leather Seats&#34;</span><span class="p">,</span> <span class="s2">&#34;Sunroof&#34;</span><span class="p">,</span> <span class="s2">&#34;Navigation&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;model_a&#34;</span><span class="p">:</span> <span class="p">[</span><span class="kc">True</span><span class="p">,</span> <span class="kc">False</span><span class="p">,</span> <span class="kc">True</span><span class="p">,</span> <span class="kc">True</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;model_b&#34;</span><span class="p">:</span> <span class="p">[</span><span class="kc">True</span><span class="p">,</span> <span class="kc">True</span><span class="p">,</span> <span class="kc">False</span><span class="p">,</span> <span class="kc">True</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;model_c&#34;</span><span class="p">:</span> <span class="p">[</span><span class="kc">False</span><span class="p">,</span> <span class="kc">True</span><span class="p">,</span> <span class="kc">True</span><span class="p">,</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></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">bool_df</span><span class="p">,</span> <span class="n">rowname_col</span><span class="o">=</span><span class="s2">&#34;feature&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_tf</span><span class="p">(</span><span class="n">tf_style</span><span class="o">=</span><span class="s2">&#34;check-mark&#34;</span><span class="p">,</span> <span class="n">colors</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;green&#34;</span><span class="p">,</span> <span class="s2">&#34;red&#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="n">title</span><span class="o">=</span><span class="s2">&#34;Car Features Comparison&#34;</span><span class="p">,</span> <span class="n">subtitle</span><span class="o">=</span><span class="s2">&#34;Using check-mark style&#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 id="xeyyyfbenb" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
#xeyyyfbenb table {
          font-family: -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>#xeyyyfbenb thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#xeyyyfbenb p { margin: 0; padding: 0; }
#xeyyyfbenb .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: 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; }
#xeyyyfbenb .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#xeyyyfbenb .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; }
#xeyyyfbenb .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; }
#xeyyyfbenb .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; }
#xeyyyfbenb .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#xeyyyfbenb .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; }
#xeyyyfbenb .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; }
#xeyyyfbenb .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; }
#xeyyyfbenb .gt_column_spanner_outer:first-child { padding-left: 0; }
#xeyyyfbenb .gt_column_spanner_outer:last-child { padding-right: 0; }
#xeyyyfbenb .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%; }
#xeyyyfbenb .gt_spanner_row { border-bottom-style: hidden; }
#xeyyyfbenb .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; }
#xeyyyfbenb .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; }
#xeyyyfbenb .gt_from_md&gt; :first-child { margin-top: 0; }
#xeyyyfbenb .gt_from_md&gt; :last-child { margin-bottom: 0; }
#xeyyyfbenb .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; }
#xeyyyfbenb .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; }
#xeyyyfbenb .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; }
#xeyyyfbenb .gt_row_group_first td { border-top-width: 2px; }
#xeyyyfbenb .gt_row_group_first th { border-top-width: 2px; }
#xeyyyfbenb .gt_striped { color: #333333; background-color: #F4F4F4; }
#xeyyyfbenb .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; }
#xeyyyfbenb .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#xeyyyfbenb .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#xeyyyfbenb .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#xeyyyfbenb .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; }
#xeyyyfbenb .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#xeyyyfbenb .gt_left { text-align: left; }
#xeyyyfbenb .gt_center { text-align: center; }
#xeyyyfbenb .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#xeyyyfbenb .gt_font_normal { font-weight: normal; }
#xeyyyfbenb .gt_font_bold { font-weight: bold; }
#xeyyyfbenb .gt_font_italic { font-style: italic; }
#xeyyyfbenb .gt_super { font-size: 65%; }
#xeyyyfbenb .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#xeyyyfbenb .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table class="gt_table" data-quarto-postprocess="true" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="4" class="gt_heading gt_title gt_font_normal">Car Features Comparison</th>
</tr>
<tr class="gt_heading">
<th colspan="4" class="gt_heading gt_subtitle gt_font_normal gt_bottom_border">Using check-mark style</th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col"></th>
<th id="model_a" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" scope="col">model_a</th>
<th id="model_b" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" scope="col">model_b</th>
<th id="model_c" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" scope="col">model_c</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_left gt_stub" data-quarto-table-cell-role="th">Premium Sound</td>
<td class="gt_row gt_center"><span style="color:green">✔</span></td>
<td class="gt_row gt_center"><span style="color:green">✔</span></td>
<td class="gt_row gt_center"><span style="color:red">✘</span></td>
</tr>
<tr>
<td class="gt_row gt_left gt_stub" data-quarto-table-cell-role="th">Leather Seats</td>
<td class="gt_row gt_center"><span style="color:red">✘</span></td>
<td class="gt_row gt_center"><span style="color:green">✔</span></td>
<td class="gt_row gt_center"><span style="color:green">✔</span></td>
</tr>
<tr>
<td class="gt_row gt_left gt_stub" data-quarto-table-cell-role="th">Sunroof</td>
<td class="gt_row gt_center"><span style="color:green">✔</span></td>
<td class="gt_row gt_center"><span style="color:red">✘</span></td>
<td class="gt_row gt_center"><span style="color:green">✔</span></td>
</tr>
<tr>
<td class="gt_row gt_left gt_stub" data-quarto-table-cell-role="th">Navigation</td>
<td class="gt_row gt_center"><span style="color:green">✔</span></td>
<td class="gt_row gt_center"><span style="color:green">✔</span></td>
<td class="gt_row gt_center"><span style="color:red">✘</span></td>
</tr>
</tbody>
</table>
</div>
<p>You can also use different symbols and colors for a more distinctive look:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">bool_df</span><span class="p">,</span> <span class="n">rowname_col</span><span class="o">=</span><span class="s2">&#34;feature&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_tf</span><span class="p">(</span><span class="n">tf_style</span><span class="o">=</span><span class="s2">&#34;circles&#34;</span><span class="p">,</span> <span class="n">colors</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;#4CAF50&#34;</span><span class="p">,</span> <span class="s2">&#34;#F44336&#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="n">title</span><span class="o">=</span><span class="s2">&#34;Car Features Comparison&#34;</span><span class="p">,</span> <span class="n">subtitle</span><span class="o">=</span><span class="s2">&#34;Using circles style&#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 id="imxvwavyav" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
#imxvwavyav table {
          font-family: -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>#imxvwavyav thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#imxvwavyav p { margin: 0; padding: 0; }
#imxvwavyav .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: 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; }
#imxvwavyav .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#imxvwavyav .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; }
#imxvwavyav .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; }
#imxvwavyav .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; }
#imxvwavyav .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#imxvwavyav .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; }
#imxvwavyav .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; }
#imxvwavyav .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; }
#imxvwavyav .gt_column_spanner_outer:first-child { padding-left: 0; }
#imxvwavyav .gt_column_spanner_outer:last-child { padding-right: 0; }
#imxvwavyav .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%; }
#imxvwavyav .gt_spanner_row { border-bottom-style: hidden; }
#imxvwavyav .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; }
#imxvwavyav .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; }
#imxvwavyav .gt_from_md&gt; :first-child { margin-top: 0; }
#imxvwavyav .gt_from_md&gt; :last-child { margin-bottom: 0; }
#imxvwavyav .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; }
#imxvwavyav .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; }
#imxvwavyav .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; }
#imxvwavyav .gt_row_group_first td { border-top-width: 2px; }
#imxvwavyav .gt_row_group_first th { border-top-width: 2px; }
#imxvwavyav .gt_striped { color: #333333; background-color: #F4F4F4; }
#imxvwavyav .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; }
#imxvwavyav .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#imxvwavyav .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#imxvwavyav .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#imxvwavyav .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; }
#imxvwavyav .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#imxvwavyav .gt_left { text-align: left; }
#imxvwavyav .gt_center { text-align: center; }
#imxvwavyav .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#imxvwavyav .gt_font_normal { font-weight: normal; }
#imxvwavyav .gt_font_bold { font-weight: bold; }
#imxvwavyav .gt_font_italic { font-style: italic; }
#imxvwavyav .gt_super { font-size: 65%; }
#imxvwavyav .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#imxvwavyav .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table class="gt_table" data-quarto-postprocess="true" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="4" class="gt_heading gt_title gt_font_normal">Car Features Comparison</th>
</tr>
<tr class="gt_heading">
<th colspan="4" class="gt_heading gt_subtitle gt_font_normal gt_bottom_border">Using circles style</th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col"></th>
<th id="model_a" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" scope="col">model_a</th>
<th id="model_b" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" scope="col">model_b</th>
<th id="model_c" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" scope="col">model_c</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_left gt_stub" data-quarto-table-cell-role="th">Premium Sound</td>
<td class="gt_row gt_center"><span style="color:#4CAF50">●</span></td>
<td class="gt_row gt_center"><span style="color:#4CAF50">●</span></td>
<td class="gt_row gt_center"><span style="color:#F44336">⭘</span></td>
</tr>
<tr>
<td class="gt_row gt_left gt_stub" data-quarto-table-cell-role="th">Leather Seats</td>
<td class="gt_row gt_center"><span style="color:#F44336">⭘</span></td>
<td class="gt_row gt_center"><span style="color:#4CAF50">●</span></td>
<td class="gt_row gt_center"><span style="color:#4CAF50">●</span></td>
</tr>
<tr>
<td class="gt_row gt_left gt_stub" data-quarto-table-cell-role="th">Sunroof</td>
<td class="gt_row gt_center"><span style="color:#4CAF50">●</span></td>
<td class="gt_row gt_center"><span style="color:#F44336">⭘</span></td>
<td class="gt_row gt_center"><span style="color:#4CAF50">●</span></td>
</tr>
<tr>
<td class="gt_row gt_left gt_stub" data-quarto-table-cell-role="th">Navigation</td>
<td class="gt_row gt_center"><span style="color:#4CAF50">●</span></td>
<td class="gt_row gt_center"><span style="color:#4CAF50">●</span></td>
<td class="gt_row gt_center"><span style="color:#F44336">⭘</span></td>
</tr>
</tbody>
</table>
</div>
<p>The <code>~~.GT.fmt_tf()</code> method transforms boolean values into visually appealing symbols that make it easy to quickly scan and compare data across rows and columns.</p>
<h3 id="rotating-column-labels-with-cols_label_rotate">Rotating column labels with <code>cols_label_rotate()</code>
</h3>
<p>When dealing with many columns or long column names, horizontal space becomes precious. The <code>~~.GT.cols_label_rotate()</code> method solves this by rotating column labels vertically, allowing for more compact table layouts.</p>
<p>Here&rsquo;s an example where we use the <code>gtcars</code> dataset to create a table which communicates a feature matrix:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><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></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">style</span><span class="p">,</span> <span class="n">loc</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables.data</span> <span class="kn">import</span> <span class="n">gtcars</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="c1"># Manipulate dataset to create a feature comparison table</span>
</span></span><span class="line"><span class="cl"><span class="n">gtcars_mini</span> <span class="o">=</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">from_pandas</span><span class="p">(</span><span class="n">gtcars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">filter</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;year&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="mi">2017</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">filter</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;ctry_origin&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">is_in</span><span class="p">([</span><span class="s2">&#34;Germany&#34;</span><span class="p">,</span> <span class="s2">&#34;Italy&#34;</span><span class="p">,</span> <span class="s2">&#34;United Kingdom&#34;</span><span class="p">]))</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">with_columns</span><span class="p">([</span>
</span></span><span class="line"><span class="cl">        <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;hp&#34;</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">500</span><span class="p">)</span><span class="o">.</span><span class="n">alias</span><span class="p">(</span><span class="s2">&#34;High Power&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <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;mpg_h&#34;</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">25</span><span class="p">)</span><span class="o">.</span><span class="n">alias</span><span class="p">(</span><span class="s2">&#34;Fuel Efficient&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <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;drivetrain&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;awd&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">alias</span><span class="p">(</span><span class="s2">&#34;All Wheel Drive&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <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;msrp&#34;</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">100000</span><span class="p">)</span><span class="o">.</span><span class="n">alias</span><span class="p">(</span><span class="s2">&#34;Premium Price&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <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;trsmn&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">str</span><span class="o">.</span><span class="n">contains</span><span class="p">(</span><span class="s2">&#34;manual&#34;</span><span class="p">))</span><span class="o">.</span><span class="n">alias</span><span class="p">(</span><span class="s2">&#34;Manual Transmission&#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">select</span><span class="p">([</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;mfr&#34;</span><span class="p">,</span> <span class="s2">&#34;model&#34;</span><span class="p">,</span> <span class="s2">&#34;trim&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;High Power&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Fuel Efficient&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;All Wheel Drive&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Premium Price&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Manual Transmission&#34;</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">head</span><span class="p">(</span><span class="mi">10</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="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">gtcars_mini</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_tf</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">by_dtype</span><span class="p">(</span><span class="n">pl</span><span class="o">.</span><span class="n">Boolean</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">tf_style</span><span class="o">=</span><span class="s2">&#34;check-mark&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">colors</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;#2E8B57&#34;</span><span class="p">,</span> <span class="s2">&#34;#DC143C&#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_label_rotate</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">by_dtype</span><span class="p">(</span><span class="n">pl</span><span class="o">.</span><span class="n">Boolean</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="nb">dir</span><span class="o">=</span><span class="s2">&#34;sideways-lr&#34;</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_header</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">title</span><span class="o">=</span><span class="s2">&#34;European Luxury Cars Feature Matrix&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">subtitle</span><span class="o">=</span><span class="s2">&#34;2017 Models with Performance &amp; Luxury Features&#34;</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_stylize</span><span class="p">(</span><span class="n">style</span><span class="o">=</span><span class="mi">1</span><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">text</span><span class="p">(</span><span class="n">size</span><span class="o">=</span><span class="s2">&#34;11px&#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="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div id="wxhgwvpraw" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
#wxhgwvpraw table {
          font-family: -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>#wxhgwvpraw thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#wxhgwvpraw p { margin: 0; padding: 0; }
#wxhgwvpraw .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: 2px; border-top-color: #004D80; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #004D80; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; }
#wxhgwvpraw .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#wxhgwvpraw .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; }
#wxhgwvpraw .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; }
#wxhgwvpraw .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; }
#wxhgwvpraw .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #0076BA; }
#wxhgwvpraw .gt_col_headings { border-top-style: solid; border-top-width: 2px; border-top-color: #0076BA; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #0076BA; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; }
#wxhgwvpraw .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; }
#wxhgwvpraw .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; }
#wxhgwvpraw .gt_column_spanner_outer:first-child { padding-left: 0; }
#wxhgwvpraw .gt_column_spanner_outer:last-child { padding-right: 0; }
#wxhgwvpraw .gt_column_spanner { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #0076BA; vertical-align: bottom; padding-top: 5px; padding-bottom: 5px; overflow-x: hidden; display: inline-block; width: 100%; }
#wxhgwvpraw .gt_spanner_row { border-bottom-style: hidden; }
#wxhgwvpraw .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: #0076BA; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #0076BA; 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; }
#wxhgwvpraw .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: #0076BA; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #0076BA; vertical-align: middle; }
#wxhgwvpraw .gt_from_md&gt; :first-child { margin-top: 0; }
#wxhgwvpraw .gt_from_md&gt; :last-child { margin-bottom: 0; }
#wxhgwvpraw .gt_row { padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: none; border-top-width: 1px; border-top-color: #89D3FE; border-left-style: none; border-left-width: 1px; border-left-color: #89D3FE; border-right-style: none; border-right-width: 1px; border-right-color: #89D3FE; vertical-align: middle; overflow-x: hidden; }
#wxhgwvpraw .gt_stub { color: #FFFFFF; background-color: #0076BA; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #0076BA; padding-left: 5px; padding-right: 5px; }
#wxhgwvpraw .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; }
#wxhgwvpraw .gt_row_group_first td { border-top-width: 2px; }
#wxhgwvpraw .gt_row_group_first th { border-top-width: 2px; }
#wxhgwvpraw .gt_striped { color: #333333; background-color: #F4F4F4; }
#wxhgwvpraw .gt_table_body { border-top-style: solid; border-top-width: 2px; border-top-color: #0076BA; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #0076BA; }
#wxhgwvpraw .gt_grand_summary_row { color: #333333; background-color: #89D3FE; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#wxhgwvpraw .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#wxhgwvpraw .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#wxhgwvpraw .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; }
#wxhgwvpraw .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#wxhgwvpraw .gt_left { text-align: left; }
#wxhgwvpraw .gt_center { text-align: center; }
#wxhgwvpraw .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#wxhgwvpraw .gt_font_normal { font-weight: normal; }
#wxhgwvpraw .gt_font_bold { font-weight: bold; }
#wxhgwvpraw .gt_font_italic { font-style: italic; }
#wxhgwvpraw .gt_super { font-size: 65%; }
#wxhgwvpraw .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#wxhgwvpraw .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table class="gt_table" data-quarto-postprocess="true" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="8" class="gt_heading gt_title gt_font_normal">European Luxury Cars Feature Matrix</th>
</tr>
<tr class="gt_heading">
<th colspan="8" class="gt_heading gt_subtitle gt_font_normal gt_bottom_border">2017 Models with Performance &amp; Luxury Features</th>
</tr>
<tr class="gt_col_headings">
<th id="mfr" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">mfr</th>
<th id="model" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">model</th>
<th id="trim" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">trim</th>
<th id="High-Power" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" style="text-align: left; writing-mode: sideways-lr; vertical-align: middle; padding: 8px 0px;" scope="col">High Power</th>
<th id="Fuel-Efficient" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" style="text-align: left; writing-mode: sideways-lr; vertical-align: middle; padding: 8px 0px;" scope="col">Fuel Efficient</th>
<th id="All-Wheel-Drive" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" style="text-align: left; writing-mode: sideways-lr; vertical-align: middle; padding: 8px 0px;" scope="col">All Wheel Drive</th>
<th id="Premium-Price" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" style="text-align: left; writing-mode: sideways-lr; vertical-align: middle; padding: 8px 0px;" scope="col">Premium Price</th>
<th id="Manual-Transmission" class="gt_col_heading gt_columns_bottom_border gt_center" data-quarto-table-cell-role="th" style="text-align: left; writing-mode: sideways-lr; vertical-align: middle; padding: 8px 0px;" scope="col">Manual Transmission</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_left" style="font-size: 11px">Ferrari</td>
<td class="gt_row gt_left" style="font-size: 11px">GTC4Lusso</td>
<td class="gt_row gt_left" style="font-size: 11px">Base Coupe</td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#2E8B57">✔</span></td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#2E8B57">✔</span></td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#2E8B57">✔</span></td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
</tr>
<tr>
<td class="gt_row gt_left gt_striped" style="font-size: 11px">Aston Martin</td>
<td class="gt_row gt_left gt_striped" style="font-size: 11px">DB11</td>
<td class="gt_row gt_left gt_striped" style="font-size: 11px">Base Coupe</td>
<td class="gt_row gt_center gt_striped" style="font-size: 11px"><span style="color:#2E8B57">✔</span></td>
<td class="gt_row gt_center gt_striped" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
<td class="gt_row gt_center gt_striped" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
<td class="gt_row gt_center gt_striped" style="font-size: 11px"><span style="color:#2E8B57">✔</span></td>
<td class="gt_row gt_center gt_striped" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
</tr>
<tr>
<td class="gt_row gt_left" style="font-size: 11px">Lotus</td>
<td class="gt_row gt_left" style="font-size: 11px">Evora</td>
<td class="gt_row gt_left" style="font-size: 11px">2+2 Coupe</td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
</tr>
<tr>
<td class="gt_row gt_left gt_striped" style="font-size: 11px">Porsche</td>
<td class="gt_row gt_left gt_striped" style="font-size: 11px">718 Boxster</td>
<td class="gt_row gt_left gt_striped" style="font-size: 11px">Base Convertible</td>
<td class="gt_row gt_center gt_striped" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
<td class="gt_row gt_center gt_striped" style="font-size: 11px"><span style="color:#2E8B57">✔</span></td>
<td class="gt_row gt_center gt_striped" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
<td class="gt_row gt_center gt_striped" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
<td class="gt_row gt_center gt_striped" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
</tr>
<tr>
<td class="gt_row gt_left" style="font-size: 11px">Porsche</td>
<td class="gt_row gt_left" style="font-size: 11px">718 Cayman</td>
<td class="gt_row gt_left" style="font-size: 11px">Base Coupe</td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#2E8B57">✔</span></td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
<td class="gt_row gt_center" style="font-size: 11px"><span style="color:#DC143C">✘</span></td>
</tr>
</tbody>
</table>
</div>
<p>This example demonstrates how both the <code>~~.GT.fmt_tf()</code> and <code>~~.GT.cols_label_rotate()</code> methods can work well together. The boolean columns use checkmarks (✓/✗) with custom <code>colors=</code>, while the rotated labels save horizontal space in this dense feature matrix. The combination allows you to put more information into a compact and still readable format.</p>
<h3 id="enhanced-datetime-formatting-with-fmt_datetime">Enhanced datetime formatting with <code>fmt_datetime()</code>
</h3>
<p>The <code>~~.GT.fmt_datetime()</code> method now supports custom format strings through the new <code>format_str=</code> parameter, giving you complete control over how datetime values appear in your tables.</p>
<p>Here&rsquo;s an example using the included <code>gibraltar</code> weather dataset:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><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></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></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">great_tables.data</span> <span class="kn">import</span> <span class="n">gibraltar</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></span><span class="line"><span class="cl"><span class="c1"># Prepare the meteorological data</span>
</span></span><span class="line"><span class="cl"><span class="n">gibraltar_mini</span> <span class="o">=</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">from_pandas</span><span class="p">(</span><span class="n">gibraltar</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">with_columns</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">pl</span><span class="o">.</span><span class="n">concat_str</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;date&#34;</span><span class="p">),</span> <span class="n">pl</span><span class="o">.</span><span class="n">lit</span><span class="p">(</span><span class="s2">&#34; &#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;time&#34;</span><span class="p">)])</span>
</span></span><span class="line"><span class="cl">            <span class="o">.</span><span class="n">str</span><span class="o">.</span><span class="n">strptime</span><span class="p">(</span><span class="n">pl</span><span class="o">.</span><span class="n">Datetime</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s2">&#34;%Y-%m-</span><span class="si">%d</span><span class="s2"> %H:%M&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="o">.</span><span class="n">alias</span><span class="p">(</span><span class="s2">&#34;datetime&#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="o">.</span><span class="n">filter</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;datetime&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">dt</span><span class="o">.</span><span class="n">hour</span><span class="p">()</span><span class="o">.</span><span class="n">is_in</span><span class="p">([</span><span class="mi">6</span><span class="p">,</span> <span class="mi">12</span><span class="p">,</span> <span class="mi">18</span><span class="p">]))</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">select</span><span class="p">([</span><span class="s2">&#34;datetime&#34;</span><span class="p">,</span> <span class="s2">&#34;temp&#34;</span><span class="p">,</span> <span class="s2">&#34;humidity&#34;</span><span class="p">,</span> <span class="s2">&#34;condition&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="s2">&#34;datetime&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">head</span><span class="p">(</span><span class="mi">10</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="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">GT</span><span class="p">(</span><span class="n">gibraltar_mini</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_datetime</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">columns</span><span class="o">=</span><span class="s2">&#34;datetime&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">format_str</span><span class="o">=</span><span class="s2">&#34;%b </span><span class="si">%d</span><span class="s2"> %Y (</span><span class="si">%a</span><span class="s2">) - %I:%M %p&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_number</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;temp&#34;</span><span class="p">,</span> <span class="n">decimals</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">pattern</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">{x}</span><span class="s2">°C&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">fmt_percent</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;humidity&#34;</span><span class="p">,</span> <span class="n">scale_values</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">decimals</span><span class="o">=</span><span class="mi">0</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">datetime</span><span class="o">=</span><span class="s2">&#34;Time&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">temp</span><span class="o">=</span><span class="s2">&#34;Temperature&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">humidity</span><span class="o">=</span><span class="s2">&#34;Humidity&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">condition</span><span class="o">=</span><span class="s2">&#34;Conditions&#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_header</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">title</span><span class="o">=</span><span class="s2">&#34;Gibraltar Temperature and Humidity Conditions&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">subtitle</span><span class="o">=</span><span class="s2">&#34;Morning, Noon, and Evening Readings&#34;</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_stylize</span><span class="p">(</span><span class="n">style</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s2">&#34;cyan&#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 id="tndrcevexg" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
#tndrcevexg table {
          font-family: -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>#tndrcevexg thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#tndrcevexg p { margin: 0; padding: 0; }
#tndrcevexg .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: 2px; border-top-color: #016763; border-right-style: none; border-right-width: 2px; border-right-color: #D3D3D3; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #016763; border-left-style: none; border-left-width: 2px; border-left-color: #D3D3D3; }
#tndrcevexg .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#tndrcevexg .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; }
#tndrcevexg .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; }
#tndrcevexg .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; }
#tndrcevexg .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #01837B; }
#tndrcevexg .gt_col_headings { border-top-style: solid; border-top-width: 2px; border-top-color: #01837B; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #01837B; border-left-style: none; border-left-width: 1px; border-left-color: #D3D3D3; border-right-style: none; border-right-width: 1px; border-right-color: #D3D3D3; }
#tndrcevexg .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; }
#tndrcevexg .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; }
#tndrcevexg .gt_column_spanner_outer:first-child { padding-left: 0; }
#tndrcevexg .gt_column_spanner_outer:last-child { padding-right: 0; }
#tndrcevexg .gt_column_spanner { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #01837B; vertical-align: bottom; padding-top: 5px; padding-bottom: 5px; overflow-x: hidden; display: inline-block; width: 100%; }
#tndrcevexg .gt_spanner_row { border-bottom-style: hidden; }
#tndrcevexg .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: #01837B; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #01837B; 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; }
#tndrcevexg .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: #01837B; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #01837B; vertical-align: middle; }
#tndrcevexg .gt_from_md&gt; :first-child { margin-top: 0; }
#tndrcevexg .gt_from_md&gt; :last-child { margin-bottom: 0; }
#tndrcevexg .gt_row { padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; margin: 10px; border-top-style: none; border-top-width: 1px; border-top-color: #A5FEF2; border-left-style: none; border-left-width: 1px; border-left-color: #A5FEF2; border-right-style: none; border-right-width: 1px; border-right-color: #A5FEF2; vertical-align: middle; overflow-x: hidden; }
#tndrcevexg .gt_stub { color: #FFFFFF; background-color: #01837B; font-size: 100%; font-weight: initial; text-transform: inherit; border-right-style: solid; border-right-width: 2px; border-right-color: #01837B; padding-left: 5px; padding-right: 5px; }
#tndrcevexg .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; }
#tndrcevexg .gt_row_group_first td { border-top-width: 2px; }
#tndrcevexg .gt_row_group_first th { border-top-width: 2px; }
#tndrcevexg .gt_striped { color: #333333; background-color: #F4F4F4; }
#tndrcevexg .gt_table_body { border-top-style: solid; border-top-width: 2px; border-top-color: #01837B; border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #01837B; }
#tndrcevexg .gt_grand_summary_row { color: #333333; background-color: #A5FEF2; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#tndrcevexg .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#tndrcevexg .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#tndrcevexg .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; }
#tndrcevexg .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#tndrcevexg .gt_left { text-align: left; }
#tndrcevexg .gt_center { text-align: center; }
#tndrcevexg .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#tndrcevexg .gt_font_normal { font-weight: normal; }
#tndrcevexg .gt_font_bold { font-weight: bold; }
#tndrcevexg .gt_font_italic { font-style: italic; }
#tndrcevexg .gt_super { font-size: 65%; }
#tndrcevexg .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#tndrcevexg .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table class="gt_table" data-quarto-postprocess="true" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="4" class="gt_heading gt_title gt_font_normal">Gibraltar Temperature and Humidity Conditions</th>
</tr>
<tr class="gt_heading">
<th colspan="4" class="gt_heading gt_subtitle gt_font_normal gt_bottom_border">Morning, Noon, and Evening Readings</th>
</tr>
<tr class="gt_col_headings">
<th id="datetime" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">Time</th>
<th id="temp" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">Temperature</th>
<th id="humidity" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">Humidity</th>
<th id="condition" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">Conditions</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_right">May 01 2023 (Mon) - 06:50 AM</td>
<td class="gt_row gt_right">17.2°C</td>
<td class="gt_row gt_right">1%</td>
<td class="gt_row gt_left">Fair</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped">May 01 2023 (Mon) - 12:20 PM</td>
<td class="gt_row gt_right gt_striped">22.2°C</td>
<td class="gt_row gt_right gt_striped">1%</td>
<td class="gt_row gt_left gt_striped">Fair</td>
</tr>
<tr>
<td class="gt_row gt_right">May 01 2023 (Mon) - 12:50 PM</td>
<td class="gt_row gt_right">22.2°C</td>
<td class="gt_row gt_right">1%</td>
<td class="gt_row gt_left">Fair</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped">May 01 2023 (Mon) - 06:20 PM</td>
<td class="gt_row gt_right gt_striped">20.0°C</td>
<td class="gt_row gt_right gt_striped">1%</td>
<td class="gt_row gt_left gt_striped">Fair</td>
</tr>
<tr>
<td class="gt_row gt_right">May 01 2023 (Mon) - 06:50 PM</td>
<td class="gt_row gt_right">20.0°C</td>
<td class="gt_row gt_right">1%</td>
<td class="gt_row gt_left">Fair</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped">May 02 2023 (Tue) - 06:50 AM</td>
<td class="gt_row gt_right gt_striped">17.8°C</td>
<td class="gt_row gt_right gt_striped">1%</td>
<td class="gt_row gt_left gt_striped">Mostly Cloudy</td>
</tr>
<tr>
<td class="gt_row gt_right">May 02 2023 (Tue) - 12:20 PM</td>
<td class="gt_row gt_right">18.9°C</td>
<td class="gt_row gt_right">1%</td>
<td class="gt_row gt_left">Mostly Cloudy</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped">May 02 2023 (Tue) - 12:50 PM</td>
<td class="gt_row gt_right gt_striped">20.0°C</td>
<td class="gt_row gt_right gt_striped">1%</td>
<td class="gt_row gt_left gt_striped">Mostly Cloudy</td>
</tr>
<tr>
<td class="gt_row gt_right">May 02 2023 (Tue) - 06:20 PM</td>
<td class="gt_row gt_right">22.2°C</td>
<td class="gt_row gt_right">1%</td>
<td class="gt_row gt_left">Fair</td>
</tr>
<tr>
<td class="gt_row gt_right gt_striped">May 02 2023 (Tue) - 06:50 PM</td>
<td class="gt_row gt_right gt_striped">22.2°C</td>
<td class="gt_row gt_right gt_striped">1%</td>
<td class="gt_row gt_left gt_striped">Fair</td>
</tr>
</tbody>
</table>
</div>
<p>The custom datetime formatting string in <code>format_str=&quot;%b %d %Y (%a) - %I:%M %p&quot;</code> creates a readable datetime format that&rsquo;s perfect for weather reporting, showing the day of week, month, day, year, and the time in 12-hour format.</p>
<h3 id="acknowledgements-and-whats-next">Acknowledgements and what&rsquo;s next
</h3>
<p>We&rsquo;re grateful to all the contributors who made this release possible. These new features represent significant improvements for creating space-efficient tables while also maximizing visual appeal.</p>
<p>The combination of these features lets you now create complex, professional tables with hierarchical column structures, boolean indicators, space-saving labels, and nicely formatted datetime displays.</p>
<p>We&rsquo;re always happy to get feedback and hear about how you&rsquo;re using Great Tables:</p>
<ol>
<li><a href="https://github.com/posit-dev/great-tables/issues" target="_blank" rel="noopener">GitHub Issues</a>
</li>
<li><a href="https://github.com/posit-dev/great-tables/discussions" target="_blank" rel="noopener">GitHub Discussions</a>
</li>
<li><a href="https://discord.com/invite/Ux7nrcXHVV" target="_blank" rel="noopener">Discord</a>
</li>
</ol>
<p>Keep building those beautiful tables!</p>
]]></description>
    </item>
    <item>
      <title>Posit at SciPy 2025</title>
      <link>https://posit-open-source.netlify.app/blog/shiny/shiny-at-scipy-2025/</link>
      <pubDate>Mon, 14 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/shiny/shiny-at-scipy-2025/</guid>
      <dc:creator>Shiny Team</dc:creator><description><![CDATA[<p><strong>The Shiny Team was at SciPy 2025!</strong>
If you missed us at <a href="https://www.scipy2025.scipy.org/" target="_blank" rel="noopener">SciPy 2025</a>
 here&rsquo;s what we were up to.</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/shiny/shiny-at-scipy-2025/scipy-posit-shiny-team.jpg" alt="posit shiny team at our scipy booth" />
<figcaption aria-hidden="true">posit shiny team at our scipy booth</figcaption>
</figure>
<p>We just got back from a week at <a href="https://www.scipy2025.scipy.org/" target="_blank" rel="noopener">SciPy 2025</a>
 and had a great time meeting and talking with folks at the conference.
If you didn&rsquo;t get a chance to say hi to us,
you can also find us online on <a href="https://github.com/posit-dev/py-shiny" target="_blank" rel="noopener"><i></i> GitHub</a>
 or
<a href="https://discord.com/invite/yMGCamUMnS" target="_blank" rel="noopener"><i></i> Discord</a>
.
You can also learn more about Posit at <a href="https://posit.co" target="_blank" rel="noopener"><i></i> posit.co</a>
.</p>
<p>If you missed a tutorial or talk we have all the information below.
You&rsquo;ll have to wait a bit for the SciPy folks to get all the tutorials and talks uploaded to <a href="https://www.youtube.com/@SciPy-Conf/videos" target="_blank" rel="noopener"><i></i> Youtube</a>
.</p>
<h2 id="talks-by-posit-folks">Talks by Posit Folks
</h2>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/shiny/shiny-at-scipy-2025/scipy-og.jpg" length="81799" type="image/jpeg" />
    </item>
    <item>
      <title>Adding Plots to Great Tables</title>
      <link>https://posit-open-source.netlify.app/blog/great-tables/plots-in-tables/</link>
      <pubDate>Thu, 03 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/great-tables/plots-in-tables/</guid>
      <dc:creator>Jules Walzer-Goldfeld</dc:creator>
      <dc:creator>Michael Chow</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>While working on <a href="https://posit-dev.github.io/gt-extras/articles/intro.html" target="_blank" rel="noopener"><strong>gt-extras</strong></a>
, I&rsquo;ve been exploring how to add small plots to Great Tables. These can go by many names, like spark lines, nanoplots, and so on. In this post, I&rsquo;ll look at three approaches I tried: adding plots with <a href="https://plotnine.org/" target="_blank" rel="noopener"><code>plotnine</code></a>
, <a href="https://github.com/orsinium-labs/svg.py" target="_blank" rel="noopener"><code>svg.py</code></a>
, or adding HTML directly. In the first two cases, the plots are SVGs, while the latter entails a collection of composed HTML div elements.</p>
<p>Here are the pieces I&rsquo;ll cover:</p>
<ul>
<li><strong>svg.py</strong>: creating your own tiny chart directly for a row.</li>
<li><strong>direct HTML</strong>: adding HTML divs directly.</li>
<li><strong>plotnine</strong>: adding a full, stripped-down chart to a row.</li>
</ul>
<p>In the end, it&rsquo;s often simplest to use <code>svg.py</code>, since you can create basic charts with minimal overhead. Building elements with HTML has even <em>less</em> overhead, but it is also slightly less user-friendly. At the other end of the spectrum, as your charts become more complex, using existing packages like the more exhaustive <code>plotnine</code> is a good alternative.</p>
<div id="fpuvtcetir" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
#fpuvtcetir table {
          font-family: -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>#fpuvtcetir thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#fpuvtcetir p { margin: 0; padding: 0; }
#fpuvtcetir .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: 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; }
#fpuvtcetir .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#fpuvtcetir .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; }
#fpuvtcetir .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; }
#fpuvtcetir .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; }
#fpuvtcetir .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#fpuvtcetir .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; }
#fpuvtcetir .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; }
#fpuvtcetir .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; }
#fpuvtcetir .gt_column_spanner_outer:first-child { padding-left: 0; }
#fpuvtcetir .gt_column_spanner_outer:last-child { padding-right: 0; }
#fpuvtcetir .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%; }
#fpuvtcetir .gt_spanner_row { border-bottom-style: hidden; }
#fpuvtcetir .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; }
#fpuvtcetir .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; }
#fpuvtcetir .gt_from_md&gt; :first-child { margin-top: 0; }
#fpuvtcetir .gt_from_md&gt; :last-child { margin-bottom: 0; }
#fpuvtcetir .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; }
#fpuvtcetir .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; }
#fpuvtcetir .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; }
#fpuvtcetir .gt_row_group_first td { border-top-width: 2px; }
#fpuvtcetir .gt_row_group_first th { border-top-width: 2px; }
#fpuvtcetir .gt_striped { color: #333333; background-color: #F4F4F4; }
#fpuvtcetir .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; }
#fpuvtcetir .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#fpuvtcetir .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#fpuvtcetir .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#fpuvtcetir .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; }
#fpuvtcetir .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#fpuvtcetir .gt_left { text-align: left; }
#fpuvtcetir .gt_center { text-align: center; }
#fpuvtcetir .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#fpuvtcetir .gt_font_normal { font-weight: normal; }
#fpuvtcetir .gt_font_bold { font-weight: bold; }
#fpuvtcetir .gt_font_italic { font-style: italic; }
#fpuvtcetir .gt_super { font-size: 65%; }
#fpuvtcetir .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#fpuvtcetir .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table>
  <thead>
      <tr>
          <th>Animal</th>
          <th>Legs</th>
          <th>Plot</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ostrich</td>
          <td>2</td>
          <td>2</td>
      </tr>
      <tr>
          <td>Spider</td>
          <td>8</td>
          <td>8</td>
      </tr>
      <tr>
          <td>Lion</td>
          <td>4</td>
          <td>4</td>
      </tr>
  </tbody>
</table>
</div>
<p>Here is the final result:</p>
<details class="code-fold">
<summary>Code</summary>
<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></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 class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">svg</span> <span class="kn">import</span> <span class="n">SVG</span><span class="p">,</span> <span class="n">Rect</span><span class="p">,</span> <span class="n">Line</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">pl</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">({</span><span class="s2">&#34;Animal&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;Ostrich&#34;</span><span class="p">,</span> <span class="s2">&#34;Spider&#34;</span><span class="p">,</span> <span class="s2">&#34;Lion&#34;</span><span class="p">],</span> <span class="s2">&#34;Legs&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">4</span><span class="p">],</span> <span class="s2">&#34;Plot&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">4</span><span class="p">]})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">width</span> <span class="o">=</span> <span class="mi">50</span>
</span></span><span class="line"><span class="cl"><span class="n">height</span> <span class="o">=</span> <span class="mi">30</span>
</span></span><span class="line"><span class="cl"><span class="n">max_legs_value</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s2">&#34;Legs&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">max</span><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">create_plot_svg_py</span><span class="p">(</span><span class="n">val</span><span class="p">:</span> <span class="nb">int</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="n">canvas</span> <span class="o">=</span> <span class="n">SVG</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">width</span><span class="o">=</span><span class="n">width</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">height</span><span class="o">=</span><span class="n">height</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">elements</span><span class="o">=</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">            <span class="n">Rect</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">x</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">y</span><span class="o">=</span><span class="n">height</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">width</span><span class="o">=</span><span class="n">width</span> <span class="o">*</span> <span class="p">(</span><span class="n">val</span> <span class="o">/</span> <span class="n">max_legs_value</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">height</span><span class="o">=</span><span class="n">height</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">fill</span><span class="o">=</span><span class="s2">&#34;blue&#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">Line</span><span class="p">(</span><span class="n">x1</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">x2</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">y1</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">y2</span><span class="o">=</span><span class="n">height</span><span class="p">,</span> <span class="n">stroke</span><span class="o">=</span><span class="s2">&#34;black&#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">html</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;&lt;div&gt;</span><span class="si">{</span><span class="n">canvas</span><span class="si">}</span><span class="s2">&lt;/div&gt;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">html</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">GT</span><span class="p">(</span><span class="n">df</span><span class="p">)</span><span class="o">.</span><span class="n">fmt</span><span class="p">(</span><span class="n">fns</span><span class="o">=</span><span class="n">create_plot_svg_py</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;Plot&#34;</span><span class="p">])</span>
</span></span></code></pre></td></tr></table>
</div>
</div></details>
<div id="pftjpqlkkc" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
#pftjpqlkkc table {
          font-family: -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>#pftjpqlkkc thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#pftjpqlkkc p { margin: 0; padding: 0; }
#pftjpqlkkc .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: 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; }
#pftjpqlkkc .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#pftjpqlkkc .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; }
#pftjpqlkkc .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; }
#pftjpqlkkc .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; }
#pftjpqlkkc .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#pftjpqlkkc .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; }
#pftjpqlkkc .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; }
#pftjpqlkkc .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; }
#pftjpqlkkc .gt_column_spanner_outer:first-child { padding-left: 0; }
#pftjpqlkkc .gt_column_spanner_outer:last-child { padding-right: 0; }
#pftjpqlkkc .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%; }
#pftjpqlkkc .gt_spanner_row { border-bottom-style: hidden; }
#pftjpqlkkc .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; }
#pftjpqlkkc .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; }
#pftjpqlkkc .gt_from_md&gt; :first-child { margin-top: 0; }
#pftjpqlkkc .gt_from_md&gt; :last-child { margin-bottom: 0; }
#pftjpqlkkc .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; }
#pftjpqlkkc .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; }
#pftjpqlkkc .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; }
#pftjpqlkkc .gt_row_group_first td { border-top-width: 2px; }
#pftjpqlkkc .gt_row_group_first th { border-top-width: 2px; }
#pftjpqlkkc .gt_striped { color: #333333; background-color: #F4F4F4; }
#pftjpqlkkc .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; }
#pftjpqlkkc .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#pftjpqlkkc .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#pftjpqlkkc .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#pftjpqlkkc .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; }
#pftjpqlkkc .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#pftjpqlkkc .gt_left { text-align: left; }
#pftjpqlkkc .gt_center { text-align: center; }
#pftjpqlkkc .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#pftjpqlkkc .gt_font_normal { font-weight: normal; }
#pftjpqlkkc .gt_font_bold { font-weight: bold; }
#pftjpqlkkc .gt_font_italic { font-style: italic; }
#pftjpqlkkc .gt_super { font-size: 65%; }
#pftjpqlkkc .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#pftjpqlkkc .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table class="gt_table" data-quarto-postprocess="true" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="gt_col_headings">
<th id="Animal" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">Animal</th>
<th id="Legs" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">Legs</th>
<th id="Plot" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">Plot</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_left">Ostrich</td>
<td class="gt_row gt_right">2</td>
<td class="gt_row gt_right"><div>
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="30">
<rect x="0" y="7.5" width="12.5" height="15.0" fill="blue"></rect><line stroke="black" x1="0" y1="0" x2="0" y2="30"></line>
</svg>
</div></td>
</tr>
<tr>
<td class="gt_row gt_left">Spider</td>
<td class="gt_row gt_right">8</td>
<td class="gt_row gt_right"><div>
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="30">
<rect x="0" y="7.5" width="50.0" height="15.0" fill="blue"></rect><line stroke="black" x1="0" y1="0" x2="0" y2="30"></line>
</svg>
</div></td>
</tr>
<tr>
<td class="gt_row gt_left">Lion</td>
<td class="gt_row gt_right">4</td>
<td class="gt_row gt_right"><div>
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="30">
<rect x="0" y="7.5" width="25.0" height="15.0" fill="blue"></rect><line stroke="black" x1="0" y1="0" x2="0" y2="30"></line>
</svg>
</div></td>
</tr>
</tbody>
</table>
</div>
<h2 id="setup">Setup
</h2>
<p>Here is the code to start:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></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 class="kn">from</span> <span class="nn">great_tables</span> <span class="kn">import</span> <span class="n">GT</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">pl</span><span class="o">.</span><span class="n">DataFrame</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;Animal&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;Ostrich&#34;</span><span class="p">,</span> <span class="s2">&#34;Spider&#34;</span><span class="p">,</span> <span class="s2">&#34;Lion&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Legs&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">4</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Plot&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">2</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">4</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">gt</span> <span class="o">=</span> <span class="n">GT</span><span class="p">(</span><span class="n">df</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="the-binding-component-gtfmt">The Binding Component: GT.fmt()
</h2>
<p>Let&rsquo;s take advantage of the <a href="https://posit-dev.github.io/great-tables/reference/GT.fmt.html#great_tables.GT.fmt" target="_blank" rel="noopener"><code>fmt()</code></a>
 method to apply a plotting function that formats our row values into plots. To see how we might use <code>fmt()</code>, we first need to define a formatting function to apply to each cell in a column. It will take as input the value in the cell, and should return whatever you want in that cell. Before plotting, let&rsquo;s imagine we wanted to replace the number with a tally of the number of legs:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">create_leg_tally</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">int</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="k">return</span> <span class="s2">&#34;|&#34;</span> <span class="o">*</span> <span class="n">value</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">gt</span><span class="o">.</span><span class="n">fmt</span><span class="p">(</span><span class="n">fns</span><span class="o">=</span><span class="n">create_leg_tally</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="s2">&#34;Plot&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div id="stpyfgvtjj" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
#stpyfgvtjj table {
          font-family: -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>#stpyfgvtjj thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#stpyfgvtjj p { margin: 0; padding: 0; }
#stpyfgvtjj .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: 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; }
#stpyfgvtjj .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#stpyfgvtjj .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; }
#stpyfgvtjj .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; }
#stpyfgvtjj .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; }
#stpyfgvtjj .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#stpyfgvtjj .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; }
#stpyfgvtjj .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; }
#stpyfgvtjj .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; }
#stpyfgvtjj .gt_column_spanner_outer:first-child { padding-left: 0; }
#stpyfgvtjj .gt_column_spanner_outer:last-child { padding-right: 0; }
#stpyfgvtjj .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%; }
#stpyfgvtjj .gt_spanner_row { border-bottom-style: hidden; }
#stpyfgvtjj .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; }
#stpyfgvtjj .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; }
#stpyfgvtjj .gt_from_md&gt; :first-child { margin-top: 0; }
#stpyfgvtjj .gt_from_md&gt; :last-child { margin-bottom: 0; }
#stpyfgvtjj .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; }
#stpyfgvtjj .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; }
#stpyfgvtjj .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; }
#stpyfgvtjj .gt_row_group_first td { border-top-width: 2px; }
#stpyfgvtjj .gt_row_group_first th { border-top-width: 2px; }
#stpyfgvtjj .gt_striped { color: #333333; background-color: #F4F4F4; }
#stpyfgvtjj .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; }
#stpyfgvtjj .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#stpyfgvtjj .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#stpyfgvtjj .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#stpyfgvtjj .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; }
#stpyfgvtjj .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#stpyfgvtjj .gt_left { text-align: left; }
#stpyfgvtjj .gt_center { text-align: center; }
#stpyfgvtjj .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#stpyfgvtjj .gt_font_normal { font-weight: normal; }
#stpyfgvtjj .gt_font_bold { font-weight: bold; }
#stpyfgvtjj .gt_font_italic { font-style: italic; }
#stpyfgvtjj .gt_super { font-size: 65%; }
#stpyfgvtjj .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#stpyfgvtjj .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table>
  <thead>
      <tr>
          <th>Animal</th>
          <th>Legs</th>
          <th>Plot</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ostrich</td>
          <td>2</td>
          <td>||</td>
      </tr>
      <tr>
          <td>Spider</td>
          <td>8</td>
          <td>||||||||</td>
      </tr>
      <tr>
          <td>Lion</td>
          <td>4</td>
          <td>||||</td>
      </tr>
  </tbody>
</table>
</div>
<h2 id="a-lightweight-approach-svgpy">A Lightweight Approach: Svg.py
</h2>
<p>Now we can apply that same logic to making our plots. Let&rsquo;s start with the function that will eventually be passed into <code>fmt()</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span></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">svg</span> <span class="kn">import</span> <span class="n">SVG</span><span class="p">,</span> <span class="n">Rect</span><span class="p">,</span> <span class="n">Line</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">height</span> <span class="o">=</span> <span class="mi">30</span>
</span></span><span class="line"><span class="cl"><span class="n">width</span> <span class="o">=</span> <span class="mi">50</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">create_plot_svg_py</span><span class="p">(</span><span class="n">val</span><span class="p">:</span> <span class="nb">int</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="n">canvas</span> <span class="o">=</span> <span class="n">SVG</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">width</span><span class="o">=</span><span class="n">width</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">height</span><span class="o">=</span><span class="n">height</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">elements</span><span class="o">=</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">            <span class="n">Rect</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">x</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">y</span><span class="o">=</span><span class="n">height</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">width</span><span class="o">=</span><span class="n">width</span> <span class="o">*</span> <span class="p">(</span><span class="n">val</span> <span class="o">/</span> <span class="n">max_legs_value</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                <span class="n">height</span><span class="o">=</span><span class="n">height</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">fill</span><span class="o">=</span><span class="s2">&#34;blue&#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">Line</span><span class="p">(</span><span class="n">x1</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">x2</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">y1</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">y2</span><span class="o">=</span><span class="n">height</span><span class="p">,</span> <span class="n">stroke</span><span class="o">=</span><span class="s2">&#34;black&#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">html</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;&lt;div&gt;</span><span class="si">{</span><span class="n">canvas</span><span class="si">}</span><span class="s2">&lt;/div&gt;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">html</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Here you get to call <code>fmt()</code> to modify the column you want to apply the plotting function to.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">gt</span><span class="o">.</span><span class="n">fmt</span><span class="p">(</span><span class="n">fns</span><span class="o">=</span><span class="n">create_plot_svg_py</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="s2">&#34;Plot&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div id="lqzcgvvbzu" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
#lqzcgvvbzu table {
          font-family: -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>#lqzcgvvbzu thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#lqzcgvvbzu p { margin: 0; padding: 0; }
#lqzcgvvbzu .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: 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; }
#lqzcgvvbzu .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#lqzcgvvbzu .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; }
#lqzcgvvbzu .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; }
#lqzcgvvbzu .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; }
#lqzcgvvbzu .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#lqzcgvvbzu .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; }
#lqzcgvvbzu .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; }
#lqzcgvvbzu .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; }
#lqzcgvvbzu .gt_column_spanner_outer:first-child { padding-left: 0; }
#lqzcgvvbzu .gt_column_spanner_outer:last-child { padding-right: 0; }
#lqzcgvvbzu .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%; }
#lqzcgvvbzu .gt_spanner_row { border-bottom-style: hidden; }
#lqzcgvvbzu .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; }
#lqzcgvvbzu .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; }
#lqzcgvvbzu .gt_from_md&gt; :first-child { margin-top: 0; }
#lqzcgvvbzu .gt_from_md&gt; :last-child { margin-bottom: 0; }
#lqzcgvvbzu .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; }
#lqzcgvvbzu .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; }
#lqzcgvvbzu .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; }
#lqzcgvvbzu .gt_row_group_first td { border-top-width: 2px; }
#lqzcgvvbzu .gt_row_group_first th { border-top-width: 2px; }
#lqzcgvvbzu .gt_striped { color: #333333; background-color: #F4F4F4; }
#lqzcgvvbzu .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; }
#lqzcgvvbzu .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#lqzcgvvbzu .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#lqzcgvvbzu .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#lqzcgvvbzu .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; }
#lqzcgvvbzu .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#lqzcgvvbzu .gt_left { text-align: left; }
#lqzcgvvbzu .gt_center { text-align: center; }
#lqzcgvvbzu .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#lqzcgvvbzu .gt_font_normal { font-weight: normal; }
#lqzcgvvbzu .gt_font_bold { font-weight: bold; }
#lqzcgvvbzu .gt_font_italic { font-style: italic; }
#lqzcgvvbzu .gt_super { font-size: 65%; }
#lqzcgvvbzu .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#lqzcgvvbzu .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table class="gt_table" data-quarto-postprocess="true" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="gt_col_headings">
<th id="Animal" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">Animal</th>
<th id="Legs" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">Legs</th>
<th id="Plot" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">Plot</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_left">Ostrich</td>
<td class="gt_row gt_right">2</td>
<td class="gt_row gt_right"><div>
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="30">
<rect x="0" y="7.5" width="12.5" height="15.0" fill="blue"></rect><line stroke="black" x1="0" y1="0" x2="0" y2="30"></line>
</svg>
</div></td>
</tr>
<tr>
<td class="gt_row gt_left">Spider</td>
<td class="gt_row gt_right">8</td>
<td class="gt_row gt_right"><div>
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="30">
<rect x="0" y="7.5" width="50.0" height="15.0" fill="blue"></rect><line stroke="black" x1="0" y1="0" x2="0" y2="30"></line>
</svg>
</div></td>
</tr>
<tr>
<td class="gt_row gt_left">Lion</td>
<td class="gt_row gt_right">4</td>
<td class="gt_row gt_right"><div>
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="30">
<rect x="0" y="7.5" width="25.0" height="15.0" fill="blue"></rect><line stroke="black" x1="0" y1="0" x2="0" y2="30"></line>
</svg>
</div></td>
</tr>
</tbody>
</table>
</div>
<p>This was very direct, we didn&rsquo;t have save to a buffer or import heavy duty plotting functions. We built the string with the help of <code>svg.py</code> and were able to insert into the table. See the string below:</p>
<!-- I would really like to wrap the output here, but nothing I've tried has worked -->
<!-- https://github.com/quarto-dev/quarto-cli/discussions/6017 -->
<pre><code>'&lt;div&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;50&quot; height=&quot;30&quot;&gt;&lt;rect x=&quot;0&quot; y=&quot;7.5&quot; width=&quot;25.0&quot; height=&quot;15.0&quot; fill=&quot;blue&quot;/&gt;&lt;line stroke=&quot;black&quot; x1=&quot;0&quot; y1=&quot;0&quot; x2=&quot;0&quot; y2=&quot;30&quot;/&gt;&lt;/svg&gt;&lt;/div&gt;'
</code></pre>
<p>Even in its outputted form the string is still easily readable, which is another upside of using an SVG generation package.</p>
<h2 id="extreme-minimalism-adding-html-directly">Extreme Minimalism: Adding HTML directly
</h2>
<p>In the previous section, note that <code>svg.py</code> simply generated a string of HTML. You can do the same thing directly.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><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-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">create_plot_html</span><span class="p">(</span><span class="n">val</span><span class="p">:</span> <span class="nb">int</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="n">bar_element</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    &lt;div style=&#34;position: absolute;
</span></span></span><span class="line"><span class="cl"><span class="s2">                width: </span><span class="si">{</span><span class="n">width</span> <span class="o">*</span> <span class="n">val</span> <span class="o">/</span> <span class="n">max_legs_value</span><span class="si">}</span><span class="s2">px;
</span></span></span><span class="line"><span class="cl"><span class="s2">                height: </span><span class="si">{</span><span class="n">height</span> <span class="o">/</span> <span class="mi">2</span><span class="si">}</span><span class="s2">px;
</span></span></span><span class="line"><span class="cl"><span class="s2">                background-color: purple;
</span></span></span><span class="line"><span class="cl"><span class="s2">                margin-top: </span><span class="si">{</span><span class="n">height</span> <span class="o">/</span> <span class="mi">4</span><span class="si">}</span><span class="s2">px;
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&gt;&lt;/div&gt;&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">line_element</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    &lt;div style=&#34;position: absolute;
</span></span></span><span class="line"><span class="cl"><span class="s2">                top: 0;
</span></span></span><span class="line"><span class="cl"><span class="s2">                bottom: 0;
</span></span></span><span class="line"><span class="cl"><span class="s2">                width: 1px;
</span></span></span><span class="line"><span class="cl"><span class="s2">                background-color: black;
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;&gt;&lt;/div&gt;&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">html</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    &lt;div style=&#34;position: relative; width: </span><span class="si">{</span><span class="n">width</span><span class="si">}</span><span class="s2">px; height: </span><span class="si">{</span><span class="n">height</span><span class="si">}</span><span class="s2">px;&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s2">        </span><span class="si">{</span><span class="n">bar_element</span><span class="si">}</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">        </span><span class="si">{</span><span class="n">line_element</span><span class="si">}</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    &lt;/div&gt;
</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></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">html</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Now that we&rsquo;ve defined our <code>create_plot_*</code> formatting function, the call to <code>fmt()</code> is identical to the one above.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></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">gt</span><span class="o">.</span><span class="n">fmt</span><span class="p">(</span><span class="n">fns</span><span class="o">=</span><span class="n">create_plot_html</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="s2">&#34;Plot&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div id="kauzxggwba" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
#kauzxggwba table {
          font-family: -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>#kauzxggwba thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#kauzxggwba p { margin: 0; padding: 0; }
#kauzxggwba .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: 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; }
#kauzxggwba .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#kauzxggwba .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; }
#kauzxggwba .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; }
#kauzxggwba .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; }
#kauzxggwba .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#kauzxggwba .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; }
#kauzxggwba .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; }
#kauzxggwba .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; }
#kauzxggwba .gt_column_spanner_outer:first-child { padding-left: 0; }
#kauzxggwba .gt_column_spanner_outer:last-child { padding-right: 0; }
#kauzxggwba .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%; }
#kauzxggwba .gt_spanner_row { border-bottom-style: hidden; }
#kauzxggwba .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; }
#kauzxggwba .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; }
#kauzxggwba .gt_from_md&gt; :first-child { margin-top: 0; }
#kauzxggwba .gt_from_md&gt; :last-child { margin-bottom: 0; }
#kauzxggwba .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; }
#kauzxggwba .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; }
#kauzxggwba .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; }
#kauzxggwba .gt_row_group_first td { border-top-width: 2px; }
#kauzxggwba .gt_row_group_first th { border-top-width: 2px; }
#kauzxggwba .gt_striped { color: #333333; background-color: #F4F4F4; }
#kauzxggwba .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; }
#kauzxggwba .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#kauzxggwba .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#kauzxggwba .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#kauzxggwba .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; }
#kauzxggwba .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#kauzxggwba .gt_left { text-align: left; }
#kauzxggwba .gt_center { text-align: center; }
#kauzxggwba .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#kauzxggwba .gt_font_normal { font-weight: normal; }
#kauzxggwba .gt_font_bold { font-weight: bold; }
#kauzxggwba .gt_font_italic { font-style: italic; }
#kauzxggwba .gt_super { font-size: 65%; }
#kauzxggwba .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#kauzxggwba .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table class="gt_table" data-quarto-postprocess="true" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="gt_col_headings">
<th id="Animal" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">Animal</th>
<th id="Legs" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">Legs</th>
<th id="Plot" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">Plot</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_left">Ostrich</td>
<td class="gt_row gt_right">2</td>
<td class="gt_row gt_right"><div style="position: relative; width: 50px; height: 30px;">
<div style="position: absolute;
                width: 12.5px;
                height: 15.0px;
                background-color: purple;
                margin-top: 7.5px;
    ">
&#10;</div>
<div style="position: absolute;
                top: 0;
                bottom: 0;
                width: 1px;
                background-color: black;
    ">
&#10;</div>
</div></td>
</tr>
<tr>
<td class="gt_row gt_left">Spider</td>
<td class="gt_row gt_right">8</td>
<td class="gt_row gt_right"><div style="position: relative; width: 50px; height: 30px;">
<div style="position: absolute;
                width: 50.0px;
                height: 15.0px;
                background-color: purple;
                margin-top: 7.5px;
    ">
&#10;</div>
<div style="position: absolute;
                top: 0;
                bottom: 0;
                width: 1px;
                background-color: black;
    ">
&#10;</div>
</div></td>
</tr>
<tr>
<td class="gt_row gt_left">Lion</td>
<td class="gt_row gt_right">4</td>
<td class="gt_row gt_right"><div style="position: relative; width: 50px; height: 30px;">
<div style="position: absolute;
                width: 25.0px;
                height: 15.0px;
                background-color: purple;
                margin-top: 7.5px;
    ">
&#10;</div>
<div style="position: absolute;
                top: 0;
                bottom: 0;
                width: 1px;
                background-color: black;
    ">
&#10;</div>
</div></td>
</tr>
</tbody>
</table>
</div>
<p>At first glance, encoding HTML in multi-line strings may not be aesthetically pleasing, nor is it particularly more lightweight than <code>svg.py</code>. Still, it provides a good alternative if you are like me and insist on being as close to the output as possible. Separately, I have found the inclusion of text to be simpler with HTML on account of the default text handling behavior that comes along with it.</p>
<h2 id="a-comprehensive-package-plotnine">A Comprehensive Package: Plotnine
</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><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></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">io</span> <span class="kn">import</span> <span class="n">StringIO</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">plotnine</span> <span class="kn">import</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">ggplot</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">aes</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">coord_flip</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">geom_col</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">scale_y_continuous</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">scale_x_continuous</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">theme_void</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">geom_hline</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">max_legs_value</span> <span class="o">=</span> <span class="n">df</span><span class="p">[</span><span class="s2">&#34;Legs&#34;</span><span class="p">]</span><span class="o">.</span><span class="n">max</span><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">create_plot_plotnine</span><span class="p">(</span><span class="n">val</span><span class="p">:</span> <span class="nb">int</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="n">plot</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">ggplot</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="o">+</span> <span class="n">aes</span><span class="p">(</span><span class="n">x</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">y</span><span class="o">=</span><span class="n">val</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">+</span> <span class="n">geom_col</span><span class="p">(</span><span class="n">width</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">fill</span><span class="o">=</span><span class="s2">&#34;green&#34;</span><span class="p">,</span> <span class="n">show_legend</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="o">+</span> <span class="n">scale_y_continuous</span><span class="p">(</span><span class="n">limits</span><span class="o">=</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">max_legs_value</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="o">+</span> <span class="n">scale_x_continuous</span><span class="p">(</span><span class="n">limits</span><span class="o">=</span><span class="p">(</span><span class="mf">0.5</span><span class="p">,</span> <span class="mf">1.5</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="o">+</span> <span class="n">coord_flip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="o">+</span> <span class="n">theme_void</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="o">+</span> <span class="n">geom_hline</span><span class="p">(</span><span class="n">yintercept</span><span class="o">=</span><span class="mi">0</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">buf</span> <span class="o">=</span> <span class="n">StringIO</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">plot</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s2">&#34;svg&#34;</span><span class="p">,</span> <span class="n">width</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span> <span class="n">height</span><span class="o">=</span><span class="mf">0.3</span><span class="p">,</span> <span class="n">verbose</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">svg_content</span> <span class="o">=</span> <span class="n">buf</span><span class="o">.</span><span class="n">getvalue</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">buf</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">html</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;&lt;div&gt;</span><span class="si">{</span><span class="n">svg_content</span><span class="si">}</span><span class="s2">&lt;/div&gt;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">html</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="c1"># This might be familiar by now</span>
</span></span><span class="line"><span class="cl"><span class="n">gt</span><span class="o">.</span><span class="n">fmt</span><span class="p">(</span><span class="n">fns</span><span class="o">=</span><span class="n">create_plot_plotnine</span><span class="p">,</span> <span class="n">columns</span><span class="o">=</span><span class="s2">&#34;Plot&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div id="hbekmneezk" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
#hbekmneezk table {
          font-family: -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>#hbekmneezk thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#hbekmneezk p { margin: 0; padding: 0; }
#hbekmneezk .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: 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; }
#hbekmneezk .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#hbekmneezk .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; }
#hbekmneezk .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; }
#hbekmneezk .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; }
#hbekmneezk .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#hbekmneezk .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; }
#hbekmneezk .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; }
#hbekmneezk .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; }
#hbekmneezk .gt_column_spanner_outer:first-child { padding-left: 0; }
#hbekmneezk .gt_column_spanner_outer:last-child { padding-right: 0; }
#hbekmneezk .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%; }
#hbekmneezk .gt_spanner_row { border-bottom-style: hidden; }
#hbekmneezk .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; }
#hbekmneezk .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; }
#hbekmneezk .gt_from_md&gt; :first-child { margin-top: 0; }
#hbekmneezk .gt_from_md&gt; :last-child { margin-bottom: 0; }
#hbekmneezk .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; }
#hbekmneezk .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; }
#hbekmneezk .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; }
#hbekmneezk .gt_row_group_first td { border-top-width: 2px; }
#hbekmneezk .gt_row_group_first th { border-top-width: 2px; }
#hbekmneezk .gt_striped { color: #333333; background-color: #F4F4F4; }
#hbekmneezk .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; }
#hbekmneezk .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#hbekmneezk .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#hbekmneezk .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#hbekmneezk .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; }
#hbekmneezk .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#hbekmneezk .gt_left { text-align: left; }
#hbekmneezk .gt_center { text-align: center; }
#hbekmneezk .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#hbekmneezk .gt_font_normal { font-weight: normal; }
#hbekmneezk .gt_font_bold { font-weight: bold; }
#hbekmneezk .gt_font_italic { font-style: italic; }
#hbekmneezk .gt_super { font-size: 65%; }
#hbekmneezk .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#hbekmneezk .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table class="gt_table" data-quarto-postprocess="true" data-quarto-disable-processing="false" data-quarto-bootstrap="false">
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="gt_col_headings">
<th id="Animal" class="gt_col_heading gt_columns_bottom_border gt_left" data-quarto-table-cell-role="th" scope="col">Animal</th>
<th id="Legs" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">Legs</th>
<th id="Plot" class="gt_col_heading gt_columns_bottom_border gt_right" data-quarto-table-cell-role="th" scope="col">Plot</th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_left">Ostrich</td>
<td class="gt_row gt_right">2</td>
<td class="gt_row gt_right"><div>
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="36pt" height="21.6pt" viewbox="0 0 36 21.6" xmlns="http://www.w3.org/2000/svg" version="1.1">
<metadata>
<rdf xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<work>
<type rdf:resource="http://purl.org/dc/dcmitype/StillImage"></type>
<date>2026-03-13T11:58:39.602794</date>
<format>image/svg+xml</format>
<creator>
<agent>
<title>
Matplotlib v3.10.8, https://matplotlib.org/
</title>
</agent>
</creator>
</work>
</rdf>
</metadata>
<defs>
</defs>
<g id="figure_1">
<g id="patch_1">
<path d="M 0 21.6 
L 36 21.6 
L 36 0 
L 0 0 
z
" style="fill: #ffffff"></path>
</g>
<g id="axes_1">
<g id="matplotlib.axis_1">
<g id="xtick_1"></g>
<g id="xtick_2"></g>
<g id="xtick_3"></g>
<g id="xtick_4"></g>
<g id="xtick_5"></g>
<g id="xtick_6"></g>
<g id="xtick_7"></g>
<g id="xtick_8"></g>
<g id="xtick_9"></g>
</g>
<g id="matplotlib.axis_2">
<g id="ytick_1"></g>
<g id="ytick_2"></g>
<g id="ytick_3"></g>
<g id="ytick_4"></g>
<g id="ytick_5"></g>
<g id="ytick_6"></g>
<g id="ytick_7"></g>
<g id="ytick_8"></g>
<g id="ytick_9"></g>
</g>
<g id="PolyCollection_1">
<path d="M 1.636364 15.709091 
L 1.636364 5.890909 
L 9.818182 5.890909 
L 9.818182 15.709091 
z
" clip-path="url(#pb77253c51c)" style="fill: #008000"></path>
</g>
<g id="LineCollection_1">
<path d="M 1.636364 21.6 
L 1.636364 -0 
" clip-path="url(#pb77253c51c)" style="fill: none; stroke: #000000; stroke-width: 0.886227"></path>
</g>
</g>
</g>
<defs>
<clippath id="pb77253c51c">
<rect x="0" y="0" width="36" height="21.6"></rect>
</clippath>
</defs>
</svg>
</div></td>
</tr>
<tr>
<td class="gt_row gt_left">Spider</td>
<td class="gt_row gt_right">8</td>
<td class="gt_row gt_right"><div>
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="36pt" height="21.6pt" viewbox="0 0 36 21.6" xmlns="http://www.w3.org/2000/svg" version="1.1">
<metadata>
<rdf xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<work>
<type rdf:resource="http://purl.org/dc/dcmitype/StillImage"></type>
<date>2026-03-13T11:58:39.641571</date>
<format>image/svg+xml</format>
<creator>
<agent>
<title>
Matplotlib v3.10.8, https://matplotlib.org/
</title>
</agent>
</creator>
</work>
</rdf>
</metadata>
<defs>
</defs>
<g id="figure_1">
<g id="patch_1">
<path d="M 0 21.6 
L 36 21.6 
L 36 0 
L 0 0 
z
" style="fill: #ffffff"></path>
</g>
<g id="axes_1">
<g id="matplotlib.axis_1">
<g id="xtick_1"></g>
<g id="xtick_2"></g>
<g id="xtick_3"></g>
<g id="xtick_4"></g>
<g id="xtick_5"></g>
<g id="xtick_6"></g>
<g id="xtick_7"></g>
<g id="xtick_8"></g>
<g id="xtick_9"></g>
</g>
<g id="matplotlib.axis_2">
<g id="ytick_1"></g>
<g id="ytick_2"></g>
<g id="ytick_3"></g>
<g id="ytick_4"></g>
<g id="ytick_5"></g>
<g id="ytick_6"></g>
<g id="ytick_7"></g>
<g id="ytick_8"></g>
<g id="ytick_9"></g>
</g>
<g id="PolyCollection_1">
<path d="M 1.636364 15.709091 
L 1.636364 5.890909 
L 34.363636 5.890909 
L 34.363636 15.709091 
z
" clip-path="url(#p7dab6c5e74)" style="fill: #008000"></path>
</g>
<g id="LineCollection_1">
<path d="M 1.636364 21.6 
L 1.636364 -0 
" clip-path="url(#p7dab6c5e74)" style="fill: none; stroke: #000000; stroke-width: 0.886227"></path>
</g>
</g>
</g>
<defs>
<clippath id="p7dab6c5e74">
<rect x="0" y="0" width="36" height="21.6"></rect>
</clippath>
</defs>
</svg>
</div></td>
</tr>
<tr>
<td class="gt_row gt_left">Lion</td>
<td class="gt_row gt_right">4</td>
<td class="gt_row gt_right"><div>
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="36pt" height="21.6pt" viewbox="0 0 36 21.6" xmlns="http://www.w3.org/2000/svg" version="1.1">
<metadata>
<rdf xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<work>
<type rdf:resource="http://purl.org/dc/dcmitype/StillImage"></type>
<date>2026-03-13T11:58:39.678418</date>
<format>image/svg+xml</format>
<creator>
<agent>
<title>
Matplotlib v3.10.8, https://matplotlib.org/
</title>
</agent>
</creator>
</work>
</rdf>
</metadata>
<defs>
</defs>
<g id="figure_1">
<g id="patch_1">
<path d="M 0 21.6 
L 36 21.6 
L 36 0 
L 0 0 
z
" style="fill: #ffffff"></path>
</g>
<g id="axes_1">
<g id="matplotlib.axis_1">
<g id="xtick_1"></g>
<g id="xtick_2"></g>
<g id="xtick_3"></g>
<g id="xtick_4"></g>
<g id="xtick_5"></g>
<g id="xtick_6"></g>
<g id="xtick_7"></g>
<g id="xtick_8"></g>
<g id="xtick_9"></g>
</g>
<g id="matplotlib.axis_2">
<g id="ytick_1"></g>
<g id="ytick_2"></g>
<g id="ytick_3"></g>
<g id="ytick_4"></g>
<g id="ytick_5"></g>
<g id="ytick_6"></g>
<g id="ytick_7"></g>
<g id="ytick_8"></g>
<g id="ytick_9"></g>
</g>
<g id="PolyCollection_1">
<path d="M 1.636364 15.709091 
L 1.636364 5.890909 
L 18 5.890909 
L 18 15.709091 
z
" clip-path="url(#p83128198cc)" style="fill: #008000"></path>
</g>
<g id="LineCollection_1">
<path d="M 1.636364 21.6 
L 1.636364 -0 
" clip-path="url(#p83128198cc)" style="fill: none; stroke: #000000; stroke-width: 0.886227"></path>
</g>
</g>
</g>
<defs>
<clippath id="p83128198cc">
<rect x="0" y="0" width="36" height="21.6"></rect>
</clippath>
</defs>
</svg>
</div></td>
</tr>
</tbody>
</table>
</div>
<p>Nice! But that was a sizable chunk of code just to create plots comprised of one bar each. If you&rsquo;re like me, you&rsquo;ll find it&rsquo;s not at all trivial to do, especially without experience using the plotting package.</p>
<p>However, this isn&rsquo;t the only graphic you might want to have on display &ndash; when you come across a use case that necessitates more detailed plots, a comprehensive plotting package like <code>plotnine</code> could very well be your best bet. Imagine we are passing in a list of tuples and want to generate a scatterplot, writing all of those as <code>svg.py</code> elements or direct HTML would be quite cumbersome.</p>
<h2 id="conclusion">Conclusion
</h2>
<p>How you choose to add plots to Great Tables is up to you. In writing graphical plotting functions for <a href="https://posit-dev.github.io/gt-extras/articles/intro.html" target="_blank" rel="noopener"><strong>gt-extras</strong></a>
, I&rsquo;ve personally turned towards an HTML-only approach that I&rsquo;ve felt comfortable with in other settings. With that said, I do believe converting table values to graphic output is a task best done with a little bit of help (whether it be <code>svg-py</code> or another plotting package will depend on how detailed your plots are).</p>
<p>The choice ultimately depends on your specific needs: simplicity and directness, versus abstraction and power. By understanding the trade-offs, you will be able to tailor your approach to the needs of your project.</p>
]]></description>
    </item>
    <item>
      <title>Great Tables &#43; marimo = Interactive Tables</title>
      <link>https://posit-open-source.netlify.app/blog/great-tables/marimo-and-great-tables/</link>
      <pubDate>Tue, 24 Jun 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/great-tables/marimo-and-great-tables/</guid>
      <dc:creator>Rich Iannone</dc:creator>
      <dc:creator>Jerry Wu</dc:creator><description><![CDATA[<p>Jerry Wu&rsquo;s <a href="https://tech.ycwu.space/posts/gt-marimo-in-quarto/20250612.html" target="_blank" rel="noopener">recent post</a>
 demonstrates how Great Tables can be made reactive in a marimo notebook. The post showcases a really nice integration where marimo&rsquo;s reactive widgets can be embedded directly into Great Tables using the <a href="https://posit-dev.github.io/great-tables/reference/html.html" target="_blank" rel="noopener"><code>html()</code></a>
 helper function. This effectively creates an interactive table that updates in real-time whenever users interact with the controls.</p>
<h2 id="interactive-table-demo">Interactive Table Demo
</h2>
<p>Here&rsquo;s a screencast (based on the <a href="https://tech.ycwu.space/posts/gt-marimo-in-quarto/20250612.html" target="_blank" rel="noopener">aforementioned post</a>
) showing Jerry&rsquo;s GT table in marimo. It demonstrates how the embedded marimo widgets create reactive effects, allowing you to directly modify the table in real-time.</p>
<figure>
<img src="https://posit-open-source.netlify.app/blog/great-tables/marimo-and-great-tables/gt_marimo.gif" alt="Interactive Great Tables with marimo widgets" />
<figcaption aria-hidden="true">Interactive Great Tables with marimo widgets</figcaption>
</figure>
<p>The marimo reactive widgets can be embedded into a GT table through the <a href="https://posit-dev.github.io/great-tables/reference/html.html" target="_blank" rel="noopener"><code>html()</code></a>
 helper function. In Jerry&rsquo;s example, the table responds to widget changes in the <a href="https://posit-dev.github.io/great-tables/reference/GT.opt_stylize.html" target="_blank" rel="noopener"><code>opt_stylize()</code></a>
 method call, where the <code>.value</code> attributes for all of the widgets are passed in as arguments.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">gt</span><span class="o">.</span><span class="n">opt_stylize</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_widget</span><span class="o">.</span><span class="n">value</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">color</span><span class="o">=</span><span class="n">color_widget</span><span class="o">.</span><span class="n">value</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">add_row_striping</span><span class="o">=</span><span class="n">row_striping_widget</span><span class="o">.</span><span class="n">value</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="marimo-showcases-the-integration">marimo Showcases the Integration
</h2>
<p>The marimo team must have seen that post and liked what they saw! They recently released a video showcasing how marimo widgets work with Great Tables to create reactive tables.</p>















  

  
  
  
    
    
  

  
  










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




<p>For more details on marimo&rsquo;s widget system, check out their <a href="https://docs.marimo.io/api/inputs/" target="_blank" rel="noopener">UI elements documentation</a>
 and <a href="https://docs.marimo.io/guides/reactivity/" target="_blank" rel="noopener">reactivity guide</a>
.</p>
<h2 id="marimo--quarto-integration">marimo + Quarto Integration
</h2>
<p>For those interested in embedding marimo notebooks directly in Quarto documents, the marimo team has also created a <a href="https://github.com/marimo-team/quarto-marimo" target="_blank" rel="noopener">quarto-marimo plugin</a>
. This extension allows you to run marimo code blocks natively within Quarto documents, making it easy to create interactive content that combines the best of both tools. It&rsquo;s a very exciting development since we use Quarto extensively (our docs and this very blog post were authored through Quarto!). And naturally there&rsquo;s a video for this as well.</p>















  

  
  
  
    
    
  

  
  










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




<h2 id="wrapping-up">Wrapping Up
</h2>
<p>We&rsquo;re excited by how the marimo team is committed to creating seamless integrations with popular tools like Great Tables and Quarto. We&rsquo;re also excited to see what new features and integrations they&rsquo;ll develop next!</p>
<p>Be sure to check out the <a href="https://www.youtube.com/@marimo-team" target="_blank" rel="noopener">marimo YouTube channel</a>
 as they consistently produce high-quality videos that demonstrate the power of reactive notebooks in action.</p>
]]></description>
      <enclosure url="https://posit-open-source.netlify.app/blog/great-tables/marimo-and-great-tables/gt_marimo.gif" length="1273278" type="image/gif" />
    </item>
    <item>
      <title>Data Validation Libraries for Polars (2025 Edition)</title>
      <link>https://posit-open-source.netlify.app/blog/pointblank/validation-libs-2025/</link>
      <pubDate>Wed, 04 Jun 2025 00:00:00 +0000</pubDate>
      <guid>https://posit-open-source.netlify.app/blog/pointblank/validation-libs-2025/</guid>
      <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>Data validation is a very important part of any data pipeline. And with Polars gaining popularity as
a superfast and feature-packed DataFrame library, developers need validation tools that work
seamlessly with it. But here&rsquo;s the thing: not all validation libraries are created equal, and
choosing the wrong one can lead to frustration, technical debt, or validation gaps that could bite
you later.</p>
<p>In this survey (conducted halfway through 2025) we&rsquo;ll explore five Python validation libraries that
support Polars DataFrames, each bringing distinct strengths to different validation challenges.</p>
<div class="callout callout-note" role="note" aria-label="Note">
<div class="callout-header">
<span class="callout-title">Note</span>
</div>
<div class="callout-body">
<p>Great Expectations, while being one of the most established data validation frameworks in the Python
ecosystem, is not included in this survey as it doesn&rsquo;t yet offer native Polars support. See <a href="https://github.com/great-expectations/great_expectations/issues/10702" target="_blank" rel="noopener">this
issue</a>
 and
<a href="https://github.com/great-expectations/great_expectations/discussions/10144" target="_blank" rel="noopener">this discussion</a>
 for
the inside baseball.</p>
</div>
</div>
<h2 id="recommendations">Recommendations
</h2>
<p>Here are the unique strengths for each library:</p>
<div id="vzwypnqhnw" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
#vzwypnqhnw table {
          font-family: -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>#vzwypnqhnw thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#vzwypnqhnw p { margin: 0; padding: 0; }
#vzwypnqhnw .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: 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; }
#vzwypnqhnw .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#vzwypnqhnw .gt_title { color: #333333; font-size: 125%; font-weight: initial; padding-top: 4px; padding-bottom: 4px; padding-left: 10px; padding-right: 10px; border-bottom-color: #FFFFFF; border-bottom-width: 0; }
#vzwypnqhnw .gt_subtitle { color: #333333; font-size: 85%; font-weight: initial; padding-top: 3px; padding-bottom: 5px; padding-left: 10px; padding-right: 10px; border-top-color: #FFFFFF; border-top-width: 0; }
#vzwypnqhnw .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; }
#vzwypnqhnw .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#vzwypnqhnw .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; }
#vzwypnqhnw .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: 10px; padding-right: 10px; overflow-x: hidden; }
#vzwypnqhnw .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; }
#vzwypnqhnw .gt_column_spanner_outer:first-child { padding-left: 0; }
#vzwypnqhnw .gt_column_spanner_outer:last-child { padding-right: 0; }
#vzwypnqhnw .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%; }
#vzwypnqhnw .gt_spanner_row { border-bottom-style: hidden; }
#vzwypnqhnw .gt_group_heading { padding-top: 8px; padding-bottom: 8px; padding-left: 10px; padding-right: 10px; 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; }
#vzwypnqhnw .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; }
#vzwypnqhnw .gt_from_md&gt; :first-child { margin-top: 0; }
#vzwypnqhnw .gt_from_md&gt; :last-child { margin-bottom: 0; }
#vzwypnqhnw .gt_row { padding-top: 8px; padding-bottom: 8px; padding-left: 10px; padding-right: 10px; 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; }
#vzwypnqhnw .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: 10px; padding-right: 10px; }
#vzwypnqhnw .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: 10px; padding-right: 10px; vertical-align: top; }
#vzwypnqhnw .gt_row_group_first td { border-top-width: 2px; }
#vzwypnqhnw .gt_row_group_first th { border-top-width: 2px; }
#vzwypnqhnw .gt_striped { color: #333333; background-color: #F4F4F4; }
#vzwypnqhnw .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; }
#vzwypnqhnw .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#vzwypnqhnw .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#vzwypnqhnw .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#vzwypnqhnw .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; }
#vzwypnqhnw .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 10px; padding-right: 10px; text-align: left; }
#vzwypnqhnw .gt_left { text-align: left; }
#vzwypnqhnw .gt_center { text-align: center; }
#vzwypnqhnw .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#vzwypnqhnw .gt_font_normal { font-weight: normal; }
#vzwypnqhnw .gt_font_bold { font-weight: bold; }
#vzwypnqhnw .gt_font_italic { font-style: italic; }
#vzwypnqhnw .gt_super { font-size: 65%; }
#vzwypnqhnw .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#vzwypnqhnw .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table>
  <thead>
      <tr>
          <th>Library</th>
          <th>⭐</th>
          <th>Best Features</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://github.com/unionai-oss/pandera" style="color: #333333; text-underline-offset: 3px;">Pandera</a></td>
          <td>3,838</td>
          <td>Statistical testing, schema-centric validation, mypy integration</td>
      </tr>
      <tr>
          <td><a href="https://github.com/JakobGM/patito" style="color: #333333; text-underline-offset: 3px;">Patito</a></td>
          <td>468</td>
          <td>Pydantic integration, model-based validation, row-level objects</td>
      </tr>
      <tr>
          <td><a href="https://github.com/posit-dev/pointblank" style="color: #333333; text-underline-offset: 3px;">Pointblank</a></td>
          <td>173</td>
          <td>Interactive reports, threshold management, stakeholder communication</td>
      </tr>
      <tr>
          <td><a href="https://github.com/akmalsoliev/Validoopsie" style="color: #333333; text-underline-offset: 3px;">Validoopsie</a></td>
          <td>63</td>
          <td>Built-in logging, composable validation, impact levels, lightweight Great Expectations alternative</td>
      </tr>
      <tr>
          <td><a href="https://github.com/Quantco/dataframely" style="color: #333333; text-underline-offset: 3px;">Dataframely</a></td>
          <td>319</td>
          <td>Collection validation, advanced type safety, failure analysis</td>
      </tr>
  </tbody>
</table>
</div>
<p>Based on these strengths, here are my recommendations for which libraries to use according to use case:</p>
<div id="pliqxkgbyp" style="padding-left:0px;padding-right:0px;padding-top:10px;padding-bottom:10px;overflow-x:auto;overflow-y:auto;width:auto;height:auto;">
<style>
#pliqxkgbyp table {
          font-family: -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>#pliqxkgbyp thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#pliqxkgbyp p { margin: 0; padding: 0; }
#pliqxkgbyp .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: 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; }
#pliqxkgbyp .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#pliqxkgbyp .gt_title { color: #333333; font-size: 125%; font-weight: initial; padding-top: 4px; padding-bottom: 4px; padding-left: 10px; padding-right: 10px; border-bottom-color: #FFFFFF; border-bottom-width: 0; }
#pliqxkgbyp .gt_subtitle { color: #333333; font-size: 85%; font-weight: initial; padding-top: 3px; padding-bottom: 5px; padding-left: 10px; padding-right: 10px; border-top-color: #FFFFFF; border-top-width: 0; }
#pliqxkgbyp .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; }
#pliqxkgbyp .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#pliqxkgbyp .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; }
#pliqxkgbyp .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: 10px; padding-right: 10px; overflow-x: hidden; }
#pliqxkgbyp .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; }
#pliqxkgbyp .gt_column_spanner_outer:first-child { padding-left: 0; }
#pliqxkgbyp .gt_column_spanner_outer:last-child { padding-right: 0; }
#pliqxkgbyp .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%; }
#pliqxkgbyp .gt_spanner_row { border-bottom-style: hidden; }
#pliqxkgbyp .gt_group_heading { padding-top: 8px; padding-bottom: 8px; padding-left: 10px; padding-right: 10px; 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; }
#pliqxkgbyp .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; }
#pliqxkgbyp .gt_from_md&gt; :first-child { margin-top: 0; }
#pliqxkgbyp .gt_from_md&gt; :last-child { margin-bottom: 0; }
#pliqxkgbyp .gt_row { padding-top: 8px; padding-bottom: 8px; padding-left: 10px; padding-right: 10px; 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; }
#pliqxkgbyp .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: 10px; padding-right: 10px; }
#pliqxkgbyp .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: 10px; padding-right: 10px; vertical-align: top; }
#pliqxkgbyp .gt_row_group_first td { border-top-width: 2px; }
#pliqxkgbyp .gt_row_group_first th { border-top-width: 2px; }
#pliqxkgbyp .gt_striped { color: #333333; background-color: #F4F4F4; }
#pliqxkgbyp .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; }
#pliqxkgbyp .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#pliqxkgbyp .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#pliqxkgbyp .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#pliqxkgbyp .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; }
#pliqxkgbyp .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 10px; padding-right: 10px; text-align: left; }
#pliqxkgbyp .gt_left { text-align: left; }
#pliqxkgbyp .gt_center { text-align: center; }
#pliqxkgbyp .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#pliqxkgbyp .gt_font_normal { font-weight: normal; }
#pliqxkgbyp .gt_font_bold { font-weight: bold; }
#pliqxkgbyp .gt_font_italic { font-style: italic; }
#pliqxkgbyp .gt_super { font-size: 65%; }
#pliqxkgbyp .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#pliqxkgbyp .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table>
  <thead>
      <tr>
          <th>Use Case</th>
          <th>Best Libraries</th>
          <th>Description</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Type-safe pipelines</td>
          <td>Pandera, Dataframely, Patito</td>
          <td>Static type checking and compile-time validation</td>
      </tr>
      <tr>
          <td>Stakeholder reporting</td>
          <td>Pointblank</td>
          <td>Sharing validation results with non-technical teams</td>
      </tr>
      <tr>
          <td>Row-level object modeling</td>
          <td>Patito</td>
          <td>Converting DataFrame rows to Python objects with business logic</td>
      </tr>
      <tr>
          <td>Statistical validation</td>
          <td>Pandera</td>
          <td>Testing data distributions and statistical properties</td>
      </tr>
      <tr>
          <td>Data quality improvement</td>
          <td>Pointblank, Validoopsie</td>
          <td>Gradual quality improvement with threshold tracking</td>
      </tr>
  </tbody>
</table>
</div>
<h2 id="setup">Setup
</h2>
<p>We are going to run through examples with <strong>Pandera</strong>, <strong>Patito</strong>, <strong>Pointblank</strong>, <strong>Validoopsie</strong>,
and <strong>Dataframely</strong>, using this Polars DataFrame as our test case:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span></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="c1"># Standard dataset for all validation examples</span>
</span></span><span class="line"><span class="cl"><span class="n">user_data</span> <span class="o">=</span> <span class="n">pl</span><span class="o">.</span><span class="n">DataFrame</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;user_id&#34;</span><span class="p">:</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 class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;age&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mi">25</span><span class="p">,</span> <span class="mi">30</span><span class="p">,</span> <span class="mi">22</span><span class="p">,</span> <span class="mi">45</span><span class="p">,</span> <span class="mi">95</span><span class="p">],</span>  <span class="c1"># &lt;- includes a very high age</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;user1@example.com&#34;</span><span class="p">,</span> <span class="s2">&#34;user2@example.com&#34;</span><span class="p">,</span> <span class="s2">&#34;invalid-email&#34;</span><span class="p">,</span>  <span class="c1"># &lt;- has an invalid email</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;user4@example.com&#34;</span><span class="p">,</span> <span class="s2">&#34;user5@example.com&#34;</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;score&#34;</span><span class="p">:</span> <span class="p">[</span><span class="mf">85.5</span><span class="p">,</span> <span class="mf">92.0</span><span class="p">,</span> <span class="mf">78.3</span><span class="p">,</span> <span class="mf">88.7</span><span class="p">,</span> <span class="mf">95.2</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>We&rsquo;ll try to run the same data validation across the surveyed libraries, so we&rsquo;ll check:</p>
<ul>
<li>schema validation (correct column types)</li>
<li><code>user_id</code> values greater than <code>0</code></li>
<li><code>age</code> values between <code>18</code> and <code>80</code> (inclusive)</li>
<li><code>email</code> strings matching a basic email regex pattern</li>
<li><code>score</code> values between <code>0</code> and <code>100</code> (inclusive)</li>
</ul>
<p>Now let&rsquo;s dive into each library, starting with the statistically-focused Pandera.</p>
<h2 id="1-pandera-schema-first-validation-with-statistical-checks">1. Pandera: Schema-First Validation with Statistical Checks
</h2>
<p>Pandera is a statistical data validation toolkit designed to provide a flexible and expressive API
for performing data validation on dataframe-like objects. The library centers on schema-centric
validation, where you define the expected structure and constraints of your data upfront. You can
enable both runtime validation and static type checking integration. Pandera added Polars support in
version <code>0.19.0</code> (early 2024).</p>
<h3 id="example">Example
</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span></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">pandera.polars</span> <span class="k">as</span> <span class="nn">pa</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define schema using our standard dataset</span>
</span></span><span class="line"><span class="cl"><span class="n">schema</span> <span class="o">=</span> <span class="n">pa</span><span class="o">.</span><span class="n">DataFrameSchema</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;user_id&#34;</span><span class="p">:</span> <span class="n">pa</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">pl</span><span class="o">.</span><span class="n">Int64</span><span class="p">,</span> <span class="n">checks</span><span class="o">=</span><span class="n">pa</span><span class="o">.</span><span class="n">Check</span><span class="o">.</span><span class="n">gt</span><span class="p">(</span><span class="mi">0</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;age&#34;</span><span class="p">:</span> <span class="n">pa</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">pl</span><span class="o">.</span><span class="n">Int64</span><span class="p">,</span> <span class="n">checks</span><span class="o">=</span><span class="p">[</span><span class="n">pa</span><span class="o">.</span><span class="n">Check</span><span class="o">.</span><span class="n">ge</span><span class="p">(</span><span class="mi">18</span><span class="p">),</span> <span class="n">pa</span><span class="o">.</span><span class="n">Check</span><span class="o">.</span><span class="n">le</span><span class="p">(</span><span class="mi">80</span><span class="p">)]),</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="n">pa</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">pl</span><span class="o">.</span><span class="n">Utf8</span><span class="p">,</span> <span class="n">checks</span><span class="o">=</span><span class="n">pa</span><span class="o">.</span><span class="n">Check</span><span class="o">.</span><span class="n">str_matches</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;^[^@]+@[^@]+\.[^@]+$&#34;</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;score&#34;</span><span class="p">:</span> <span class="n">pa</span><span class="o">.</span><span class="n">Column</span><span class="p">(</span><span class="n">pl</span><span class="o">.</span><span class="n">Float64</span><span class="p">,</span> <span class="n">checks</span><span class="o">=</span><span class="n">pa</span><span class="o">.</span><span class="n">Check</span><span class="o">.</span><span class="n">in_range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">100</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"># Validate the schema</span>
</span></span><span class="line"><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">validated_data</span> <span class="o">=</span> <span class="n">schema</span><span class="o">.</span><span class="n">validate</span><span class="p">(</span><span class="n">user_data</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;Validation successful!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">except</span> <span class="n">pa</span><span class="o">.</span><span class="n">errors</span><span class="o">.</span><span class="n">SchemaError</span> <span class="k">as</span> <span class="n">e</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;Validation failed: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><pre><code>Validation failed: Column 'age' failed validator number 1: &lt;Check less_than_or_equal_to: less_than_or_equal_to(80)&gt; failure case examples: [{'age': 95}]
</code></pre>
<p>This example demonstrates Pandera&rsquo;s declarative approach, where you define what your data should
look like rather than writing imperative validation logic. The schema acts as both documentation and
as a validation contract. Notice how multiple checks can be applied to a single column (here, the
<code>age</code> column receives two checks), and the validation either succeeds completely or provides
error information about what failed.</p>
<h3 id="comparisons">Comparisons
</h3>
<p>Both Pandera and Patito use declarative, schema-centric approaches, but differ in their design
philosophies:</p>
<ul>
<li>Pandera uses a dictionary-like schema structure with Column objects for defining validation rules</li>
<li>Patito uses Pydantic model classes with familiar Field syntax for validation constraints</li>
<li>Pandera focuses heavily on statistical validation capabilities like hypothesis testing</li>
<li>Patito emphasizes integration with existing Pydantic workflows and object modeling</li>
<li>a key behavioral difference: Patito reports all validation errors in a single pass, while Pandera
stops at the first failure</li>
</ul>
<p>The choice between them often comes down to whether you prefer Pandera&rsquo;s statistical focus or
Patito&rsquo;s Pydantic integration.</p>
<p>Unlike Pointblank&rsquo;s step-by-step validation reporting, Pandera validates the entire schema at once.
Compared to Patito&rsquo;s model-based approach, Pandera focuses more on statistical validation
capabilities. Unlike Validoopsie&rsquo;s and Pointblank&rsquo;s method chaining style, Pandera uses a more
declarative, schema-centric approach.</p>
<h3 id="unique-strengths-and-when-to-use">Unique Strengths and When to Use
</h3>
<p>Here are some of stand-out features that Pandera has:</p>
<ul>
<li>type-safe schema definitions with <code>mypy</code> integration</li>
<li>statistical hypothesis testing for data distributions: perform t-tests, chi-square tests, and
custom statistical tests directly in your validation schema</li>
<li>excellent integration with Pandas, Polars, and Arrow support</li>
<li>declarative schema syntax that serves as documentation</li>
<li>built-in support for data coercion and transformation</li>
</ul>
<p>This statistical validation capability goes beyond basic type and range checking to test actual data
relationships and distributional assumptions. For example, you can validate that the mean height of
group <code>&quot;M&quot;</code> is significantly greater than group <code>&quot;F&quot;</code> using a two-sample t-test, or test whether a
column follows a normal distribution. This makes Pandera uniquely powerful for data science
workflows where the statistical properties of your data are as important as individual data points
meeting basic constraints.</p>
<p>Data practitioners should choose Pandera when building type-safe data pipelines where schema
validation is critical, especially in data science workflows that require statistical validation.
It&rsquo;s ideal for users that value static type checking, need to validate statistical properties of
their data, or want schemas that double as documentation.</p>
<p>Pandera also excels in environments where data contracts between teams are important and where the
statistical properties of data matter as much as basic type checking.</p>
<h2 id="2-patito-pydantic-style-data-models-for-dataframes">2. Patito: Pydantic-Style Data Models for DataFrames
</h2>
<p>Patito brings Pydantic&rsquo;s well-received model-based validation approach to DataFrame validation,
creating a bridge between Pydantic-style data validation and DataFrame processing. The library&rsquo;s
primary goal is to provide a familiar, Pydantic-style interface for defining and validating
DataFrame schemas, making it particularly appealing to developers already using Pydantic in their
applications.</p>
<p>Patito launched with Polars support from the beginning (in late 2022). Native Polars integration is
touted as one of its core features, reflecting the growing adoption of Polars in the Python
ecosystem.</p>
<h3 id="example-1">Example
</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></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">patito</span> <span class="k">as</span> <span class="nn">pt</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Annotated</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">UserModel</span><span class="p">(</span><span class="n">pt</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="n">pt</span><span class="o">.</span><span class="n">Field</span><span class="p">(</span><span class="n">gt</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">age</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="n">pt</span><span class="o">.</span><span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mi">18</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mi">80</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">    <span class="n">email</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">pt</span><span class="o">.</span><span class="n">Field</span><span class="p">(</span><span class="n">pattern</span><span class="o">=</span><span class="sa">r</span><span class="s2">&#34;^[^@]+@[^@]+\.[^@]+$&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">score</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="n">pt</span><span class="o">.</span><span class="n">Field</span><span class="p">(</span><span class="n">ge</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="n">le</span><span class="o">=</span><span class="mf">100.0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Validate using the model</span>
</span></span><span class="line"><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">UserModel</span><span class="o">.</span><span class="n">validate</span><span class="p">(</span><span class="n">user_data</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;Validation successful!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">except</span> <span class="n">pt</span><span class="o">.</span><span class="n">exceptions</span><span class="o">.</span><span class="n">DataFrameValidationError</span> <span class="k">as</span> <span class="n">e</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;Validation failed: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><pre><code>Validation failed: 2 validation errors for UserModel
age
  1 row with out of bound values. (type=value_error.rowvalue)
email
  1 row with out of bound values. (type=value_error.rowvalue)
</code></pre>
<p>This example showcases Patito&rsquo;s model-centric approach where validation rules are embedded in class
definitions. The use of Python&rsquo;s type hints and Pydantic&rsquo;s Field syntax makes the validation rules
self-documenting. Notably, Patito reports all validation errors at once, providing a fairly
comprehensive view of data quality issues, whereas other libraries (e.g., Pandera) stop at the first
failure.</p>
<h3 id="column-validation-approaches-pandera-vs-patito">Column Validation Approaches: Pandera vs Patito
</h3>
<p><strong>Pandera offers a much more extensive and flexible system for column validation</strong> compared to
Patito&rsquo;s field-based approach. While Patito provides a solid set of built-in field constraints
(like <code>gt</code>, <code>le</code>, <code>regex</code>, <code>unique</code>, etc.) that cover common validation scenarios, Pandera&rsquo;s Check
system is designed for both simple and highly sophisticated validation logic.</p>
<p>The key architectural difference seems to lie in extensibility and complexity. Pandera&rsquo;s <code>Check</code>
objects accept arbitrary functions, allowing you to write custom validation logic that can be as
simple as <code>lambda s: s &gt; 0</code> or as complex as statistical hypothesis tests using scipy. You can
create vectorized checks that operate on entire Series objects for performance, element-wise checks
for atomic validation, and even grouped checks that validate subsets of data based on other columns.
Patito&rsquo;s <code>Field</code> constraints, while clean and declarative, are more limited to the predefined
validation types that Pydantic and Patito provide.</p>
<p>Pandera also supports advanced validation patterns that Patito doesn&rsquo;t directly offer, such as
wide-form data checks (validating relationships across multiple columns), grouped validation (where
checks are applied to subsets of data based on grouping columns), and the ability to raise warnings
instead of errors for non-critical validation failures. While Patito does support custom constraints
through Polars expressions via the <code>constraints</code> parameter, this requires knowledge of Polars
expression syntax and, depending on where you&rsquo;re coming from, could be less intuitive than Pandera&rsquo;s
function-based approach.</p>
<p>For most common validation scenarios, Patito&rsquo;s field-based validation is simpler and more readable,
especially for teams already familiar with Pydantic. However, for complex data validation
requirements, statistical validation, or when you need maximum flexibility in defining validation
logic, Pandera&rsquo;s Check system provides significantly more power and extensibility.</p>
<h3 id="unique-strengths-and-when-to-use-1">Unique Strengths and When to Use
</h3>
<ul>
<li>Pydantic-style model definitions with familiar syntax for Pydantic users</li>
<li>rich type system integration with Python&rsquo;s typing system</li>
<li>model inheritance and composition for complex data structures</li>
<li>seamless integration with existing Pydantic-based applications</li>
<li>row-level object modeling for converting DataFrame rows to Python objects with methods</li>
<li>mock data generation for testing with <code>.examples()</code> method</li>
</ul>
<p>People should choose Patito when they&rsquo;re already using Pydantic in their applications and want
consistent validation patterns across data processing and application logic. It&rsquo;s great when you
need to validate DataFrames and then work with individual rows as rich Python objects with embedded
business logic and methods (e.g., a <code>Product</code> row that has a <code>.url</code> property or
<code>.calculate_discount()</code> method). Patito is also good when you need to generate realistic test data
and want object-oriented interfaces for their data models.</p>
<h2 id="3-pointblank-comprehensive-validation-with-beautiful-reports">3. Pointblank: Comprehensive Validation with Beautiful Reports
</h2>
<p>Pointblank is a comprehensive data validation framework designed to make data quality assessment
both thorough and accessible to stakeholders. Originally inspired by the R package of the same name,
Pointblank&rsquo;s primary mission is to provide validation workflows that generate beautiful, interactive
reports that can be shared with both technical and non-technical team members.</p>
<p>Pointblank launched with Polars support as a core feature from its initial Python release in late
2024, built on top of the Narwhals and Ibis compatibility layers to provide consistent DataFrame
operations across multiple backends including Polars, Pandas, and database connections.</p>
<h3 id="example-2">Example
</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pointblank</span> <span class="k">as</span> <span class="nn">pb</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">schema</span> <span class="o">=</span> <span class="n">pb</span><span class="o">.</span><span class="n">Schema</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;user_id&#34;</span><span class="p">,</span> <span class="s2">&#34;Int64&#34;</span><span class="p">),</span> <span class="p">(</span><span class="s2">&#34;age&#34;</span><span class="p">,</span> <span class="s2">&#34;Int64&#34;</span><span class="p">),</span> <span class="p">(</span><span class="s2">&#34;email&#34;</span><span class="p">,</span> <span class="s2">&#34;String&#34;</span><span class="p">),</span> <span class="p">(</span><span class="s2">&#34;score&#34;</span><span class="p">,</span> <span class="s2">&#34;Float64&#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">validation</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">pb</span><span class="o">.</span><span class="n">Validate</span><span class="p">(</span><span class="n">data</span><span class="o">=</span><span class="n">user_data</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="s2">&#34;An example.&#34;</span><span class="p">,</span> <span class="n">tbl_name</span><span class="o">=</span><span class="s2">&#34;users&#34;</span><span class="p">,</span> <span class="n">thresholds</span><span class="o">=</span><span class="p">(</span><span class="mf">0.1</span><span class="p">,</span> <span class="mf">0.2</span><span class="p">,</span> <span class="mf">0.3</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">col_vals_gt</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;user_id&#34;</span><span class="p">,</span> <span class="n">value</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">col_vals_between</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;age&#34;</span><span class="p">,</span> <span class="n">left</span><span class="o">=</span><span class="mi">18</span><span class="p">,</span> <span class="n">right</span><span class="o">=</span><span class="mi">80</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">col_vals_regex</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;email&#34;</span><span class="p">,</span> <span class="n">pattern</span><span class="o">=</span><span class="sa">r</span><span class="s2">&#34;^[^@]+@[^@]+\.[^@]+$&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">col_vals_between</span><span class="p">(</span><span class="n">columns</span><span class="o">=</span><span class="s2">&#34;score&#34;</span><span class="p">,</span> <span class="n">left</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">right</span><span class="o">=</span><span class="mi">100</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">col_schema_match</span><span class="p">(</span><span class="n">schema</span><span class="o">=</span><span class="n">schema</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">interrogate</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">validation</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div id="pb_tbl" 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+Mono&display=swap');
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans&display=swap');
#pb_tbl 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>#pb_tbl thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#pb_tbl p { margin: 0; padding: 0; }
#pb_tbl .gt_table { display: table; border-collapse: collapse; line-height: normal; margin-left: auto; margin-right: auto; color: #333333; font-size: 90%; 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; }
#pb_tbl .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#pb_tbl .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; }
#pb_tbl .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; }
#pb_tbl .gt_heading { background-color: #FFFFFF; text-align: left; 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; }
#pb_tbl .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#pb_tbl .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; }
#pb_tbl .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; }
#pb_tbl .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; }
#pb_tbl .gt_column_spanner_outer:first-child { padding-left: 0; }
#pb_tbl .gt_column_spanner_outer:last-child { padding-right: 0; }
#pb_tbl .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%; }
#pb_tbl .gt_spanner_row { border-bottom-style: hidden; }
#pb_tbl .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; }
#pb_tbl .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; }
#pb_tbl .gt_from_md&gt; :first-child { margin-top: 0; }
#pb_tbl .gt_from_md&gt; :last-child { margin-bottom: 0; }
#pb_tbl .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; }
#pb_tbl .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; }
#pb_tbl .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; }
#pb_tbl .gt_row_group_first td { border-top-width: 2px; }
#pb_tbl .gt_row_group_first th { border-top-width: 2px; }
#pb_tbl .gt_striped { color: #333333; background-color: #F4F4F4; }
#pb_tbl .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; }
#pb_tbl .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#pb_tbl .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#pb_tbl .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#pb_tbl .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; }
#pb_tbl .gt_sourcenote { font-size: 90%; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#pb_tbl .gt_left { text-align: left; }
#pb_tbl .gt_center { text-align: center; }
#pb_tbl .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#pb_tbl .gt_font_normal { font-weight: normal; }
#pb_tbl .gt_font_bold { font-weight: bold; }
#pb_tbl .gt_font_italic { font-style: italic; }
#pb_tbl .gt_super { font-size: 65%; }
#pb_tbl .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#pb_tbl .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
<p></style></p>
<table style="table-layout: fixed;; width: 0px" class="gt_table" data-quarto-disable-processing="true" data-quarto-bootstrap="false">
<colgroup>
  <col style="width:4px;"/>
  <col style="width:35px;"/>
  <col style="width:190px;"/>
  <col style="width:120px;"/>
  <col style="width:120px;"/>
  <col style="width:50px;"/>
  <col style="width:50px;"/>
  <col style="width:60px;"/>
  <col style="width:60px;"/>
  <col style="width:60px;"/>
  <col style="width:30px;"/>
  <col style="width:30px;"/>
  <col style="width:30px;"/>
  <col style="width:65px;"/>
</colgroup>
<thead>
  <tr class="gt_heading">
    <td colspan="14" class="gt_heading gt_title gt_font_normal" style="color: #444444;font-size: 28px;text-align: left;font-weight: bold; text-align: left;">Pointblank Validation</td>
  </tr>
  <tr class="gt_heading">
    <td colspan="14" class="gt_heading gt_subtitle gt_font_normal gt_bottom_border" style="text-align: left;"><div><span style='text-decoration-style: solid; text-decoration-color: #ADD8E6; text-decoration-line: underline; text-underline-position: under; color: #333333; font-variant-numeric: tabular-nums; padding-left: 4px; margin-right: 5px; padding-right: 2px;'>An example.</span><div style="padding-top: 10px; padding-bottom: 5px;"><span style='background-color: #0075FF; color: #FFFFFF; padding: 0.5em 0.5em; position: inherit; text-transform: uppercase; margin: 5px 0px 5px 0px; border: solid 1px #0075FF; font-weight: bold; padding: 2px 15px 2px 15px; font-size: 10px;'>Polars</span><span style='background-color: none; color: #222222; padding: 0.5em 0.5em; position: inherit; margin: 5px 10px 5px -4px; border: solid 1px #0075FF; font-weight: bold; padding: 2px 15px 2px 15px; font-size: 10px;'>users</span><span><span style="background-color: #AAAAAA; color: white; padding: 0.5em 0.5em; position: inherit; text-transform: uppercase; margin: 5px 0px 5px 5px; border: solid 1px #AAAAAA; font-weight: bold; padding: 2px 15px 2px 15px; font-size: smaller;">WARNING</span><span style="background-color: none; color: #333333; padding: 0.5em 0.5em; position: inherit; margin: 5px 0px 5px -4px; font-weight: bold; border: solid 1px #AAAAAA; padding: 2px 15px 2px 15px; font-size: smaller; margin-right: 5px;">0.1</span><span style="background-color: #EBBC14; color: white; padding: 0.5em 0.5em; position: inherit; text-transform: uppercase; margin: 5px 0px 5px 1px; border: solid 1px #EBBC14; font-weight: bold; padding: 2px 15px 2px 15px; font-size: smaller;">ERROR</span><span style="background-color: none; color: #333333; padding: 0.5em 0.5em; position: inherit; margin: 5px 0px 5px -4px; font-weight: bold; border: solid 1px #EBBC14; padding: 2px 15px 2px 15px; font-size: smaller; margin-right: 5px;">0.2</span><span style="background-color: #FF3300; color: white; padding: 0.5em 0.5em; position: inherit; text-transform: uppercase; margin: 5px 0px 5px 1px; border: solid 1px #FF3300; font-weight: bold; padding: 2px 15px 2px 15px; font-size: smaller;">CRITICAL</span><span style="background-color: none; color: #333333; padding: 0.5em 0.5em; position: inherit; margin: 5px 0px 5px -4px; font-weight: bold; border: solid 1px #FF3300; padding: 2px 15px 2px 15px; font-size: smaller;">0.3</span></span></div></div></td>
  </tr>
<tr class="gt_col_headings">
  <th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-status_color"></th>
  <th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-i"></th>
  <th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-type_upd">STEP</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-columns_upd">COLUMNS</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-values_upd">VALUES</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_center" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-tbl">TBL</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_center" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-eval">EVAL</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-test_units">UNITS</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-pass">PASS</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-fail">FAIL</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_center" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-w_upd">W</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_center" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-e_upd">E</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_center" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-c_upd">C</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_center" rowspan="1" colspan="1" style="color: #666666;font-weight: bold;" scope="col" id="pb_tbl-extract_upd">EXT</th>
</tr>
</thead>
<tbody class="gt_table_body">
  <tr>
    <td style="height: 40px; background-color: #4CA64C; color: transparent;font-size: 0px;" class="gt_row gt_left">#4CA64C</td>
    <td style="height: 40px; color: #666666;font-size: 13px;font-weight: bold;" class="gt_row gt_right">1</td>
    <td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px;" class="gt_row gt_left">
        <div style="margin: 0; padding: 0; display: inline-block; height: 30px; vertical-align: middle; width: 16%;">
            <!--?xml version="1.0" encoding="UTF-8"?--><?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 67 67" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="background: #FFFFFF;">
    <title>col_vals_gt</title>
    <g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="col_vals_gt" transform="translate(1.500000, 1.500000)" fill-rule="nonzero">
            <path d="M55,0 C57.4852813,0 59.7352813,1.00735931 61.363961,2.63603897 C62.9926407,4.26471863 64,6.51471863 64,9 L64,9 L64,64 L9,64 C6.51471862,64 4.26471862,62.9926407 2.63603897,61.363961 C1.00735931,59.7352814 0,57.4852814 0,55 L0,55 L0,9 C0,6.51471863 1.00735931,4.26471863 2.63603897,2.63603897 C4.26471862,1.00735931 6.51471862,0 9,0 L9,0 L55,0 Z" id="rectangle" stroke="#000000" stroke-width="2" fill="#FFFFFF"></path>
            <path d="M48.7619048,10 L15.2380953,10 C12.3466667,10 10,12.3466667 10,15.2380952 L10,48.7619048 C10,51.6533333 12.3466667,54 15.2380953,54 L48.7619048,54 C51.6533333,54 54,51.6533333 54,48.7619048 L54,15.2380952 C54,12.3466667 51.6533333,10 48.7619048,10 Z M25.2638095,44.3828571 L24.0695238,42.6647619 L39.5847619,32 L24.0695238,21.3352381 L25.2638095,19.6171429 L43.2828572,32 L25.2638095,44.3828571 Z" id="greater_than" fill="#000000"></path>
        </g>
    </g>
</svg>
        </div>
        <div style="font-family: 'IBM Plex Mono', monospace, courier; color: black; font-size: 11px; display: inline-block; vertical-align: middle;">
            <div>col_vals_gt()</div>
        </div>
<pre><code>    &lt;/td&gt;
&lt;td style=&quot;height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;&quot; class=&quot;gt_row gt_left&quot;&gt;user_id&lt;/td&gt;
&lt;td style=&quot;height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;&quot; class=&quot;gt_row gt_left&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;height: 40px; background-color: #FCFCFC; border-left: 1px solid #D3D3D3;&quot; class=&quot;gt_row gt_center&quot;&gt;&lt;svg width=&quot;25px&quot; height=&quot;25px&quot; viewBox=&quot;0 0 25 25&quot; version=&quot;1.1&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; style=&quot;vertical-align: middle;&quot;&gt;
&lt;g id=&quot;unchanged&quot; stroke=&quot;none&quot; stroke-width=&quot;1&quot; fill=&quot;none&quot; fill-rule=&quot;evenodd&quot;&gt;
    &lt;g id=&quot;unchanged&quot; transform=&quot;translate(0.500000, 0.570147)&quot;&gt;
        &lt;rect id=&quot;Rectangle&quot; x=&quot;0.125132506&quot; y=&quot;0&quot; width=&quot;23.749735&quot; height=&quot;23.7894737&quot;&gt;&lt;/rect&gt;
        &lt;path d=&quot;M5.80375046,8.18194736 C3.77191832,8.18194736 2.11875046,9.83495328 2.11875046,11.8669474 C2.11875046,13.8989414 3.77191832,15.5519474 5.80375046,15.5519474 C7.8355826,15.5519474 9.48875046,13.8989414 9.48875046,11.8669474 C9.48875046,9.83495328 7.83552863,8.18194736 5.80375046,8.18194736 Z M5.80375046,14.814915 C4.17821997,14.814915 2.85578285,13.4924778 2.85578285,11.8669474 C2.85578285,10.2414169 4.17821997,8.91897975 5.80375046,8.91897975 C7.42928095,8.91897975 8.75171807,10.2414169 8.75171807,11.8669474 C8.75171807,13.4924778 7.42928095,14.814915 5.80375046,14.814915 Z&quot; id=&quot;Shape&quot; fill=&quot;#000000&quot; fill-rule=&quot;nonzero&quot;&gt;&lt;/path&gt;
        &lt;path d=&quot;M13.9638189,8.699335 C13.9364621,8.70430925 13.9091059,8.71176968 13.8842359,8.71923074 C13.7822704,8.73663967 13.6877654,8.77643115 13.6056956,8.83860518 L10.2433156,11.3852598 C10.0766886,11.5046343 9.97720993,11.6986181 9.97720993,11.9025491 C9.97720993,12.1064807 10.0766886,12.3004639 10.2433156,12.4198383 L13.6056956,14.966493 C13.891697,15.1803725 14.2970729,15.1231721 14.5109517,14.8371707 C14.7248313,14.5511692 14.6676309,14.145794 14.3816294,13.9319145 L12.5313257,12.5392127 L21.8812495,12.5392127 L21.8812495,11.2658854 L12.5313257,11.2658854 L14.3816294,9.87318364 C14.6377872,9.71650453 14.7497006,9.40066014 14.6477351,9.11714553 C14.5482564,8.83363156 14.262255,8.65954352 13.9638189,8.699335 Z&quot; id=&quot;arrow&quot; fill=&quot;#000000&quot; transform=&quot;translate(15.929230, 11.894737) rotate(-180.000000) translate(-15.929230, -11.894737) &quot;&gt;&lt;/path&gt;
    &lt;/g&gt;
&lt;/g&gt;
</code></pre>
<p></svg></td>
<td style="height: 40px; background-color: #FCFCFC; border-right: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color:#4CA64C;">✓</span></td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px;" class="gt_row gt_right">5</td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5;" class="gt_row gt_right">5<br />1.00</td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5;" class="gt_row gt_right">0<br />0.00</td>
<td style="height: 40px; background-color: #FCFCFC; border-left: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color: #AAAAAA;">○</span></td>
<td style="height: 40px; background-color: #FCFCFC;" class="gt_row gt_center"><span style="color: #EBBC14;">○</span></td>
<td style="height: 40px; background-color: #FCFCFC; border-right: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color: #FF3300;">○</span></td>
<td style="height: 40px;" class="gt_row gt_center">—</td></p>
  </tr>
  <tr>
    <td style="height: 40px; background-color: #EBBC14; color: transparent;font-size: 0px;" class="gt_row gt_left">#EBBC14</td>
    <td style="height: 40px; color: #666666;font-size: 13px;font-weight: bold;" class="gt_row gt_right">2</td>
    <td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px;" class="gt_row gt_left">
        <div style="margin: 0; padding: 0; display: inline-block; height: 30px; vertical-align: middle; width: 16%;">
            <!--?xml version="1.0" encoding="UTF-8"?--><?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 67 67" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>col_vals_between</title>
    <g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="col_vals_between" transform="translate(0.000000, 0.206897)">
            <path d="M56.712234,1 C59.1975153,1 61.4475153,2.00735931 63.076195,3.63603897 C64.7048747,5.26471863 65.712234,7.51471863 65.712234,10 L65.712234,10 L65.712234,65 L10.712234,65 C8.22695259,65 5.97695259,63.9926407 4.34827294,62.363961 C2.71959328,60.7352814 1.71223397,58.4852814 1.71223397,56 L1.71223397,56 L1.71223397,10 C1.71223397,7.51471863 2.71959328,5.26471863 4.34827294,3.63603897 C5.97695259,2.00735931 8.22695259,1 10.712234,1 L10.712234,1 Z" id="rectangle" stroke="#000000" stroke-width="2" fill="#FFFFFF"></path>
            <path d="M11.993484,21.96875 C10.962234,22.082031 10.188797,22.964844 10.212234,24 L10.212234,42 C10.200515,42.722656 10.579422,43.390625 11.204422,43.753906 C11.825515,44.121094 12.598953,44.121094 13.220047,43.753906 C13.845047,43.390625 14.223953,42.722656 14.212234,42 L14.212234,24 C14.220047,23.457031 14.009109,22.9375 13.626297,22.554688 C13.243484,22.171875 12.723953,21.960938 12.180984,21.96875 C12.118484,21.964844 12.055984,21.964844 11.993484,21.96875 Z M55.993484,21.96875 C54.962234,22.082031 54.188797,22.964844 54.212234,24 L54.212234,42 C54.200515,42.722656 54.579422,43.390625 55.204422,43.753906 C55.825515,44.121094 56.598953,44.121094 57.220047,43.753906 C57.845047,43.390625 58.223953,42.722656 58.212234,42 L58.212234,24 C58.220047,23.457031 58.009109,22.9375 57.626297,22.554688 C57.243484,22.171875 56.723953,21.960938 56.180984,21.96875 C56.118484,21.964844 56.055984,21.964844 55.993484,21.96875 Z M16.212234,22 C15.661453,22 15.212234,22.449219 15.212234,23 C15.212234,23.550781 15.661453,24 16.212234,24 C16.763015,24 17.212234,23.550781 17.212234,23 C17.212234,22.449219 16.763015,22 16.212234,22 Z M20.212234,22 C19.661453,22 19.212234,22.449219 19.212234,23 C19.212234,23.550781 19.661453,24 20.212234,24 C20.763015,24 21.212234,23.550781 21.212234,23 C21.212234,22.449219 20.763015,22 20.212234,22 Z M24.212234,22 C23.661453,22 23.212234,22.449219 23.212234,23 C23.212234,23.550781 23.661453,24 24.212234,24 C24.763015,24 25.212234,23.550781 25.212234,23 C25.212234,22.449219 24.763015,22 24.212234,22 Z M28.212234,22 C27.661453,22 27.212234,22.449219 27.212234,23 C27.212234,23.550781 27.661453,24 28.212234,24 C28.763015,24 29.212234,23.550781 29.212234,23 C29.212234,22.449219 28.763015,22 28.212234,22 Z M32.212234,22 C31.661453,22 31.212234,22.449219 31.212234,23 C31.212234,23.550781 31.661453,24 32.212234,24 C32.763015,24 33.212234,23.550781 33.212234,23 C33.212234,22.449219 32.763015,22 32.212234,22 Z M36.212234,22 C35.661453,22 35.212234,22.449219 35.212234,23 C35.212234,23.550781 35.661453,24 36.212234,24 C36.763015,24 37.212234,23.550781 37.212234,23 C37.212234,22.449219 36.763015,22 36.212234,22 Z M40.212234,22 C39.661453,22 39.212234,22.449219 39.212234,23 C39.212234,23.550781 39.661453,24 40.212234,24 C40.763015,24 41.212234,23.550781 41.212234,23 C41.212234,22.449219 40.763015,22 40.212234,22 Z M44.212234,22 C43.661453,22 43.212234,22.449219 43.212234,23 C43.212234,23.550781 43.661453,24 44.212234,24 C44.763015,24 45.212234,23.550781 45.212234,23 C45.212234,22.449219 44.763015,22 44.212234,22 Z M48.212234,22 C47.661453,22 47.212234,22.449219 47.212234,23 C47.212234,23.550781 47.661453,24 48.212234,24 C48.763015,24 49.212234,23.550781 49.212234,23 C49.212234,22.449219 48.763015,22 48.212234,22 Z M52.212234,22 C51.661453,22 51.212234,22.449219 51.212234,23 C51.212234,23.550781 51.661453,24 52.212234,24 C52.763015,24 53.212234,23.550781 53.212234,23 C53.212234,22.449219 52.763015,22 52.212234,22 Z M21.462234,27.96875 C21.419265,27.976563 21.376297,27.988281 21.337234,28 C21.177078,28.027344 21.02864,28.089844 20.899734,28.1875 L15.618484,32.1875 C15.356765,32.375 15.200515,32.679688 15.200515,33 C15.200515,33.320313 15.356765,33.625 15.618484,33.8125 L20.899734,37.8125 C21.348953,38.148438 21.985672,38.058594 22.321609,37.609375 C22.657547,37.160156 22.567703,36.523438 22.118484,36.1875 L19.212234,34 L49.212234,34 L46.305984,36.1875 C45.856765,36.523438 45.766922,37.160156 46.102859,37.609375 C46.438797,38.058594 47.075515,38.148438 47.524734,37.8125 L52.805984,33.8125 C53.067703,33.625 53.223953,33.320313 53.223953,33 C53.223953,32.679688 53.067703,32.375 52.805984,32.1875 L47.524734,28.1875 C47.30989,28.027344 47.040359,27.960938 46.774734,28 C46.743484,28 46.712234,28 46.680984,28 C46.282547,28.074219 45.96614,28.382813 45.884109,28.78125 C45.802078,29.179688 45.970047,29.585938 46.305984,29.8125 L49.212234,32 L19.212234,32 L22.118484,29.8125 C22.520828,29.566406 22.696609,29.070313 22.536453,28.625 C22.380203,28.179688 21.930984,27.90625 21.462234,27.96875 Z M16.212234,42 C15.661453,42 15.212234,42.449219 15.212234,43 C15.212234,43.550781 15.661453,44 16.212234,44 C16.763015,44 17.212234,43.550781 17.212234,43 C17.212234,42.449219 16.763015,42 16.212234,42 Z M20.212234,42 C19.661453,42 19.212234,42.449219 19.212234,43 C19.212234,43.550781 19.661453,44 20.212234,44 C20.763015,44 21.212234,43.550781 21.212234,43 C21.212234,42.449219 20.763015,42 20.212234,42 Z M24.212234,42 C23.661453,42 23.212234,42.449219 23.212234,43 C23.212234,43.550781 23.661453,44 24.212234,44 C24.763015,44 25.212234,43.550781 25.212234,43 C25.212234,42.449219 24.763015,42 24.212234,42 Z M28.212234,42 C27.661453,42 27.212234,42.449219 27.212234,43 C27.212234,43.550781 27.661453,44 28.212234,44 C28.763015,44 29.212234,43.550781 29.212234,43 C29.212234,42.449219 28.763015,42 28.212234,42 Z M32.212234,42 C31.661453,42 31.212234,42.449219 31.212234,43 C31.212234,43.550781 31.661453,44 32.212234,44 C32.763015,44 33.212234,43.550781 33.212234,43 C33.212234,42.449219 32.763015,42 32.212234,42 Z M36.212234,42 C35.661453,42 35.212234,42.449219 35.212234,43 C35.212234,43.550781 35.661453,44 36.212234,44 C36.763015,44 37.212234,43.550781 37.212234,43 C37.212234,42.449219 36.763015,42 36.212234,42 Z M40.212234,42 C39.661453,42 39.212234,42.449219 39.212234,43 C39.212234,43.550781 39.661453,44 40.212234,44 C40.763015,44 41.212234,43.550781 41.212234,43 C41.212234,42.449219 40.763015,42 40.212234,42 Z M44.212234,42 C43.661453,42 43.212234,42.449219 43.212234,43 C43.212234,43.550781 43.661453,44 44.212234,44 C44.763015,44 45.212234,43.550781 45.212234,43 C45.212234,42.449219 44.763015,42 44.212234,42 Z M48.212234,42 C47.661453,42 47.212234,42.449219 47.212234,43 C47.212234,43.550781 47.661453,44 48.212234,44 C48.763015,44 49.212234,43.550781 49.212234,43 C49.212234,42.449219 48.763015,42 48.212234,42 Z M52.212234,42 C51.661453,42 51.212234,42.449219 51.212234,43 C51.212234,43.550781 51.661453,44 52.212234,44 C52.763015,44 53.212234,43.550781 53.212234,43 C53.212234,42.449219 52.763015,42 52.212234,42 Z" id="inside_range" fill="#000000" fill-rule="nonzero"></path>
        </g>
    </g>
</svg>
        </div>
        <div style="font-family: 'IBM Plex Mono', monospace, courier; color: black; font-size: 10px; display: inline-block; vertical-align: middle;">
            <div>col_vals_between()</div>
        </div>
<pre><code>    &lt;/td&gt;
&lt;td style=&quot;height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;&quot; class=&quot;gt_row gt_left&quot;&gt;age&lt;/td&gt;
&lt;td style=&quot;height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;&quot; class=&quot;gt_row gt_left&quot;&gt;[18, 80]&lt;/td&gt;
&lt;td style=&quot;height: 40px; background-color: #FCFCFC; border-left: 1px solid #D3D3D3;&quot; class=&quot;gt_row gt_center&quot;&gt;&lt;svg width=&quot;25px&quot; height=&quot;25px&quot; viewBox=&quot;0 0 25 25&quot; version=&quot;1.1&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; style=&quot;vertical-align: middle;&quot;&gt;
&lt;g id=&quot;unchanged&quot; stroke=&quot;none&quot; stroke-width=&quot;1&quot; fill=&quot;none&quot; fill-rule=&quot;evenodd&quot;&gt;
    &lt;g id=&quot;unchanged&quot; transform=&quot;translate(0.500000, 0.570147)&quot;&gt;
        &lt;rect id=&quot;Rectangle&quot; x=&quot;0.125132506&quot; y=&quot;0&quot; width=&quot;23.749735&quot; height=&quot;23.7894737&quot;&gt;&lt;/rect&gt;
        &lt;path d=&quot;M5.80375046,8.18194736 C3.77191832,8.18194736 2.11875046,9.83495328 2.11875046,11.8669474 C2.11875046,13.8989414 3.77191832,15.5519474 5.80375046,15.5519474 C7.8355826,15.5519474 9.48875046,13.8989414 9.48875046,11.8669474 C9.48875046,9.83495328 7.83552863,8.18194736 5.80375046,8.18194736 Z M5.80375046,14.814915 C4.17821997,14.814915 2.85578285,13.4924778 2.85578285,11.8669474 C2.85578285,10.2414169 4.17821997,8.91897975 5.80375046,8.91897975 C7.42928095,8.91897975 8.75171807,10.2414169 8.75171807,11.8669474 C8.75171807,13.4924778 7.42928095,14.814915 5.80375046,14.814915 Z&quot; id=&quot;Shape&quot; fill=&quot;#000000&quot; fill-rule=&quot;nonzero&quot;&gt;&lt;/path&gt;
        &lt;path d=&quot;M13.9638189,8.699335 C13.9364621,8.70430925 13.9091059,8.71176968 13.8842359,8.71923074 C13.7822704,8.73663967 13.6877654,8.77643115 13.6056956,8.83860518 L10.2433156,11.3852598 C10.0766886,11.5046343 9.97720993,11.6986181 9.97720993,11.9025491 C9.97720993,12.1064807 10.0766886,12.3004639 10.2433156,12.4198383 L13.6056956,14.966493 C13.891697,15.1803725 14.2970729,15.1231721 14.5109517,14.8371707 C14.7248313,14.5511692 14.6676309,14.145794 14.3816294,13.9319145 L12.5313257,12.5392127 L21.8812495,12.5392127 L21.8812495,11.2658854 L12.5313257,11.2658854 L14.3816294,9.87318364 C14.6377872,9.71650453 14.7497006,9.40066014 14.6477351,9.11714553 C14.5482564,8.83363156 14.262255,8.65954352 13.9638189,8.699335 Z&quot; id=&quot;arrow&quot; fill=&quot;#000000&quot; transform=&quot;translate(15.929230, 11.894737) rotate(-180.000000) translate(-15.929230, -11.894737) &quot;&gt;&lt;/path&gt;
    &lt;/g&gt;
&lt;/g&gt;
</code></pre>
<p></svg></td>
<td style="height: 40px; background-color: #FCFCFC; border-right: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color:#4CA64C;">✓</span></td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px;" class="gt_row gt_right">5</td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5;" class="gt_row gt_right">4<br />0.80</td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5;" class="gt_row gt_right">1<br />0.20</td>
<td style="height: 40px; background-color: #FCFCFC; border-left: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color: #AAAAAA;">●</span></td>
<td style="height: 40px; background-color: #FCFCFC;" class="gt_row gt_center"><span style="color: #EBBC14;">●</span></td>
<td style="height: 40px; background-color: #FCFCFC; border-right: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color: #FF3300;">○</span></td>
<td style="height: 40px;" class="gt_row gt_center"><a href="data:text/csv;base64,X3Jvd19udW1fLHVzZXJfaWQsYWdlLGVtYWlsLHNjb3JlCjUsNSw5NSx1c2VyNUBleGFtcGxlLmNvbSw5NS4yCg==" download="extract_0002.csv"><button style="background-color: #67C2DC; color: #FFFFFF; border: none; padding: 5px; font-weight: bold; cursor: pointer; border-radius: 4px;">CSV</button></a></td></p>
  </tr>
  <tr>
    <td style="height: 40px; background-color: #EBBC14; color: transparent;font-size: 0px;" class="gt_row gt_left">#EBBC14</td>
    <td style="height: 40px; color: #666666;font-size: 13px;font-weight: bold;" class="gt_row gt_right">3</td>
    <td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px;" class="gt_row gt_left">
        <div style="margin: 0; padding: 0; display: inline-block; height: 30px; vertical-align: middle; width: 16%;">
            <!--?xml version="1.0" encoding="UTF-8"?--><?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 67 67" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>col_vals_regex</title>
    <g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="col_vals_regex" transform="translate(0.000000, 0.034483)">
            <path d="M56.712234,1 C59.1975153,1 61.4475153,2.00735931 63.076195,3.63603897 C64.7048747,5.26471863 65.712234,7.51471863 65.712234,10 L65.712234,10 L65.712234,65 L10.712234,65 C8.22695259,65 5.97695259,63.9926407 4.34827294,62.363961 C2.71959328,60.7352814 1.71223397,58.4852814 1.71223397,56 L1.71223397,56 L1.71223397,10 C1.71223397,7.51471863 2.71959328,5.26471863 4.34827294,3.63603897 C5.97695259,2.00735931 8.22695259,1 10.712234,1 L10.712234,1 Z" id="rectangle" stroke="#000000" stroke-width="2" fill="#FFFFFF"></path>
            <g id="regex_symbols" transform="translate(18.000000, 12.000000)" fill="#000000" fill-rule="nonzero">
                <path d="M4.17434508,33.013582 C1.94895328,33.013582 0.138006923,34.8245284 0.138006923,37.0499202 C0.138006923,39.275312 1.94895328,41.0862583 4.17434508,41.0862583 C6.39973688,41.0862583 8.21068324,39.275312 8.21068324,37.0499202 C8.21068324,34.8245284 6.39973688,33.013582 4.17434508,33.013582 Z" id="full_stop"></path>
                <path d="M23.9479718,23.3175402 L21.5628264,23.3175402 C21.2344032,23.3175402 20.9665401,23.0520067 20.9665401,22.7212538 L20.9665401,15.1022979 L14.3445004,18.8873192 C14.0626621,19.050366 13.7016292,18.952538 13.5362533,18.6706991 L12.3436806,16.6442575 C12.262157,16.506832 12.2388642,16.3437852 12.2807909,16.1900549 C12.3203879,16.0363251 12.4205455,15.9058874 12.557971,15.8266929 L19.1800101,11.9880994 L12.557971,8.15183511 C12.4205455,8.07264112 12.3203879,7.93987439 12.2807909,7.78614401 C12.2388642,7.63241423 12.262157,7.46936689 12.3413509,7.33194137 L13.5339237,5.30549975 C13.6993001,5.02366143 14.0626621,4.92816199 14.3445004,5.09120934 L20.9665401,8.87390091 L20.9665401,1.25494501 C20.9665401,0.926521818 21.2344032,0.658658658 21.5628264,0.658658658 L23.9479718,0.658658658 C24.2787247,0.658658658 24.5442582,0.926521818 24.5442582,1.25494501 L24.5442582,8.87390091 L31.1662979,5.09120934 C31.4481362,4.92816199 31.8091691,5.02366143 31.9745455,5.30549975 L33.1671182,7.33194137 C33.2486413,7.46936689 33.2719341,7.63241423 33.2300074,7.78614401 C33.1904104,7.93987439 33.0902528,8.07264112 32.9528278,8.15183511 L26.3307882,11.9880994 L32.9528278,15.8243638 C33.0879237,15.9058874 33.1880813,16.0363251 33.2300074,16.1900549 C33.269605,16.3437852 33.2486413,16.506832 33.1671182,16.6442575 L31.9745455,18.6706991 C31.8091691,18.952538 31.4481362,19.050366 31.1662979,18.8849895 L24.5442582,15.1022979 L24.5442582,22.7212538 C24.5442582,23.0520067 24.2787247,23.3175402 23.9479718,23.3175402 Z" id="asterisk"></path>
            </g>
        </g>
    </g>
</svg>
        </div>
        <div style="font-family: 'IBM Plex Mono', monospace, courier; color: black; font-size: 11px; display: inline-block; vertical-align: middle;">
            <div>col_vals_regex()</div>
        </div>
<pre><code>    &lt;/td&gt;
&lt;td style=&quot;height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;&quot; class=&quot;gt_row gt_left&quot;&gt;email&lt;/td&gt;
&lt;td style=&quot;height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;&quot; class=&quot;gt_row gt_left&quot;&gt;^[^@]+@[^@]+\.[^@]+$&lt;/td&gt;
&lt;td style=&quot;height: 40px; background-color: #FCFCFC; border-left: 1px solid #D3D3D3;&quot; class=&quot;gt_row gt_center&quot;&gt;&lt;svg width=&quot;25px&quot; height=&quot;25px&quot; viewBox=&quot;0 0 25 25&quot; version=&quot;1.1&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; style=&quot;vertical-align: middle;&quot;&gt;
&lt;g id=&quot;unchanged&quot; stroke=&quot;none&quot; stroke-width=&quot;1&quot; fill=&quot;none&quot; fill-rule=&quot;evenodd&quot;&gt;
    &lt;g id=&quot;unchanged&quot; transform=&quot;translate(0.500000, 0.570147)&quot;&gt;
        &lt;rect id=&quot;Rectangle&quot; x=&quot;0.125132506&quot; y=&quot;0&quot; width=&quot;23.749735&quot; height=&quot;23.7894737&quot;&gt;&lt;/rect&gt;
        &lt;path d=&quot;M5.80375046,8.18194736 C3.77191832,8.18194736 2.11875046,9.83495328 2.11875046,11.8669474 C2.11875046,13.8989414 3.77191832,15.5519474 5.80375046,15.5519474 C7.8355826,15.5519474 9.48875046,13.8989414 9.48875046,11.8669474 C9.48875046,9.83495328 7.83552863,8.18194736 5.80375046,8.18194736 Z M5.80375046,14.814915 C4.17821997,14.814915 2.85578285,13.4924778 2.85578285,11.8669474 C2.85578285,10.2414169 4.17821997,8.91897975 5.80375046,8.91897975 C7.42928095,8.91897975 8.75171807,10.2414169 8.75171807,11.8669474 C8.75171807,13.4924778 7.42928095,14.814915 5.80375046,14.814915 Z&quot; id=&quot;Shape&quot; fill=&quot;#000000&quot; fill-rule=&quot;nonzero&quot;&gt;&lt;/path&gt;
        &lt;path d=&quot;M13.9638189,8.699335 C13.9364621,8.70430925 13.9091059,8.71176968 13.8842359,8.71923074 C13.7822704,8.73663967 13.6877654,8.77643115 13.6056956,8.83860518 L10.2433156,11.3852598 C10.0766886,11.5046343 9.97720993,11.6986181 9.97720993,11.9025491 C9.97720993,12.1064807 10.0766886,12.3004639 10.2433156,12.4198383 L13.6056956,14.966493 C13.891697,15.1803725 14.2970729,15.1231721 14.5109517,14.8371707 C14.7248313,14.5511692 14.6676309,14.145794 14.3816294,13.9319145 L12.5313257,12.5392127 L21.8812495,12.5392127 L21.8812495,11.2658854 L12.5313257,11.2658854 L14.3816294,9.87318364 C14.6377872,9.71650453 14.7497006,9.40066014 14.6477351,9.11714553 C14.5482564,8.83363156 14.262255,8.65954352 13.9638189,8.699335 Z&quot; id=&quot;arrow&quot; fill=&quot;#000000&quot; transform=&quot;translate(15.929230, 11.894737) rotate(-180.000000) translate(-15.929230, -11.894737) &quot;&gt;&lt;/path&gt;
    &lt;/g&gt;
&lt;/g&gt;
</code></pre>
<p></svg></td>
<td style="height: 40px; background-color: #FCFCFC; border-right: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color:#4CA64C;">✓</span></td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px;" class="gt_row gt_right">5</td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5;" class="gt_row gt_right">4<br />0.80</td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5;" class="gt_row gt_right">1<br />0.20</td>
<td style="height: 40px; background-color: #FCFCFC; border-left: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color: #AAAAAA;">●</span></td>
<td style="height: 40px; background-color: #FCFCFC;" class="gt_row gt_center"><span style="color: #EBBC14;">●</span></td>
<td style="height: 40px; background-color: #FCFCFC; border-right: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color: #FF3300;">○</span></td>
<td style="height: 40px;" class="gt_row gt_center"><a href="data:text/csv;base64,X3Jvd19udW1fLHVzZXJfaWQsYWdlLGVtYWlsLHNjb3JlCjMsMywyMixpbnZhbGlkLWVtYWlsLDc4LjMK" download="extract_0003.csv"><button style="background-color: #67C2DC; color: #FFFFFF; border: none; padding: 5px; font-weight: bold; cursor: pointer; border-radius: 4px;">CSV</button></a></td></p>
  </tr>
  <tr>
    <td style="height: 40px; background-color: #4CA64C; color: transparent;font-size: 0px;" class="gt_row gt_left">#4CA64C</td>
    <td style="height: 40px; color: #666666;font-size: 13px;font-weight: bold;" class="gt_row gt_right">4</td>
    <td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px;" class="gt_row gt_left">
        <div style="margin: 0; padding: 0; display: inline-block; height: 30px; vertical-align: middle; width: 16%;">
            <!--?xml version="1.0" encoding="UTF-8"?--><?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 67 67" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>col_vals_between</title>
    <g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="col_vals_between" transform="translate(0.000000, 0.206897)">
            <path d="M56.712234,1 C59.1975153,1 61.4475153,2.00735931 63.076195,3.63603897 C64.7048747,5.26471863 65.712234,7.51471863 65.712234,10 L65.712234,10 L65.712234,65 L10.712234,65 C8.22695259,65 5.97695259,63.9926407 4.34827294,62.363961 C2.71959328,60.7352814 1.71223397,58.4852814 1.71223397,56 L1.71223397,56 L1.71223397,10 C1.71223397,7.51471863 2.71959328,5.26471863 4.34827294,3.63603897 C5.97695259,2.00735931 8.22695259,1 10.712234,1 L10.712234,1 Z" id="rectangle" stroke="#000000" stroke-width="2" fill="#FFFFFF"></path>
            <path d="M11.993484,21.96875 C10.962234,22.082031 10.188797,22.964844 10.212234,24 L10.212234,42 C10.200515,42.722656 10.579422,43.390625 11.204422,43.753906 C11.825515,44.121094 12.598953,44.121094 13.220047,43.753906 C13.845047,43.390625 14.223953,42.722656 14.212234,42 L14.212234,24 C14.220047,23.457031 14.009109,22.9375 13.626297,22.554688 C13.243484,22.171875 12.723953,21.960938 12.180984,21.96875 C12.118484,21.964844 12.055984,21.964844 11.993484,21.96875 Z M55.993484,21.96875 C54.962234,22.082031 54.188797,22.964844 54.212234,24 L54.212234,42 C54.200515,42.722656 54.579422,43.390625 55.204422,43.753906 C55.825515,44.121094 56.598953,44.121094 57.220047,43.753906 C57.845047,43.390625 58.223953,42.722656 58.212234,42 L58.212234,24 C58.220047,23.457031 58.009109,22.9375 57.626297,22.554688 C57.243484,22.171875 56.723953,21.960938 56.180984,21.96875 C56.118484,21.964844 56.055984,21.964844 55.993484,21.96875 Z M16.212234,22 C15.661453,22 15.212234,22.449219 15.212234,23 C15.212234,23.550781 15.661453,24 16.212234,24 C16.763015,24 17.212234,23.550781 17.212234,23 C17.212234,22.449219 16.763015,22 16.212234,22 Z M20.212234,22 C19.661453,22 19.212234,22.449219 19.212234,23 C19.212234,23.550781 19.661453,24 20.212234,24 C20.763015,24 21.212234,23.550781 21.212234,23 C21.212234,22.449219 20.763015,22 20.212234,22 Z M24.212234,22 C23.661453,22 23.212234,22.449219 23.212234,23 C23.212234,23.550781 23.661453,24 24.212234,24 C24.763015,24 25.212234,23.550781 25.212234,23 C25.212234,22.449219 24.763015,22 24.212234,22 Z M28.212234,22 C27.661453,22 27.212234,22.449219 27.212234,23 C27.212234,23.550781 27.661453,24 28.212234,24 C28.763015,24 29.212234,23.550781 29.212234,23 C29.212234,22.449219 28.763015,22 28.212234,22 Z M32.212234,22 C31.661453,22 31.212234,22.449219 31.212234,23 C31.212234,23.550781 31.661453,24 32.212234,24 C32.763015,24 33.212234,23.550781 33.212234,23 C33.212234,22.449219 32.763015,22 32.212234,22 Z M36.212234,22 C35.661453,22 35.212234,22.449219 35.212234,23 C35.212234,23.550781 35.661453,24 36.212234,24 C36.763015,24 37.212234,23.550781 37.212234,23 C37.212234,22.449219 36.763015,22 36.212234,22 Z M40.212234,22 C39.661453,22 39.212234,22.449219 39.212234,23 C39.212234,23.550781 39.661453,24 40.212234,24 C40.763015,24 41.212234,23.550781 41.212234,23 C41.212234,22.449219 40.763015,22 40.212234,22 Z M44.212234,22 C43.661453,22 43.212234,22.449219 43.212234,23 C43.212234,23.550781 43.661453,24 44.212234,24 C44.763015,24 45.212234,23.550781 45.212234,23 C45.212234,22.449219 44.763015,22 44.212234,22 Z M48.212234,22 C47.661453,22 47.212234,22.449219 47.212234,23 C47.212234,23.550781 47.661453,24 48.212234,24 C48.763015,24 49.212234,23.550781 49.212234,23 C49.212234,22.449219 48.763015,22 48.212234,22 Z M52.212234,22 C51.661453,22 51.212234,22.449219 51.212234,23 C51.212234,23.550781 51.661453,24 52.212234,24 C52.763015,24 53.212234,23.550781 53.212234,23 C53.212234,22.449219 52.763015,22 52.212234,22 Z M21.462234,27.96875 C21.419265,27.976563 21.376297,27.988281 21.337234,28 C21.177078,28.027344 21.02864,28.089844 20.899734,28.1875 L15.618484,32.1875 C15.356765,32.375 15.200515,32.679688 15.200515,33 C15.200515,33.320313 15.356765,33.625 15.618484,33.8125 L20.899734,37.8125 C21.348953,38.148438 21.985672,38.058594 22.321609,37.609375 C22.657547,37.160156 22.567703,36.523438 22.118484,36.1875 L19.212234,34 L49.212234,34 L46.305984,36.1875 C45.856765,36.523438 45.766922,37.160156 46.102859,37.609375 C46.438797,38.058594 47.075515,38.148438 47.524734,37.8125 L52.805984,33.8125 C53.067703,33.625 53.223953,33.320313 53.223953,33 C53.223953,32.679688 53.067703,32.375 52.805984,32.1875 L47.524734,28.1875 C47.30989,28.027344 47.040359,27.960938 46.774734,28 C46.743484,28 46.712234,28 46.680984,28 C46.282547,28.074219 45.96614,28.382813 45.884109,28.78125 C45.802078,29.179688 45.970047,29.585938 46.305984,29.8125 L49.212234,32 L19.212234,32 L22.118484,29.8125 C22.520828,29.566406 22.696609,29.070313 22.536453,28.625 C22.380203,28.179688 21.930984,27.90625 21.462234,27.96875 Z M16.212234,42 C15.661453,42 15.212234,42.449219 15.212234,43 C15.212234,43.550781 15.661453,44 16.212234,44 C16.763015,44 17.212234,43.550781 17.212234,43 C17.212234,42.449219 16.763015,42 16.212234,42 Z M20.212234,42 C19.661453,42 19.212234,42.449219 19.212234,43 C19.212234,43.550781 19.661453,44 20.212234,44 C20.763015,44 21.212234,43.550781 21.212234,43 C21.212234,42.449219 20.763015,42 20.212234,42 Z M24.212234,42 C23.661453,42 23.212234,42.449219 23.212234,43 C23.212234,43.550781 23.661453,44 24.212234,44 C24.763015,44 25.212234,43.550781 25.212234,43 C25.212234,42.449219 24.763015,42 24.212234,42 Z M28.212234,42 C27.661453,42 27.212234,42.449219 27.212234,43 C27.212234,43.550781 27.661453,44 28.212234,44 C28.763015,44 29.212234,43.550781 29.212234,43 C29.212234,42.449219 28.763015,42 28.212234,42 Z M32.212234,42 C31.661453,42 31.212234,42.449219 31.212234,43 C31.212234,43.550781 31.661453,44 32.212234,44 C32.763015,44 33.212234,43.550781 33.212234,43 C33.212234,42.449219 32.763015,42 32.212234,42 Z M36.212234,42 C35.661453,42 35.212234,42.449219 35.212234,43 C35.212234,43.550781 35.661453,44 36.212234,44 C36.763015,44 37.212234,43.550781 37.212234,43 C37.212234,42.449219 36.763015,42 36.212234,42 Z M40.212234,42 C39.661453,42 39.212234,42.449219 39.212234,43 C39.212234,43.550781 39.661453,44 40.212234,44 C40.763015,44 41.212234,43.550781 41.212234,43 C41.212234,42.449219 40.763015,42 40.212234,42 Z M44.212234,42 C43.661453,42 43.212234,42.449219 43.212234,43 C43.212234,43.550781 43.661453,44 44.212234,44 C44.763015,44 45.212234,43.550781 45.212234,43 C45.212234,42.449219 44.763015,42 44.212234,42 Z M48.212234,42 C47.661453,42 47.212234,42.449219 47.212234,43 C47.212234,43.550781 47.661453,44 48.212234,44 C48.763015,44 49.212234,43.550781 49.212234,43 C49.212234,42.449219 48.763015,42 48.212234,42 Z M52.212234,42 C51.661453,42 51.212234,42.449219 51.212234,43 C51.212234,43.550781 51.661453,44 52.212234,44 C52.763015,44 53.212234,43.550781 53.212234,43 C53.212234,42.449219 52.763015,42 52.212234,42 Z" id="inside_range" fill="#000000" fill-rule="nonzero"></path>
        </g>
    </g>
</svg>
        </div>
        <div style="font-family: 'IBM Plex Mono', monospace, courier; color: black; font-size: 10px; display: inline-block; vertical-align: middle;">
            <div>col_vals_between()</div>
        </div>
<pre><code>    &lt;/td&gt;
&lt;td style=&quot;height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;&quot; class=&quot;gt_row gt_left&quot;&gt;score&lt;/td&gt;
&lt;td style=&quot;height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;&quot; class=&quot;gt_row gt_left&quot;&gt;[0, 100]&lt;/td&gt;
&lt;td style=&quot;height: 40px; background-color: #FCFCFC; border-left: 1px solid #D3D3D3;&quot; class=&quot;gt_row gt_center&quot;&gt;&lt;svg width=&quot;25px&quot; height=&quot;25px&quot; viewBox=&quot;0 0 25 25&quot; version=&quot;1.1&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; style=&quot;vertical-align: middle;&quot;&gt;
&lt;g id=&quot;unchanged&quot; stroke=&quot;none&quot; stroke-width=&quot;1&quot; fill=&quot;none&quot; fill-rule=&quot;evenodd&quot;&gt;
    &lt;g id=&quot;unchanged&quot; transform=&quot;translate(0.500000, 0.570147)&quot;&gt;
        &lt;rect id=&quot;Rectangle&quot; x=&quot;0.125132506&quot; y=&quot;0&quot; width=&quot;23.749735&quot; height=&quot;23.7894737&quot;&gt;&lt;/rect&gt;
        &lt;path d=&quot;M5.80375046,8.18194736 C3.77191832,8.18194736 2.11875046,9.83495328 2.11875046,11.8669474 C2.11875046,13.8989414 3.77191832,15.5519474 5.80375046,15.5519474 C7.8355826,15.5519474 9.48875046,13.8989414 9.48875046,11.8669474 C9.48875046,9.83495328 7.83552863,8.18194736 5.80375046,8.18194736 Z M5.80375046,14.814915 C4.17821997,14.814915 2.85578285,13.4924778 2.85578285,11.8669474 C2.85578285,10.2414169 4.17821997,8.91897975 5.80375046,8.91897975 C7.42928095,8.91897975 8.75171807,10.2414169 8.75171807,11.8669474 C8.75171807,13.4924778 7.42928095,14.814915 5.80375046,14.814915 Z&quot; id=&quot;Shape&quot; fill=&quot;#000000&quot; fill-rule=&quot;nonzero&quot;&gt;&lt;/path&gt;
        &lt;path d=&quot;M13.9638189,8.699335 C13.9364621,8.70430925 13.9091059,8.71176968 13.8842359,8.71923074 C13.7822704,8.73663967 13.6877654,8.77643115 13.6056956,8.83860518 L10.2433156,11.3852598 C10.0766886,11.5046343 9.97720993,11.6986181 9.97720993,11.9025491 C9.97720993,12.1064807 10.0766886,12.3004639 10.2433156,12.4198383 L13.6056956,14.966493 C13.891697,15.1803725 14.2970729,15.1231721 14.5109517,14.8371707 C14.7248313,14.5511692 14.6676309,14.145794 14.3816294,13.9319145 L12.5313257,12.5392127 L21.8812495,12.5392127 L21.8812495,11.2658854 L12.5313257,11.2658854 L14.3816294,9.87318364 C14.6377872,9.71650453 14.7497006,9.40066014 14.6477351,9.11714553 C14.5482564,8.83363156 14.262255,8.65954352 13.9638189,8.699335 Z&quot; id=&quot;arrow&quot; fill=&quot;#000000&quot; transform=&quot;translate(15.929230, 11.894737) rotate(-180.000000) translate(-15.929230, -11.894737) &quot;&gt;&lt;/path&gt;
    &lt;/g&gt;
&lt;/g&gt;
</code></pre>
<p></svg></td>
<td style="height: 40px; background-color: #FCFCFC; border-right: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color:#4CA64C;">✓</span></td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px;" class="gt_row gt_right">5</td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5;" class="gt_row gt_right">5<br />1.00</td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5;" class="gt_row gt_right">0<br />0.00</td>
<td style="height: 40px; background-color: #FCFCFC; border-left: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color: #AAAAAA;">○</span></td>
<td style="height: 40px; background-color: #FCFCFC;" class="gt_row gt_center"><span style="color: #EBBC14;">○</span></td>
<td style="height: 40px; background-color: #FCFCFC; border-right: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color: #FF3300;">○</span></td>
<td style="height: 40px;" class="gt_row gt_center">—</td></p>
  </tr>
  <tr>
    <td style="height: 40px; background-color: #4CA64C; color: transparent;font-size: 0px;" class="gt_row gt_left">#4CA64C</td>
    <td style="height: 40px; color: #666666;font-size: 13px;font-weight: bold;" class="gt_row gt_right">5</td>
    <td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px;" class="gt_row gt_left">
        <div style="margin: 0; padding: 0; display: inline-block; height: 30px; vertical-align: middle; width: 16%;">
            <!--?xml version="1.0" encoding="UTF-8"?--><?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 67 67" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>col_schema_match</title>
    <g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="col_schema_match" transform="translate(0.000000, 0.310345)">
            <path d="M56.712234,1.01466935 C59.1975153,1.01466935 61.4475153,2.02202867 63.076195,3.65070832 C64.7048747,5.27938798 65.712234,7.52938798 65.712234,10.0146694 L65.712234,10.0146694 L65.712234,65.0146694 L10.712234,65.0146694 C8.22695259,65.0146694 5.97695259,64.00731 4.34827294,62.3786304 C2.71959328,60.7499507 1.71223397,58.4999507 1.71223397,56.0146694 L1.71223397,56.0146694 L1.71223397,10.0146694 C1.71223397,7.52938798 2.71959328,5.27938798 4.34827294,3.65070832 C5.97695259,2.02202867 8.22695259,1.01466935 10.712234,1.01466935 L10.712234,1.01466935 Z" id="rectangle" stroke="#000000" stroke-width="2" fill="#FFFFFF"></path>
            <path d="M53.712234,39.7885268 L54.212234,56.2885268 L42.212234,56.7885268 L42.212234,39.7885268 L53.712234,39.7885268 Z M39.712234,39.7885268 L39.712234,56.7885268 L27.712234,56.7885268 L27.712234,39.7885268 L39.712234,39.7885268 Z M25.212234,39.7885268 L25.212234,56.7885268 L13.712234,56.7885268 L13.212234,40.2885268 L25.212234,39.7885268 Z" id="columns_schema" stroke="#000000" fill-rule="nonzero"></path>
            <g id="vertical_equal" transform="translate(30.000000, 29.000000)" stroke="#000000" stroke-linecap="square">
                <line x1="2.21223397" y1="0.514669353" x2="2.21223397" y2="7.58573716" id="Line"></line>
                <line x1="5.21223397" y1="0.514669353" x2="5.21223397" y2="7.58573716" id="Line-Copy"></line>
            </g>
            <path d="M41.712234,9.01466935 L41.712234,27.0146694 L53.712234,27.0146694 C54.262234,27.0146694 54.712234,26.5646694 54.712234,26.0146694 L54.712234,10.0146694 C54.712234,9.46466935 54.262234,9.01466935 53.712234,9.01466935 L41.712234,9.01466935 Z M27.212234,9.01466935 C27.212234,9.01466935 27.212234,15.0146694 27.212234,27.0146694 L40.212234,27.0146694 L40.212234,9.01466935 C31.5455673,9.01466935 27.212234,9.01466935 27.212234,9.01466935 Z M13.712234,9.01466935 C13.162234,9.01466935 12.712234,9.46466935 12.712234,10.0146694 L12.712234,26.0146694 C12.712234,26.5646694 13.162234,27.0146694 13.712234,27.0146694 L25.712234,27.0146694 L25.712234,9.01466935 L13.712234,9.01466935 Z" id="columns_real" fill="#000000" fill-rule="nonzero"></path>
        </g>
    </g>
</svg>
        </div>
        <div style="font-family: 'IBM Plex Mono', monospace, courier; color: black; font-size: 10px; display: inline-block; vertical-align: middle;">
            <div>col_schema_match()</div>
        </div>
<pre><code>    &lt;/td&gt;
&lt;td style=&quot;height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;&quot; class=&quot;gt_row gt_left&quot;&gt;&amp;mdash;&lt;/td&gt;
&lt;td style=&quot;height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;&quot; class=&quot;gt_row gt_left&quot;&gt;SCHEMA&lt;/td&gt;
&lt;td style=&quot;height: 40px; background-color: #FCFCFC; border-left: 1px solid #D3D3D3;&quot; class=&quot;gt_row gt_center&quot;&gt;&lt;svg width=&quot;25px&quot; height=&quot;25px&quot; viewBox=&quot;0 0 25 25&quot; version=&quot;1.1&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; style=&quot;vertical-align: middle;&quot;&gt;
&lt;g id=&quot;unchanged&quot; stroke=&quot;none&quot; stroke-width=&quot;1&quot; fill=&quot;none&quot; fill-rule=&quot;evenodd&quot;&gt;
    &lt;g id=&quot;unchanged&quot; transform=&quot;translate(0.500000, 0.570147)&quot;&gt;
        &lt;rect id=&quot;Rectangle&quot; x=&quot;0.125132506&quot; y=&quot;0&quot; width=&quot;23.749735&quot; height=&quot;23.7894737&quot;&gt;&lt;/rect&gt;
        &lt;path d=&quot;M5.80375046,8.18194736 C3.77191832,8.18194736 2.11875046,9.83495328 2.11875046,11.8669474 C2.11875046,13.8989414 3.77191832,15.5519474 5.80375046,15.5519474 C7.8355826,15.5519474 9.48875046,13.8989414 9.48875046,11.8669474 C9.48875046,9.83495328 7.83552863,8.18194736 5.80375046,8.18194736 Z M5.80375046,14.814915 C4.17821997,14.814915 2.85578285,13.4924778 2.85578285,11.8669474 C2.85578285,10.2414169 4.17821997,8.91897975 5.80375046,8.91897975 C7.42928095,8.91897975 8.75171807,10.2414169 8.75171807,11.8669474 C8.75171807,13.4924778 7.42928095,14.814915 5.80375046,14.814915 Z&quot; id=&quot;Shape&quot; fill=&quot;#000000&quot; fill-rule=&quot;nonzero&quot;&gt;&lt;/path&gt;
        &lt;path d=&quot;M13.9638189,8.699335 C13.9364621,8.70430925 13.9091059,8.71176968 13.8842359,8.71923074 C13.7822704,8.73663967 13.6877654,8.77643115 13.6056956,8.83860518 L10.2433156,11.3852598 C10.0766886,11.5046343 9.97720993,11.6986181 9.97720993,11.9025491 C9.97720993,12.1064807 10.0766886,12.3004639 10.2433156,12.4198383 L13.6056956,14.966493 C13.891697,15.1803725 14.2970729,15.1231721 14.5109517,14.8371707 C14.7248313,14.5511692 14.6676309,14.145794 14.3816294,13.9319145 L12.5313257,12.5392127 L21.8812495,12.5392127 L21.8812495,11.2658854 L12.5313257,11.2658854 L14.3816294,9.87318364 C14.6377872,9.71650453 14.7497006,9.40066014 14.6477351,9.11714553 C14.5482564,8.83363156 14.262255,8.65954352 13.9638189,8.699335 Z&quot; id=&quot;arrow&quot; fill=&quot;#000000&quot; transform=&quot;translate(15.929230, 11.894737) rotate(-180.000000) translate(-15.929230, -11.894737) &quot;&gt;&lt;/path&gt;
    &lt;/g&gt;
&lt;/g&gt;
</code></pre>
<p></svg></td>
<td style="height: 40px; background-color: #FCFCFC; border-right: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color:#4CA64C;">✓</span></td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px;" class="gt_row gt_right">1</td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5;" class="gt_row gt_right">1<br />1.00</td>
<td style="height: 40px; color: black;font-family: IBM Plex Mono;font-size: 11px; border-left: 1px dashed #E5E5E5;" class="gt_row gt_right">0<br />0.00</td>
<td style="height: 40px; background-color: #FCFCFC; border-left: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color: #AAAAAA;">○</span></td>
<td style="height: 40px; background-color: #FCFCFC;" class="gt_row gt_center"><span style="color: #EBBC14;">○</span></td>
<td style="height: 40px; background-color: #FCFCFC; border-right: 1px solid #D3D3D3;" class="gt_row gt_center"><span style="color: #FF3300;">○</span></td>
<td style="height: 40px;" class="gt_row gt_center">—</td></p>
  </tr>
</tbody>
  <tfoot class="gt_sourcenotes">
  <tr>
    <td class="gt_sourcenote" colspan="14" style="text-align: left;"><div style='margin-top: 5px; margin-bottom: 5px;'><span style='background-color: #FFF; color: #444; padding: 0.5em 0.5em; position: inherit; text-transform: uppercase; margin-left: 10px; margin-right: 5px; border: solid 1px #999999; font-variant-numeric: tabular-nums; border-radius: 0; padding: 2px 10px 2px 10px;'>2026-04-02 19:07:38 UTC</span><span style='background-color: #FFF; color: #444; padding: 0.5em 0.5em; position: inherit; margin-right: 5px; border: solid 1px #999999; font-variant-numeric: tabular-nums; border-radius: 0; padding: 2px 10px 2px 10px;'>< 1 s</span><span style='background-color: #FFF; color: #444; padding: 0.5em 0.5em; position: inherit; text-transform: uppercase; margin: 5px 1px 5px -1px; border: solid 1px #999999; font-variant-numeric: tabular-nums; border-radius: 0; padding: 2px 10px 2px 10px;'>2026-04-02 19:07:38 UTC</span></div></td>
  </tr>
  <tr>
    <td class="gt_sourcenote" colspan="14" style="text-align: left;"><hr style='border: none; border-top-width: 1px; border-top-style: dotted; border-top-color: #B5B5B5; margin-top: -3px; margin-bottom: 3px;'>
<strong>Notes</strong>
<p><span style='font-variant: small-caps; font-weight: bold; font-size: smaller; text-transform: uppercase; color: #333333;'>Step 5</span> <span style='font-family: "IBM Plex Mono", monospace; font-size: smaller;'>(schema_check)</span> <span style="color:#4CA64C;">✓</span> Schema validation <strong>passed</strong>.</p>
<details style="margin-top: 2px; margin-bottom: 8px; font-size: 12px; text-indent: 12px;">
<summary style="cursor: pointer; font-weight: bold; color: #555; margin-bottom: -5px;">Schema Comparison</summary>
<div style="margin-top: 6px; padding-left: 15px; padding-right: 15px;">
<div id="pb_step_tbl" 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+Mono&display=swap');
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans&display=swap');
#pb_step_tbl 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>#pb_step_tbl thead, tbody, tfoot, tr, td, th { border-style: none; }
tr { background-color: transparent; }
#pb_step_tbl p { margin: 0; padding: 0; }
#pb_step_tbl .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: 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; }
#pb_step_tbl .gt_caption { padding-top: 4px; padding-bottom: 4px; }
#pb_step_tbl .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; }
#pb_step_tbl .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; }
#pb_step_tbl .gt_heading { background-color: #FFFFFF; text-align: left; 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; }
#pb_step_tbl .gt_bottom_border { border-bottom-style: solid; border-bottom-width: 2px; border-bottom-color: #D3D3D3; }
#pb_step_tbl .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; }
#pb_step_tbl .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; }
#pb_step_tbl .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; }
#pb_step_tbl .gt_column_spanner_outer:first-child { padding-left: 0; }
#pb_step_tbl .gt_column_spanner_outer:last-child { padding-right: 0; }
#pb_step_tbl .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%; }
#pb_step_tbl .gt_spanner_row { border-bottom-style: hidden; }
#pb_step_tbl .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; }
#pb_step_tbl .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; }
#pb_step_tbl .gt_from_md&gt; :first-child { margin-top: 0; }
#pb_step_tbl .gt_from_md&gt; :last-child { margin-bottom: 0; }
#pb_step_tbl .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; }
#pb_step_tbl .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; }
#pb_step_tbl .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; }
#pb_step_tbl .gt_row_group_first td { border-top-width: 2px; }
#pb_step_tbl .gt_row_group_first th { border-top-width: 2px; }
#pb_step_tbl .gt_striped { color: #333333; background-color: #F4F4F4; }
#pb_step_tbl .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; }
#pb_step_tbl .gt_grand_summary_row { color: #333333; background-color: #FFFFFF; text-transform: inherit; padding-top: 8px; padding-bottom: 8px; padding-left: 5px; padding-right: 5px; }
#pb_step_tbl .gt_first_grand_summary_row_bottom { border-top-style: double; border-top-width: 6px; border-top-color: #D3D3D3; }
#pb_step_tbl .gt_last_grand_summary_row_top { border-bottom-style: double; border-bottom-width: 6px; border-bottom-color: #D3D3D3; }
#pb_step_tbl .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; }
#pb_step_tbl .gt_sourcenote { font-size: 12px; padding-top: 4px; padding-bottom: 4px; padding-left: 5px; padding-right: 5px; text-align: left; }
#pb_step_tbl .gt_left { text-align: left; }
#pb_step_tbl .gt_center { text-align: center; }
#pb_step_tbl .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#pb_step_tbl .gt_font_normal { font-weight: normal; }
#pb_step_tbl .gt_font_bold { font-weight: bold; }
#pb_step_tbl .gt_font_italic { font-style: italic; }
#pb_step_tbl .gt_super { font-size: 65%; }
#pb_step_tbl .gt_footnote_marks { font-size: 75%; vertical-align: 0.4em; position: initial; }
#pb_step_tbl .gt_asterisk { font-size: 100%; vertical-align: 0; }</p>
</style>
<table style="table-layout: fixed;; width: 0px" class="gt_table" data-quarto-disable-processing="true" data-quarto-bootstrap="false">
<colgroup>
  <col style="width:40px;"/>
  <col style="width:190px;"/>
  <col style="width:190px;"/>
  <col style="width:40px;"/>
  <col style="width:190px;"/>
  <col style="width:30px;"/>
  <col style="width:190px;"/>
  <col style="width:30px;"/>
</colgroup>
<thead>
<tr class="gt_col_headings gt_spanner_row">
  <th class="gt_center gt_columns_top_border gt_column_spanner_outer" rowspan="1" colspan="3" scope="colgroup" id="pb_step_tbl-TARGET">
    <span class="gt_column_spanner">TARGET</span>
  </th>
  <th class="gt_center gt_columns_top_border gt_column_spanner_outer" rowspan="1" colspan="5" scope="colgroup" id="pb_step_tbl-EXPECTED">
    <span class="gt_column_spanner">EXPECTED</span>
  </th>
</tr>
<tr class="gt_col_headings">
  <th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="pb_step_tbl-index_target"></th>
  <th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" scope="col" id="pb_step_tbl-col_name_target">COLUMN</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" scope="col" id="pb_step_tbl-dtype_target">DATA TYPE</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_right" rowspan="1" colspan="1" scope="col" id="pb_step_tbl-index_exp"></th>
  <th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" scope="col" id="pb_step_tbl-col_name_exp">COLUMN</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" scope="col" id="pb_step_tbl-col_name_exp_correct"></th>
  <th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" scope="col" id="pb_step_tbl-dtype_exp">DATA TYPE</th>
  <th class="gt_col_heading gt_columns_bottom_border gt_left" rowspan="1" colspan="1" scope="col" id="pb_step_tbl-dtype_exp_correct"></th>
</tr>
</thead>
<tbody class="gt_table_body">
  <tr>
    <td style="font-size: 13px;" class="gt_row gt_right">1</td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">user_id</td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">Int64</td>
    <td style="font-size: 13px; border-left: 3px double #E5E5E5;" class="gt_row gt_right">1</td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">user_id</td>
    <td class="gt_row gt_left"><span style='color: #4CA64C;'>✓</span></td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">Int64</td>
    <td class="gt_row gt_left"><span style='color: #4CA64C;'>✓</span></td>
  </tr>
  <tr>
    <td style="font-size: 13px;" class="gt_row gt_right">2</td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">age</td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">Int64</td>
    <td style="font-size: 13px; border-left: 3px double #E5E5E5;" class="gt_row gt_right">2</td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">age</td>
    <td class="gt_row gt_left"><span style='color: #4CA64C;'>✓</span></td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">Int64</td>
    <td class="gt_row gt_left"><span style='color: #4CA64C;'>✓</span></td>
  </tr>
  <tr>
    <td style="font-size: 13px;" class="gt_row gt_right">3</td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">email</td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">String</td>
    <td style="font-size: 13px; border-left: 3px double #E5E5E5;" class="gt_row gt_right">3</td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">email</td>
    <td class="gt_row gt_left"><span style='color: #4CA64C;'>✓</span></td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">String</td>
    <td class="gt_row gt_left"><span style='color: #4CA64C;'>✓</span></td>
  </tr>
  <tr>
    <td style="font-size: 13px;" class="gt_row gt_right">4</td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">score</td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">Float64</td>
    <td style="font-size: 13px; border-left: 3px double #E5E5E5;" class="gt_row gt_right">4</td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">score</td>
    <td class="gt_row gt_left"><span style='color: #4CA64C;'>✓</span></td>
    <td style="color: black;font-family: IBM Plex Mono;font-size: 13px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;" class="gt_row gt_left">Float64</td>
    <td class="gt_row gt_left"><span style='color: #4CA64C;'>✓</span></td>
  </tr>
</tbody>
  <tfoot class="gt_sourcenotes">
  <tr>
    <td class="gt_sourcenote" colspan="8"><div style='padding-bottom: 2px;'>Supplied Column Schema:</div><div style='border-style: solid; border-width: thin; border-color: lightblue; padding-left: 2px; padding-right: 2px; padding-bottom: 3px;'><code style='color: #303030; font-family: monospace; font-size: 8px;'>[('user_id', 'Int64'), ('age', 'Int64'), ('email', 'String'), ('score', 'Float64')]</code></div></td>
  </tr>
  <tr>
    <td class="gt_sourcenote" colspan="8">
<div style='padding-bottom: 2px;'>Schema Match Settings</div>
<div style='padding-bottom: 4px;'><div style="display: flex; font-size: 13.7px; padding-top: 2px;"><div style="border-style: solid; border-width: 1px; border-color: #87CEFA; border-radius: 5px; background-color: #F0F8FF; font-size: x-small; padding-left: 4px; padding-right: 4px; margin-left: 5px; margin-right: 5px;  margin-top: 2px; ">COMPLETE</div><div style="border-style: solid; border-width: 1px; border-color: #87CEFA; border-radius: 5px; background-color: #F0F8FF; font-size: x-small; padding-left: 4px; padding-right: 4px; margin-left: 5px; margin-right: 5px;  margin-top: 2px; ">IN ORDER</div><div style="border-style: solid; border-width: 1px; border-color: #A9A9A9; border-radius: 5px; background-color: #F5F5F5; font-size: x-small; padding-left: 4px; padding-right: 4px; margin-left: 5px; margin-right: 5px;  margin-top: 2px; ">COLUMN &ne; column</div><div style="border-style: solid; border-width: 1px; border-color: #A9A9A9; border-radius: 5px; background-color: #F5F5F5; font-size: x-small; padding-left: 4px; padding-right: 4px; margin-left: 5px; margin-right: 5px;  margin-top: 2px; ">DTYPE &ne; dtype</div><div style="border-style: solid; border-width: 1px; border-color: #A9A9A9; border-radius: 5px; background-color: #F5F5F5; font-size: x-small; padding-left: 4px; padding-right: 4px; margin-left: 5px; margin-right: 5px;  margin-top: 2px; ">float &ne; float64</div></div></div>
</td>
  </tr>
</tfoot>
</table>
</div>
</div>
</details>
</td>
  </tr>
</tfoot>
</table>
</div>
<p>This example demonstrates Pointblank&rsquo;s chainable validation approach where each validation step is
clearly defined and can be configured with different threshold levels. The resulting validation
object provides rich, interactive reporting that shows not just what passed or failed, but detailed
statistics about the validation process. The threshold system allows for nuanced responses to data
quality issues.</p>
<h3 id="comparisons-1">Comparisons
</h3>
<p>Unlike Pandera&rsquo;s schema-first approach, Pointblank focuses on step-by-step validation with detailed
reporting and flexible failure thresholds that can be set at both the global and individual
validation step level. Both Pointblank and Validoopsie use numeric threshold values for granular
control over acceptable failure rates, but they differ in their primary focus: Pointblank emphasizes
comprehensive reporting and stakeholder communication, while Validoopsie prioritizes operational
resilience through its impact level system (low/medium/high) that controls whether threshold
breaches are logged, reported, or raise exceptions.</p>
<p>While both libraries support custom validation logic, Pointblank&rsquo;s <code>specially()</code> method integrates
seamlessly with its reporting system, whereas Validoopsie provides a structured framework for
creating custom validation classes that fit into its modular validation catalog.</p>
<h3 id="unique-strengths-and-when-to-use-2">Unique Strengths and When to Use
</h3>
<ul>
<li>beautiful, interactive HTML reports perfect for sharing with stakeholders</li>
<li>threshold-based alerting system with configurable actions</li>
<li>segmented validation for analyzing subsets of data</li>
<li>LLM-powered validation suggestions via <code>DraftValidation</code></li>
<li>comprehensive data inspection tools and summary tables</li>
<li>step-by-step validation reporting with detailed failure analysis (via <code>.get_step_report()</code>)</li>
</ul>
<p>Data practitioners might want to choose Pointblank when stakeholder communication and comprehensive
data quality reporting are priorities. Because of the reporting tables it can generate, it&rsquo;s
well-suited for data teams that need to regularly report on data quality to relevant stakeholders.
Pointblank also excels in production data monitoring scenarios, data observability workflows, and
situations where understanding the nuances of data quality issues matters more than simple pass/fail
validation.</p>
<h2 id="4-validoopsie-composable-checks-with-smart-failure-handling">4. Validoopsie: Composable Checks with Smart Failure Handling
</h2>
<p>Validoopsie is built around composable validation principles, providing a toolkit for creating
reusable validation functions organized into logical modules. Drawing inspiration from Great
Expectations but with a much lighter footprint, Validoopsie emphasizes building validation logic
from modular, testable components that can be combined in flexible ways to create complex validation
workflows. The library had Polars support from its very first release (early-2025).</p>
<p>What sets Validoopsie apart is its sophisticated approach to handling validation failures through
<em>impact levels</em> and <em>threshold tolerances</em>. These features that give you fine-grained control over
how your validation pipeline behaves when things go wrong.</p>
<h3 id="example-3">Example
</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><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></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">validoopsie</span> <span class="kn">import</span> <span class="n">Validate</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">narwhals.dtypes</span> <span class="kn">import</span> <span class="n">Int64</span><span class="p">,</span> <span class="n">Float64</span><span class="p">,</span> <span class="n">String</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Composable validation checks with impact levels and thresholds</span>
</span></span><span class="line"><span class="cl"><span class="n">validation</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">Validate</span><span class="p">(</span><span class="n">user_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">ValuesValidation</span><span class="o">.</span><span class="n">ColumnValuesToBeBetween</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">column</span><span class="o">=</span><span class="s2">&#34;user_id&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">min_value</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">impact</span><span class="o">=</span><span class="s2">&#34;high&#34;</span>  <span class="c1"># Critical - will raise exception</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">ValuesValidation</span><span class="o">.</span><span class="n">ColumnValuesToBeBetween</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">column</span><span class="o">=</span><span class="s2">&#34;age&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">min_value</span><span class="o">=</span><span class="mi">18</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">max_value</span><span class="o">=</span><span class="mi">80</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">threshold</span><span class="o">=</span><span class="mf">0.1</span><span class="p">,</span>  <span class="c1"># Allow 10% failures</span>
</span></span><span class="line"><span class="cl">        <span class="n">impact</span><span class="o">=</span><span class="s2">&#34;medium&#34;</span>  <span class="c1"># Important but not critical</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">StringValidation</span><span class="o">.</span><span class="n">PatternMatch</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">column</span><span class="o">=</span><span class="s2">&#34;email&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">pattern</span><span class="o">=</span><span class="sa">r</span><span class="s2">&#34;^[^@]+@[^@]+\.[^@]+$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">threshold</span><span class="o">=</span><span class="mf">0.05</span><span class="p">,</span>  <span class="c1"># Allow 5% malformed emails</span>
</span></span><span class="line"><span class="cl">        <span class="n">impact</span><span class="o">=</span><span class="s2">&#34;low&#34;</span>  <span class="c1"># Record but don&#39;t interrupt</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">ValuesValidation</span><span class="o">.</span><span class="n">ColumnValuesToBeBetween</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">column</span><span class="o">=</span><span class="s2">&#34;score&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">min_value</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">max_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="n">impact</span><span class="o">=</span><span class="s2">&#34;medium&#34;</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">TypeValidation</span><span class="o">.</span><span class="n">TypeCheck</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">frame_schema_definition</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;user_id&#34;</span><span class="p">:</span> <span class="n">Int64</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;age&#34;</span><span class="p">:</span> <span class="n">Int64</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="n">String</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;score&#34;</span><span class="p">:</span> <span class="n">Float64</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="n">impact</span><span class="o">=</span><span class="s2">&#34;high&#34;</span>  <span class="c1"># Schema compliance is critical</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"># Get validation results</span>
</span></span><span class="line"><span class="cl"><span class="n">validation</span><span class="o">.</span><span class="n">validate</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Access detailed results for analysis</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Validation results:&#34;</span><span class="p">,</span> <span class="n">validation</span><span class="o">.</span><span class="n">results</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><pre><span class="ansi-green-fg">2026-04-02 12:07:38.191</span> | <span class="ansi-bold">INFO    </span> | <span class="ansi-cyan-fg">validoopsie.validate</span>:<span class="ansi-cyan-fg">validate</span>:<span class="ansi-cyan-fg">414</span> - <span class="ansi-bold">Passed validation: {'validation': 'ColumnValuesToBeBetween', 'impact': 'high', 'timestamp': '2026-04-02T12:07:38.179694-07:00', 'column': 'user_id', 'result': {'status': 'Success', 'threshold_pass': True, 'message': 'All items passed the validation.', 'frame_row_number': 5, 'threshold': 0.0}}</span>

<span class="ansi-green-fg">2026-04-02 12:07:38.191</span> | <span class="ansi-red-fg ansi-bold">ERROR   </span> | <span class="ansi-cyan-fg">validoopsie.validate</span>:<span class="ansi-cyan-fg">validate</span>:<span class="ansi-cyan-fg">406</span> - <span class="ansi-red-fg ansi-bold">Failed validation: ColumnValuesToBeBetween_age - The column 'age' has values that are not between 18 and 80.</span>

<span class="ansi-green-fg">2026-04-02 12:07:38.192</span> | <span class="ansi-yellow-fg ansi-bold">WARNING </span> | <span class="ansi-cyan-fg">validoopsie.validate</span>:<span class="ansi-cyan-fg">validate</span>:<span class="ansi-cyan-fg">408</span> - <span class="ansi-yellow-fg ansi-bold">Failed validation: PatternMatch_email - The column 'email' has entries that do not match the pattern '^[^@]+@[^@]+\.[^@]+$'.</span>

<span class="ansi-green-fg">2026-04-02 12:07:38.192</span> | <span class="ansi-bold">INFO    </span> | <span class="ansi-cyan-fg">validoopsie.validate</span>:<span class="ansi-cyan-fg">validate</span>:<span class="ansi-cyan-fg">414</span> - <span class="ansi-bold">Passed validation: {'validation': 'ColumnValuesToBeBetween', 'impact': 'medium', 'timestamp': '2026-04-02T12:07:38.182269-07:00', 'column': 'score', 'result': {'status': 'Success', 'threshold_pass': True, 'message': 'All items passed the validation.', 'frame_row_number': 5, 'threshold': 0.0}}</span>

<span class="ansi-green-fg">2026-04-02 12:07:38.192</span> | <span class="ansi-bold">INFO    </span> | <span class="ansi-cyan-fg">validoopsie.validate</span>:<span class="ansi-cyan-fg">validate</span>:<span class="ansi-cyan-fg">414</span> - <span class="ansi-bold">Passed validation: {'validation': 'TypeCheck', 'impact': 'high', 'timestamp': '2026-04-02T12:07:38.182676-07:00', 'column': 'DataTypeColumnValidation', 'result': {'status': 'Success', 'threshold_pass': True, 'message': 'All items passed the validation.', 'frame_row_number': 4, 'threshold': 0.0}}</span>
</pre>
<pre><code>Validation results: {'Summary': {'passed': False, 'validations': ['ColumnValuesToBeBetween_user_id', 'ColumnValuesToBeBetween_age', 'PatternMatch_email', 'ColumnValuesToBeBetween_score', 'TypeCheck_DataTypeColumnValidation'], 'failed_validation': ['ColumnValuesToBeBetween_age', 'PatternMatch_email']}, 'ColumnValuesToBeBetween_user_id': {'validation': 'ColumnValuesToBeBetween', 'impact': 'high', 'timestamp': '2026-04-02T12:07:38.179694-07:00', 'column': 'user_id', 'result': {'status': 'Success', 'threshold_pass': True, 'message': 'All items passed the validation.', 'frame_row_number': 5, 'threshold': 0.0}}, 'ColumnValuesToBeBetween_age': {'validation': 'ColumnValuesToBeBetween', 'impact': 'medium', 'timestamp': '2026-04-02T12:07:38.181019-07:00', 'column': 'age', 'result': {'status': 'Fail', 'threshold_pass': False, 'message': &quot;The column 'age' has values that are not between 18 and 80.&quot;, 'failing_items': [95], 'failed_number': 1, 'frame_row_number': 5, 'threshold': 0.1, 'failed_percentage': 0.2}}, 'PatternMatch_email': {'validation': 'PatternMatch', 'impact': 'low', 'timestamp': '2026-04-02T12:07:38.181596-07:00', 'column': 'email', 'result': {'status': 'Fail', 'threshold_pass': False, 'message': &quot;The column 'email' has entries that do not match the pattern '^[^@]+@[^@]+\\.[^@]+$'.&quot;, 'failing_items': ['invalid-email'], 'failed_number': 1, 'frame_row_number': 5, 'threshold': 0.05, 'failed_percentage': 0.2}}, 'ColumnValuesToBeBetween_score': {'validation': 'ColumnValuesToBeBetween', 'impact': 'medium', 'timestamp': '2026-04-02T12:07:38.182269-07:00', 'column': 'score', 'result': {'status': 'Success', 'threshold_pass': True, 'message': 'All items passed the validation.', 'frame_row_number': 5, 'threshold': 0.0}}, 'TypeCheck_DataTypeColumnValidation': {'validation': 'TypeCheck', 'impact': 'high', 'timestamp': '2026-04-02T12:07:38.182676-07:00', 'column': 'DataTypeColumnValidation', 'result': {'status': 'Success', 'threshold_pass': True, 'message': 'All items passed the validation.', 'frame_row_number': 4, 'threshold': 0.0}}}
</code></pre>
<p>This example showcases Validoopsie&rsquo;s key differentiators: modular validation categories
(<code>ValuesValidation</code>, <code>StringValidation</code>, <code>TypeValidation</code>) combined with <em>impact levels</em> that
control failure behavior and <em>thresholds</em> that allow controlled tolerance for data quality issues.
Unlike other libraries that treat all validation failures equally, Validoopsie lets you specify
which validations are critical (&ldquo;high&rdquo; impact raises exceptions) versus informational (&ldquo;low&rdquo; impact
just logs results).</p>
<p>Validoopsie&rsquo;s most powerful feature is its three-tier <code>impact=</code> system combined with <code>threshold=</code>
tolerance:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Example showing sophisticated failure handling</span>
</span></span><span class="line"><span class="cl"><span class="n">validation</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">Validate</span><span class="p">(</span><span class="n">user_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Critical validation - no tolerance</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">NullValidation</span><span class="o">.</span><span class="n">ColumnNotBeNull</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">column</span><span class="o">=</span><span class="s2">&#34;user_id&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">impact</span><span class="o">=</span><span class="s2">&#34;high&#34;</span>    <span class="c1"># Will raise an exception if any Null values found</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Important validation with tolerance</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">StringValidation</span><span class="o">.</span><span class="n">PatternMatch</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">column</span><span class="o">=</span><span class="s2">&#34;email&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">pattern</span><span class="o">=</span><span class="sa">r</span><span class="s2">&#34;^[^@]+@[^@]+\.[^@]+$&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">threshold</span><span class="o">=</span><span class="mf">0.15</span><span class="p">,</span>  <span class="c1"># Allow up to 15% malformed emails</span>
</span></span><span class="line"><span class="cl">        <span class="n">impact</span><span class="o">=</span><span class="s2">&#34;medium&#34;</span>  <span class="c1"># Log failures but don&#39;t stop processing</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Informational validation</span>
</span></span><span class="line"><span class="cl">    <span class="o">.</span><span class="n">ValuesValidation</span><span class="o">.</span><span class="n">ColumnValuesToBeBetween</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">column</span><span class="o">=</span><span class="s2">&#34;score&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">min_value</span><span class="o">=</span><span class="mi">90</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">max_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="n">threshold</span><span class="o">=</span><span class="mf">0.8</span><span class="p">,</span>  <span class="c1"># Allow 80% to be outside &#34;excellent&#34; range</span>
</span></span><span class="line"><span class="cl">        <span class="n">impact</span><span class="o">=</span><span class="s2">&#34;low&#34;</span>    <span class="c1"># Just track high performers</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">validation</span><span class="o">.</span><span class="n">validate</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><pre><span class="ansi-green-fg">2026-04-02 12:07:38.199</span> | <span class="ansi-bold">INFO    </span> | <span class="ansi-cyan-fg">validoopsie.validate</span>:<span class="ansi-cyan-fg">validate</span>:<span class="ansi-cyan-fg">414</span> - <span class="ansi-bold">Passed validation: {'validation': 'ColumnNotBeNull', 'impact': 'high', 'timestamp': '2026-04-02T12:07:38.197433-07:00', 'column': 'user_id', 'result': {'status': 'Success', 'threshold_pass': True, 'message': 'All items passed the validation.', 'frame_row_number': 5, 'threshold': 0.0}}</span>

<span class="ansi-green-fg">2026-04-02 12:07:38.199</span> | <span class="ansi-red-fg ansi-bold">ERROR   </span> | <span class="ansi-cyan-fg">validoopsie.validate</span>:<span class="ansi-cyan-fg">validate</span>:<span class="ansi-cyan-fg">406</span> - <span class="ansi-red-fg ansi-bold">Failed validation: PatternMatch_email - The column 'email' has entries that do not match the pattern '^[^@]+@[^@]+\.[^@]+$'.</span>

<span class="ansi-green-fg">2026-04-02 12:07:38.199</span> | <span class="ansi-bold">INFO    </span> | <span class="ansi-cyan-fg">validoopsie.validate</span>:<span class="ansi-cyan-fg">validate</span>:<span class="ansi-cyan-fg">414</span> - <span class="ansi-bold">Passed validation: {'validation': 'ColumnValuesToBeBetween', 'impact': 'low', 'timestamp': '2026-04-02T12:07:38.198631-07:00', 'column': 'score', 'result': {'status': 'Success', 'threshold_pass': True, 'message': "The column 'score' has values that are not between 90 and 100.", 'failing_items': [78.3, 85.5, 88.7], 'failed_number': 3, 'frame_row_number': 5, 'threshold': 0.8, 'failed_percentage': 0.6}}</span>
</pre>
<p>Validoopsie strikes a unique balance between operational flexibility and production reliability,
making it an excellent choice for teams that need sophisticated failure handling without the
complexity of larger validation frameworks.</p>
<h3 id="comparisons-2">Comparisons
</h3>
<p>Validoopsie&rsquo;s functional approach contrasts with Pandera&rsquo;s schema-centric methodology and Patito&rsquo;s
object-oriented models. While Pandera focuses on statistical validation and Patito emphasizes
Pydantic integration, Validoopsie prioritizes flexibility and operational robustness.</p>
<p>Compared to Pointblank, both libraries offer sophisticated threshold-based failure handling using
numeric values (e.g., 0.1 for 10% tolerance), but they differ in their architectural approach:
Validoopsie combines numeric thresholds with impact levels (low/medium/high) that control the
behavioral response to threshold breaches, while Pointblank integrates thresholds directly into its
comprehensive reporting and alerting system. Both support custom validation, but Validoopsie uses a
modular validation catalog approach while Pointblank&rsquo;s <code>specially()</code> method integrates seamlessly
with its step-by-step reporting workflow.</p>
<p>Validoopsie is the only library in this survey that provides built-in logging capabilities, making
it particularly valuable for production environments where validation events need to be tracked and
monitored.</p>
<p>The library&rsquo;s Great Expectations inspiration is evident in its modular design, but Validoopsie
delivers this functionality with a much lighter dependency footprint and simpler API. Teams
familiar with Great Expectations will find Validoopsie&rsquo;s approach familiar but more streamlined.</p>
<h3 id="unique-strengths-and-when-to-use-3">Unique Strengths and When to Use
</h3>
<p>Validoopsie&rsquo;s standout features include:</p>
<ul>
<li>graduated failure handling through impact levels (low/medium/high) combined with numeric
thresholds that control both tolerance levels and behavioral responses to failures</li>
<li>numeric threshold tolerance allowing controlled acceptance of data quality issues (e.g., &ldquo;allow
10% email format failures&rdquo; with <code>threshold=0.1</code>)</li>
<li>built-in structured logging using loguru allows for automatic logging of validation results,
failures, and performance metrics (unique among these libraries)</li>
<li>being a lightweight Great Expectations alternative with similar composability but minimal
dependencies</li>
<li>an extensive validation catalog organized into logical namespaces (Date, String, Null, Values,
etc.)</li>
<li>custom validation framework with consistent patterns for creating domain-specific rules</li>
</ul>
<p>Choose Validoopsie when you need:</p>
<ul>
<li>operational resilience in production pipelines where partial data quality issues shouldn&rsquo;t
stop processing</li>
<li>comprehensive validation logging and monitoring for observability in production environments</li>
<li>fine-grained control over validation failure behavior with different criticality levels</li>
<li>lightweight Great Expectations functionality without the complexity and dependencies</li>
<li>custom validation development with a clear, consistent framework</li>
<li>modular validation design that promotes reusability across projects</li>
</ul>
<p>Validoopsie is particularly well-suited for data engineering teams building robust production
pipelines where data quality monitoring is important but pipeline availability is critical. Its
impact/threshold system makes it uniquely powerful for environments where you need to distinguish
between &ldquo;nice to have&rdquo; and &ldquo;must have&rdquo; data quality requirements.</p>
<h2 id="5-dataframely-type-safe-schema-validation-with-advanced-features">5. Dataframely: Type-Safe Schema Validation with Advanced Features
</h2>
<p>Dataframely is a comprehensive data validation framework that brings type-safe schema validation to
Polars DataFrames with some of the most advanced features in the ecosystem. The library focuses on
providing both runtime validation and static type checking, with particular strengths in
collection validation for related DataFrames and extensive integration capabilities with external
tools.</p>
<p>Dataframely launched in early 2025 with native Polars support as a core feature, built specifically
for the modern data ecosystem with first-class support for complex validation scenarios.</p>
<h3 id="example-4">Example
</h3>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><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">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">dataframely</span> <span class="k">as</span> <span class="nn">dy</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">UserSchema</span><span class="p">(</span><span class="n">dy</span><span class="o">.</span><span class="n">Schema</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">user_id</span> <span class="o">=</span> <span class="n">dy</span><span class="o">.</span><span class="n">Int64</span><span class="p">(</span><span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="nb">min</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">nullable</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">age</span> <span class="o">=</span> <span class="n">dy</span><span class="o">.</span><span class="n">Int64</span><span class="p">(</span><span class="n">nullable</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">email</span> <span class="o">=</span> <span class="n">dy</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">regex</span><span class="o">=</span><span class="sa">r</span><span class="s2">&#34;^[^@]+@[^@]+\.[^@]+$&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">score</span> <span class="o">=</span> <span class="n">dy</span><span class="o">.</span><span class="n">Float64</span><span class="p">(</span><span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="nb">min</span><span class="o">=</span><span class="mf">0.0</span><span class="p">,</span> <span class="nb">max</span><span class="o">=</span><span class="mf">100.0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Use @dy.rule() for age range validation</span>
</span></span><span class="line"><span class="cl">    <span class="nd">@dy.rule</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">age_in_range</span><span class="p">(</span><span class="bp">cls</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">pl</span><span class="o">.</span><span class="n">Expr</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">pl</span><span class="o">.</span><span class="n">col</span><span class="p">(</span><span class="s2">&#34;age&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">is_between</span><span class="p">(</span><span class="mi">18</span><span class="p">,</span> <span class="mi">80</span><span class="p">,</span> <span class="n">closed</span><span class="o">=</span><span class="s2">&#34;both&#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"># Validate using the schema</span>
</span></span><span class="line"><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">validated_data</span> <span class="o">=</span> <span class="n">UserSchema</span><span class="o">.</span><span class="n">validate</span><span class="p">(</span><span class="n">user_data</span><span class="p">,</span> <span class="n">cast</span><span class="o">=</span><span class="kc">True</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;Validation successful!&#34;</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">validated_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</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;Validation failed: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This example showcases Dataframely&rsquo;s class-based schema approach with several notable features:
primary key constraints, comprehensive type validation with bounds, regex pattern matching, and
custom validation rules using the <code>@dy.rule()</code> decorator (used here for age range checking).</p>
<p>The <code>cast=True</code> parameter automatically coerces column types to match the schema definitions. This
is really useful when working with data from external sources where column types might not exactly
match your schema expectations (e.g., integers loaded as strings from CSV files).</p>
<p>Dataframely features soft validation and failure introspection. As one of Dataframely&rsquo;s standout
features, it brings a fairly sophisticated approach to validation failures. Rather than just raising
exceptions, it provides detailed failure analysis:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Soft validation: separate valid and invalid rows</span>
</span></span><span class="line"><span class="cl"><span class="n">good_data</span><span class="p">,</span> <span class="n">failure_info</span> <span class="o">=</span> <span class="n">UserSchema</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">user_data</span><span class="p">,</span> <span class="n">cast</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Valid rows:&#34;</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">good_data</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;Failure counts:&#34;</span><span class="p">,</span> <span class="n">failure_info</span><span class="o">.</span><span class="n">counts</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;Co-occurrence analysis:&#34;</span><span class="p">,</span> <span class="n">failure_info</span><span class="o">.</span><span class="n">cooccurrence_counts</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Inspect the actual failed rows</span>
</span></span><span class="line"><span class="cl"><span class="n">failed_rows</span> <span class="o">=</span> <span class="n">failure_info</span><span class="o">.</span><span class="n">invalid</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;Failed data:&#34;</span><span class="p">,</span> <span class="n">failed_rows</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="comparisons-3">Comparisons
</h3>
<p>While both Dataframely and Pandera offer schema-centric validation approaches, they serve different
validation philosophies. Pandera excels in statistical validation with hypothesis testing and
distribution checks, making it ideal for data science workflows where statistical properties matter.
Dataframely, by contrast, emphasizes relational data integrity and type safety, providing more
sophisticated failure analysis and collection-level validation capabilities that Pandera doesn&rsquo;t
offer.</p>
<p>The relationship between Dataframely and Patito is particularly interesting since both use
class-based schema definitions. However, Dataframely extends far beyond Patito&rsquo;s Pydantic-focused
approach. Where Patito provides clean, simple validation with excellent Pydantic integration,
Dataframely offers advanced features like collection validation, group rules, and comprehensive
failure introspection. Teams already invested in Pydantic workflows might prefer Patito&rsquo;s
simplicity, while those building complex data systems will appreciate Dataframely&rsquo;s feature set.</p>
<p>Dataframely and Pointblank represent two different approaches to comprehensive data validation.
Pointblank shines in stakeholder communication with its beautiful interactive reports and
threshold-based alerting systems, making it perfect for data quality reporting. Dataframely focuses
instead on type safety and complex validation logic, with unique collection validation capabilities
that no other library in this survey provides. The choice between these two will comes down to
whether your priority is communicating validation results or ensuring complex data relationships
remain consistent.</p>
<p>When compared to Validoopsie&rsquo;s method chaining approach, Dataframely offers a more structured,
schema-centric methodology with advanced type safety features that Validoopsie doesn&rsquo;t provide.
While Validoopsie excels in operational flexibility and lightweight design for building reusable
validation components, Dataframely&rsquo;s strength lies in its comprehensive type system integration,
collection validation capabilities, and sophisticated failure analysis. And that makes it ideal for
complex data engineering workflows where relationships between multiple DataFrames matter as much as
individual DataFrame validation.</p>
<h3 id="unique-strengths-and-when-to-use-4">Unique Strengths and When to Use
</h3>
<p>Dataframely&rsquo;s standout features include:</p>
<ul>
<li>advanced type safety with full mypy integration and generic DataFrame types</li>
<li>collection validation for ensuring consistency across related DataFrames</li>
<li>group-based validation rules using <code>@dy.rule(group_by=[...])</code> for aggregate constraints</li>
<li>schema inheritance for reducing code duplication in related schemas</li>
<li>production-ready soft validation that separates valid and invalid data</li>
</ul>
<p>One might choose Dataframely when building complex data systems where:</p>
<ul>
<li>type safety and static analysis are critical for code quality</li>
<li>you need to validate relationships between multiple related DataFrames</li>
<li>you&rsquo;re working with production pipelines that need to handle partial data quality issues
gracefully</li>
<li>schema reuse and inheritance would benefit your codebase organization</li>
</ul>
<p>Dataframely is particularly well-suited for data engineering teams building robust, type-safe data
pipelines where the relationships between different data entities are as important as the validation
of individual DataFrames. Its collection validation capabilities make it uniquely powerful for
ensuring referential integrity in complex data workflows.</p>
<h2 id="choosing-the-right-library">Choosing the Right Library
</h2>
<p>With five solid validation libraries to choose from, the decision often comes down to your team&rsquo;s
specific workflow, existing tech stack, and validation requirements. Here are some practical
considerations to help guide your choice:</p>
<p><em>Start with your existing tools</em></p>
<p>If you&rsquo;re already using Pydantic extensively, Patito will feel natural. Teams that are heavily
invested in type checking and statistical analysis should probably gravitate toward Pandera. If
you&rsquo;re building data products that need stakeholder buy-in, Pointblank&rsquo;s reporting capabilities
become incredibly useful in that context. For teams already committed to strong typing and static
analysis workflows, Dataframely&rsquo;s advanced type safety features will feel like a natural extension
of your existing practices.</p>
<p><em>Consider your validation complexity</em></p>
<p>For straightforward schema validation and type checking, any of these libraries will work well. But
if you need statistical hypothesis testing, Pandera is your best bet. For highly custom validation
logic that needs to be composed and reused, Validoopsie shines. When validation results need to be
communicated to non-technical stakeholders, Pointblank&rsquo;s interactive reports are basically
unmatched. If you&rsquo;re dealing with complex relational data where multiple DataFrames need to maintain
consistency with each other, Dataframely&rsquo;s collection validation capabilities are unique in the
ecosystem.</p>
<p><em>Think about failure tolerance requirements</em></p>
<p>One of the most important architectural differences among these libraries is how they handle
validation failures. Only Pointblank and Validoopsie offer numeric threshold-based failure
tolerance. This is the ability to accept a controlled percentage of validation failures without
treating the entire validation as failed.</p>
<p>This distinction can be crucial for production environments where some level of data quality issues
is acceptable and you need fine-grained control over when validations should fail versus warn. In
many real-world scenarios, poor data quality is a given reality, and the goal becomes gradually
improving quality over time rather than enforcing perfection. Thresholds can then be seen not as
simple failure tolerances but more like data quality metrics and improvement goals (e.g., you might
start with <code>threshold=0.15</code> for email validation and progressively tighten to <code>0.05</code> as upstream
systems improve).</p>
<p><em>Think about your team&rsquo;s preferences</em></p>
<p>There&rsquo;s a human dimension here. Some data teams might prefer the declarative, schema-first approach
of Pandera, Patito, and Dataframely, whereas others like the step-by-step, method-chaining style of
Pointblank and Validoopsie. There&rsquo;s really no right or wrong choice here. It&rsquo;s all about what feels
right and most natural for your team&rsquo;s coding style and mental model.</p>
<p><em>Don&rsquo;t feel locked into one choice</em></p>
<p>My hunch is that many teams already successfully use different libraries for different parts of
their data pipeline. They&rsquo;re leveraging each tool&rsquo;s strengths where they matter most. So you could
conceivably use Patito for Pydantic-style validation, Pandera for statistical checks in your
analysis pipeline, Pointblank for generating stakeholder reports, and Dataframely for complex data
engineering workflows (use &rsquo;em all!). This multi-library approach can be particularly effective in
larger organizations with diverse validation needs.</p>
<p>I suppose the key is to start with one library that fits your immediate needs, learn it well, and
then consider expanding your toolkit as your validation requirements evolve.</p>
<h2 id="summary-and-wrapping-up">Summary and Wrapping Up
</h2>
<p>The Python ecosystem offers truly excellent options for validating Polars DataFrames! Choosing is
always tough but this is how one could make the decision based on specific needs:</p>
<ul>
<li>for type-safe pipelines, <strong>Pandera</strong>, <strong>Dataframely</strong>, or <strong>Patito</strong> are ideal</li>
<li>for stakeholder reporting, <strong>Pointblank</strong> is a great choice</li>
<li>for row-level object modeling, go with <strong>Patito</strong></li>
<li>for statistical validation, <strong>Pandera</strong> is perfect</li>
<li>for data quality improvement, <strong>Pointblank</strong> or <strong>Validoopsie</strong> fit well</li>
</ul>
<p>Each library has evolved to serve different aspects of the data validation ecosystem. Try them all
and, with a little understanding of their strengths, you&rsquo;ll get good at picking the right data
validation tool for your specific use case.</p>
<p>This survey represents our understanding of these libraries as of mid-2025. Given the rapid pace of
development in the Python data ecosystem, some details may become outdated or contain inaccuracies
(we may have even gotten things wrong at the outset). If you notice any errors or have updates to
share, we&rsquo;d love to hear from you! Please reach out through:</p>
<ul>
<li><a href="https://github.com/posit-dev/pointblank/issues" target="_blank" rel="noopener">GitHub Issues</a>
</li>
<li><a href="https://github.com/posit-dev/pointblank/discussions" target="_blank" rel="noopener">GitHub Discussions</a>
</li>
<li>Our <a href="https://discord.com/invite/YH7CybCNCQ" target="_blank" rel="noopener">Discord Server</a>
</li>
</ul>
<p>Any feedback you provide helps keep this resource accurate and useful for the community!</p>
]]></description>
    </item>
  </channel>
</rss>
