<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Katie McLaughlin</title>
    <description>The latest articles on DEV Community by Katie McLaughlin (@glasnt).</description>
    <link>https://dev.to/glasnt</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F167207%2F078c6167-7b77-47d5-bc3a-eed0cf2e89e2.jpeg</url>
      <title>DEV Community: Katie McLaughlin</title>
      <link>https://dev.to/glasnt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/glasnt"/>
    <language>en</language>
    <item>
      <title>Interactive AI Eval Dashboards with Data Studio</title>
      <dc:creator>Katie McLaughlin</dc:creator>
      <pubDate>Tue, 08 Sep 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/googleai/interactive-ai-eval-dashboards-with-data-studio-1kl9</link>
      <guid>https://dev.to/googleai/interactive-ai-eval-dashboards-with-data-studio-1kl9</guid>
      <description>&lt;p&gt;Welcome to the final entry of our series about designing, analyzing and visualizing AI evals! As with the previous entries, you are more than welcome to simply read over my exploration or, for those of you looking for extra credit, feel free to play along at home. If this isn’t you, please skip to "Overview of New Pre-Configured Dashboard Components"&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: This blog series contains AI-generated diagrams alongside actual screenshots and hand-drawn edits of both. AI also assisted in minor copy editing. Furthermore, the math is mine but the formula formatting is AI's.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Implementation prerequisites
&lt;/h2&gt;

&lt;p&gt;If you intend to (re)implement the procedures in this walkthrough, all you need is the CSV export (&lt;code&gt;data_mega_export.csv&lt;/code&gt;) generated during &lt;a href="https://dev.to/googleai/step-up-to-the-sheets-ai-eval-export-and-illustrating-data-bak"&gt;Part 3&lt;/a&gt; (from evaluations performed in &lt;a href="https://dev.to/googleai/designing-ai-evals-clarity-now-and-visualization-next-4eii"&gt;Part 1&lt;/a&gt;) and a Google Workspace account to access Data Studio. &lt;a href="https://dev.to/googleai/ai-evals-at-a-glance-heatmaps-for-stakeholders-2mki"&gt;Part 2&lt;/a&gt; will give you a deeper understanding of evals, trace diagnostics and back-of-the-napkin analysis but is &lt;em&gt;optional&lt;/em&gt; if you just want to build dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to the story at hand
&lt;/h2&gt;

&lt;p&gt;In this final part of our series, we connect our evaluation dataset to &lt;a href="https://datastudio.google.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Data Studio&lt;/strong&gt;&lt;/a&gt; to build codeless, interactive dashboards.&lt;/p&gt;

&lt;p&gt;While &lt;code&gt;inspect view&lt;/code&gt; provides developer-focused TUI trace diagnostics and Google Sheets gives us tabular sparklines, non-technical stakeholders (product managers, business leads, executive sponsors) need interactive dashboards where they can slice performance across our foundational 3D Evaluation Matrix on the fly without writing code. Beyond macro dimensions like &lt;strong&gt;model&lt;/strong&gt; family and &lt;strong&gt;skill group&lt;/strong&gt;, our dashboard empowers users to dynamically partition data over key operational booleans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;completed&lt;/code&gt; (Conditional Accuracy)&lt;/strong&gt;: Isolates Model Capability (accuracy on completed runs) from infrastructure noise (quota drops, timeouts, container crashes). Toggling between &lt;code&gt;TRUE&lt;/code&gt; and &lt;code&gt;ALL&lt;/code&gt; exposes Survivorship Bias in task execution.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;is_baseline_or_has_activated_skill&lt;/code&gt; (Conditional Uptake Slicer)&lt;/strong&gt;: Separates baseline control cohorts from active skill interventions where the model invoked &lt;code&gt;activate_skill&lt;/code&gt; (i.e. &lt;strong&gt;uptook&lt;/strong&gt; the skill into context).

&lt;ul&gt;
&lt;li&gt;Crucially, "activated" in Inspect AI telemetry specifically tracks skill &lt;em&gt;uptake&lt;/em&gt; (executing the &lt;code&gt;activate_skill&lt;/code&gt; tool call). Filtering for &lt;code&gt;FALSE&lt;/code&gt; isolates dormant runs where a skill was available but never uptaken.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F79egthpwmqvx7h62kamj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F79egthpwmqvx7h62kamj.jpg" alt="A 3D grid matrix visualizing evaluation conditions: Models on the vertical axis, Skill Conditions on the depth axis, and Samples on the horizontal axis. A callout box shows a grader model evaluating four facts, resulting in a score on a meter." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1-click Data Studio template setup
&lt;/h2&gt;

&lt;p&gt;We provide a pre-configured &lt;strong&gt;Master Dashboard Template&lt;/strong&gt; containing all pre-styled components used here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clone the master template
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;a href="https://datastudio.google.com/reporting/236e070d-6495-4849-9589-28e645cc7838" rel="noopener noreferrer"&gt;&lt;strong&gt;Inspect AI Dashboard Template&lt;/strong&gt;&lt;/a&gt;.
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Make a copy&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9t4wwzbo6qqs8fjv28e3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9t4wwzbo6qqs8fjv28e3.png" alt="A screenshot of the Data Studio top-right dropdown menu showing the 'Make a copy' option highlighted." width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  30-second setup and customization
&lt;/h2&gt;

&lt;p&gt;Once cloned:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the new report, press &lt;strong&gt;Add data&lt;/strong&gt; 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzpctrswmr7s61n6o2fm4.png" alt="A screenshot of the Data Studio toolbar showing the 'Add data' button highlighted next to 'Add page' and 'Blend'." width="800" height="512"&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Google Sheets&lt;/strong&gt; 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwintu3mwn2hlasrimu4.png" alt="A screenshot of Data Studio's 'Add data to report' interface, with the Google Sheets connector highlighted among other Google connectors." width="800" height="612"&gt;
&lt;/li&gt;
&lt;li&gt;Select the Spreadsheet and the Worksheet (likely the sole Worksheet) you created in &lt;a href="https://dev.to/googleai/step-up-to-the-sheets-ai-eval-export-and-illustrating-data-bak"&gt;the third entry&lt;/a&gt; and press &lt;strong&gt;Add&lt;/strong&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fil7vtgy1umuvww4m95r7.png" alt="A screenshot of the 'AI Evals' spreadsheet and the 'data\_mega\_export' worksheet selection dialog, with 'Use first row as headers' checked." width="798" height="138"&gt;
&lt;/li&gt;
&lt;li&gt;Select all the components and set their data source to that sheet
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvqgz66x5pmgqb7z9xhjv.png" alt="A screenshot of the Data Studio canvas with components showing 'Missing data source' errors, and the 'Data source' panel open on the right showing 'AI Evals - data\_mega\_export' selected." width="799" height="255"&gt;
&lt;/li&gt;
&lt;li&gt;Finally, select the bubble chart and open the &lt;strong&gt;Style&lt;/strong&gt; tab, find the &lt;strong&gt;Bubble color&lt;/strong&gt; dropdown and set it to ‘model’
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1sppzsreere8tvjx8m0w.png" alt="A screenshot of Data Studio's 'Bubble chart properties' style settings, with the 'Bubble color' dropdown set to 'model'." width="800" height="549"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it! Your interactive dashboard is now live, fully bound to your evaluation data.&lt;/p&gt;

&lt;p&gt;Before diving into the new charts, check out the bottom left corner of the report. There you will find a Pivot Table Heatmap. Like the heatmap I made in the &lt;a href="https://dev.to/googleai/ai-evals-at-a-glance-heatmaps-for-stakeholders-2mki"&gt;second entry in this series&lt;/a&gt;, this heatmap is a 2D visual matrix where color intensity represents numerical magnitude—here, quadratic curved accuracy scores. Since I already discussed how to use these for analysis in the earlier blog entry, I will not be explaining it here. If, on the other hand, you are interested in implementation, feel free to inspect and adjust the settings on the Pivot Table Heatmap in your copied report. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1lu4bh5bije31nyvmgf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz1lu4bh5bije31nyvmgf.png" alt="A Data Studio Pivot Table Heatmap displaying score values across models (gemini-3.6-flash, gemini-3.5-flash-lite) and skill groups (gemini-api, gcloud), with cell shading representing score intensity." width="799" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: This screenshot was done on a later dataset than the rest of the report and the specific metrics should not be taken into account when comparing to the rest of the screenshots here.  &lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Overview of new pre-configured dashboard components
&lt;/h2&gt;

&lt;p&gt;Data Studio translates multi-dimensional evaluation CSV data into intuitive visual dimensions (spatial X/Y coordinates, bubble area scaling, color coding, and hover popovers). Here is how each pre-built dashboard element visually measures evaluation performance:&lt;/p&gt;

&lt;h3&gt;
  
  
  Chart A: Performance and cost (scatter/bubble chart)
&lt;/h3&gt;

&lt;p&gt;This chart maps three core evaluation dimensions onto a single 2D visual canvas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal Position (X-Axis ➔ Latency)&lt;/strong&gt;: Measures average task duration in seconds. Bubbles placed further to the right represent slower runs.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vertical Position (Y-Axis ➔ Score / Accuracy)&lt;/strong&gt;: Measures average task accuracy from &lt;code&gt;0.0&lt;/code&gt; to &lt;code&gt;1.0&lt;/code&gt;. Bubbles placed higher up represent better accuracy.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bubble Size / Area (Visual Cost &amp;amp; Unit Economics)&lt;/strong&gt;: Circle size scales with average task &lt;code&gt;cost&lt;/code&gt; for a quick visual gut-check.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hover Tooltips &amp;amp; Sub-Cent Formatting&lt;/strong&gt;: Hovering displays latency, accuracy, model, skill group, and cost formatted as &lt;code&gt;Currency &amp;gt; USD&lt;/code&gt; (&lt;code&gt;#,##0.0000&lt;/code&gt;) so sub-cent costs (e.g., &lt;code&gt;$0.0044&lt;/code&gt;) don't round to &lt;code&gt;$0.00&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Chart B: Accuracy and error bars (±1 SEM whiskers)
&lt;/h3&gt;

&lt;p&gt;The second pre-configured chart visualizes accuracy scores alongside standard error of the mean (SEM) whiskers.&lt;br&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr22y7axu6dfiff8rzqfi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr22y7axu6dfiff8rzqfi.png" alt="A column chart in Data Studio comparing the average accuracy scores of google/gemini-3.5-flash-lite and google/gemini-3.6-flash, complete with standard error of the mean (SEM) whiskers showing upper and lower bounds." width="782" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual Whisker Bounds&lt;/strong&gt;: The bar height shows average accuracy (&lt;code&gt;score&lt;/code&gt;), while floating dots above and below show upper (&lt;code&gt;score_lower&lt;/code&gt;) and lower (&lt;code&gt;score_upper&lt;/code&gt;) standard error bounds.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design Note (Multi-Metric vs. Breakdown Dimension Constraint)&lt;/strong&gt;: Data Studio won't let you use a Breakdown Dimension if you're plotting multiple Y-axis metrics on the same chart. Because our whiskers need three Y-metrics (&lt;code&gt;score&lt;/code&gt;, &lt;code&gt;score_lower&lt;/code&gt;, &lt;code&gt;score_upper&lt;/code&gt;), we put &lt;code&gt;model&lt;/code&gt; on the X-axis and filter by &lt;code&gt;skill_group&lt;/code&gt; using top-level drop-downs instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Statistical Rigor Note:&lt;/em&gt; Standard error tracks pass/fail score variation across runs. Our demo (

&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;N&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;4&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
) covers a ~68% confidence range (
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;±&lt;/span&gt;&lt;span class="mord"&gt;1&lt;/span&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;&amp;nbsp;SEM&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
), while production benchmarks require 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;N&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;≥&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;30&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 for standard 95% confidence intervals.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  The 4 dynamic cohort slicers (drop-down controls)
&lt;/h3&gt;

&lt;p&gt;Located across the top control bar, these four drop-down lists act as interactive dynamic filters, immediately re-rendering both canvas charts in real time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;model&lt;/code&gt; Slicer&lt;/strong&gt;: Filters results by model family (&lt;code&gt;gemini-3.5-flash-lite&lt;/code&gt; vs &lt;code&gt;gemini-3.6-flash&lt;/code&gt;).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;skill_group&lt;/code&gt; Slicer&lt;/strong&gt;: Filters by skill setup (&lt;code&gt;gemini-api&lt;/code&gt;, &lt;code&gt;gcloud&lt;/code&gt;, baseline control).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;is_baseline_or_has_activated_skill&lt;/code&gt; Slicer (Conditional Uptake)&lt;/strong&gt;: Separates &lt;strong&gt;Baseline controls&lt;/strong&gt; and &lt;strong&gt;Active Skill runs&lt;/strong&gt; (&lt;code&gt;TRUE&lt;/code&gt; via &lt;code&gt;activate_skill&lt;/code&gt;) from &lt;strong&gt;Dormant runs&lt;/strong&gt; (&lt;code&gt;FALSE&lt;/code&gt;, offered but uncalled). Activation confirms tool uptake, not instruction compliance.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;completed&lt;/code&gt; Slicer (Conditional Accuracy / Infrastructure Noise)&lt;/strong&gt;: Filters clean runs (&lt;code&gt;TRUE&lt;/code&gt;) from system failures (&lt;code&gt;FALSE&lt;/code&gt; for quota drops, timeouts, container crashes).

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Relevance Note:&lt;/em&gt; This is vital for production sweeps hit by rate limits or timeouts—selecting &lt;code&gt;TRUE&lt;/code&gt; measures &lt;strong&gt;Model Capability&lt;/strong&gt; (&lt;strong&gt;Conditional Accuracy&lt;/strong&gt;), while &lt;code&gt;FALSE&lt;/code&gt; or &lt;code&gt;(All)&lt;/code&gt; shows the full audit trail.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  View and interact
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Click the blue &lt;strong&gt;View&lt;/strong&gt; button in the top right corner to see the dashboard as a user would.
&lt;/li&gt;
&lt;li&gt;Use the &lt;strong&gt;model&lt;/strong&gt;, &lt;strong&gt;skill_group&lt;/strong&gt;, &lt;strong&gt;completed&lt;/strong&gt;, and &lt;strong&gt;is_baseline_or_has_activated_skill&lt;/strong&gt; drop-downs at the top.
&lt;/li&gt;
&lt;li&gt;Notice how &lt;strong&gt;both&lt;/strong&gt; the Bubble Chart and the Bar Chart update instantly in unison to show only the models or skills you selected!&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Slicer-based analyses
&lt;/h3&gt;

&lt;p&gt;While the &lt;code&gt;model&lt;/code&gt; slicer would be useful in a situation with more models, since we only have two and you can visualize them using the existing coloring. &lt;code&gt;skill_group&lt;/code&gt; on the other hand is more interesting&lt;br&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwrortsg5oge4o41srqb4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwrortsg5oge4o41srqb4.png" alt="A screenshot showing the 'skill\_group' dropdown filter panel displaying options for gcloud and gemini-api, placed next to a scatter plot bubble chart." width="799" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On selecting gemini-api (Skill) and gemini-api (Baseline), there are a couple visually apparent trends. As discussed in previous sections, the gemini-api (Skill) run improved the evaluation score at the price of increased latency and cost.&lt;/p&gt;
&lt;h4&gt;
  
  
  Conditional uptake analysis: Separating skill discovery from capability
&lt;/h4&gt;

&lt;p&gt;By filtering for runs where &lt;code&gt;is_baseline_or_has_activated_skill&lt;/code&gt; is &lt;code&gt;FALSE&lt;/code&gt;, we perform a Conditional uptake analysis: examining dormant runs where a skill was provided in context (Prior), but the agent chose not to call &lt;code&gt;activate_skill&lt;/code&gt; (zero uptake).&lt;/p&gt;
&lt;h5&gt;
  
  
  Baseline or has activated skill = false (notice these both score relatively low)
&lt;/h5&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0hspjuzajlovggor2x8b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0hspjuzajlovggor2x8b.png" alt="A scatter plot bubble chart showing two orange bubbles representing dormant runs of google/gemini-3.5-flash-lite, plotting score against latency." width="799" height="514"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h5&gt;
  
  
  Baseline or has activated skill = true
&lt;/h5&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcfolz868zqq9c6wkv5b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcfolz868zqq9c6wkv5b.png" alt="A scatter plot bubble chart showing multiple blue and orange bubbles for completed runs, plotting score against latency." width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One thing to note immediately is that the total number of bubbles is GREATER than 8 (the count of Model x Skill combinations). This is because each of these bubbles are subsets of the Model x Skill x Sample x Epoch space in that &lt;strong&gt;this distinction is made at the samples/epochs level&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In Inspect AI telemetry, "activated" specifically measures &lt;strong&gt;uptake&lt;/strong&gt;—whether the model executed the &lt;code&gt;activate_skill&lt;/code&gt; tool call to load the skill into context. Interestingly, the samples of 3.5-flash-lite where the skills were activated have a much higher visual ceiling than the general case (Baseline or Has Activated Skill = Any) corresponding to the elimination of Baseline or Has Activated Skill = False.&lt;/p&gt;

&lt;p&gt;Noting that only 3.5-flash-lite had samples where it didn’t activate a provided skill, it may be worth investigating if this is a more general trend. &lt;/p&gt;
&lt;h4&gt;
  
  
  Survivorship bias and joint operational uptime (&lt;code&gt;completed&lt;/code&gt; analysis)
&lt;/h4&gt;

&lt;p&gt;For the sake of further exploration, I reran the evals with web search enabled and the same time limit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;inspect &lt;span class="nb"&gt;eval &lt;/span&gt;skills-eval.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; google/gemini-3.5-flash-lite,google/gemini-3.6-flash &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--time-limit&lt;/span&gt; 300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--epochs&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-tasks&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nv"&gt;web_access&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running all of these evals serially and in parallel causes them to use too much web search quota and reach the unforgiving &lt;code&gt;time-limit&lt;/code&gt;. As such, &lt;strong&gt;the most realistic run configuration would be to increase the time limit and use the built-in adaptive rate limits.&lt;/strong&gt; The problem is this would last a lot longer and take more resources than I care to ask you to emulate.&lt;/p&gt;

&lt;p&gt;So let’s look instead at the results of the web search enabled, 300 second time limit run:&lt;/p&gt;

&lt;h4&gt;
  
  
  completed: ALL
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvbl4jmgxy6ybtzoruosk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvbl4jmgxy6ybtzoruosk.png" alt="A scatter plot bubble chart showing several blue and orange bubbles representing all runs (both completed and system failures) with web search enabled." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since this graph contains every model x skill x sample x epoch, you can eyeball the average metric values of a number of bubbles by imagining the centroid of the polygon they define on the space. &lt;/p&gt;

&lt;p&gt;Some of the most obvious conclusions in our local web-search enabled eval sweep: 3.5-flash-lite recorded lower latency than 3.6-flash, while their average scores were comparable. To reason about what this might mean, refer to the &lt;a href="https://ai.google.dev/gemini-api/docs/models" rel="noopener noreferrer"&gt;Gemini API Model Documentation&lt;/a&gt; for official model latency, throughput, and performance specifications. Instinctually, this makes me want to check if the timed out runs (counted as score 0) affected 3.6-flash more (since it didn’t complete as quickly in these samples). To test this, let’s see how scores look when we only count completed runs.&lt;/p&gt;

&lt;h4&gt;
  
  
  completed: TRUE
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6i6jk8pc0mvq8osjcxc9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6i6jk8pc0mvq8osjcxc9.png" alt="A scatter plot bubble chart showing completed runs only, where the average score for the google/gemini-3.6-flash cohort is slightly higher than in the completed ALL chart." width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When filtering the web-search rerun sweep strictly for completed runs (&lt;code&gt;completed = TRUE&lt;/code&gt;), the average score for the 3.6-flash sample cohort trended slightly higher, converging between 0.5 and 0.6 for partially scoring runs (which earlier averaged between 0.35 and 0.60 when &lt;code&gt;completed = ALL&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Isolating &lt;code&gt;completed = TRUE&lt;/code&gt; calculates &lt;strong&gt;Conditional Accuracy&lt;/strong&gt; (Model Capability on completed runs). However, if longer or more complex tasks disproportionately trigger time-limits, filtering out incompletes introduces &lt;strong&gt;Survivorship Bias&lt;/strong&gt;, truncating the difficult tail of your test set. Comparing &lt;code&gt;completed = ALL&lt;/code&gt; against &lt;code&gt;completed = TRUE&lt;/code&gt; bridges the gap between pure model intelligence and total operational system reliability for given limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are these findings generalizable to every scenario?
&lt;/h3&gt;

&lt;p&gt;While we’ve uncovered a promising trend, the next step is to validate it with statistical rigor. Think of our current demo (
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;N&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;4&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
) as an exploratory pilot; it’s great for spotting trends, but we need more data to confirm them. To verify these results aren't coincidental, I encourage you to scale your runs to 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;N&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;≥&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;30&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
. With this larger sample size, you can achieve &lt;strong&gt;statistical significance&lt;/strong&gt;, a metric used to ensure that your findings aren't just a random outlier. This is supported by the &lt;strong&gt;p-value&lt;/strong&gt;—a standard calculation that gives you the probability that your results are a fluke. Aiming for a low p-value is the final step in moving from exploratory analysis to building the mathematically sound, launch-ready benchmark required for production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion and next steps: Where to go from here
&lt;/h2&gt;

&lt;p&gt;We’ve come full circle in this series—from running terminal-based evaluation sweeps with Inspect AI in &lt;a href="https://dev.to/googleai/designing-ai-evals-clarity-now-and-visualization-next-4eii"&gt;Part 1&lt;/a&gt;, to analyzing multi-turn LLM reasoning traces in &lt;a href="https://dev.to/googleai/ai-evals-at-a-glance-heatmaps-for-stakeholders-2mki"&gt;Part 2&lt;/a&gt;, modeling tabular metrics with &lt;code&gt;pandas&lt;/code&gt; in &lt;a href="https://dev.to/googleai/step-up-to-the-sheets-ai-eval-export-and-illustrating-data-bak"&gt;Part 3&lt;/a&gt;, and finally assembling interactive, codeless BI dashboards here in Data Studio.&lt;/p&gt;

&lt;p&gt;Now that you have the end-to-end telemetry pipeline running, the floor is yours to explore! Here are a few directions you can take your evaluation harness next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom Calculated Metrics&lt;/strong&gt;: Experiment with custom formulas in Data Studio to track ROI metrics, cost per successful task, or normalized token efficiency ratios.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expanded Matrix Sweeps&lt;/strong&gt;: Scale your evaluation matrix across new LLM provider endpoints, prompt techniques, or tool playbooks.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct Database Connectivity&lt;/strong&gt;: Replace the static CSV / Google Sheets ingestion layer with a direct connector to BigQuery, PostgreSQL, or Cloud Storage for automated, streaming evaluation updates.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Alerting &amp;amp; Thresholds&lt;/strong&gt;: Set up automated notifications when a new model deployment drops below baseline accuracy or exceeds latency/cost budgets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thank you for following along with this series, and happy evaluating!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>datascience</category>
      <category>productivity</category>
      <category>llm</category>
    </item>
    <item>
      <title>Step up to the Sheets: AI Eval Export and Illustrating Data</title>
      <dc:creator>Katie McLaughlin</dc:creator>
      <pubDate>Tue, 01 Sep 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/googleai/step-up-to-the-sheets-ai-eval-export-and-illustrating-data-bak</link>
      <guid>https://dev.to/googleai/step-up-to-the-sheets-ai-eval-export-and-illustrating-data-bak</guid>
      <description>&lt;p&gt;Welcome to part three of our series about designing, analyzing and visualizing AI Evals! As with the previous entries you are more than welcome to simply read over my exploration or for those of you looking for extra credit, feel free to play along at home.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: This blog series contains AI-generated diagrams alongside actual screenshots and hand-drawn edits of both. AI also assisted in minor copy editing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Implementation prerequisites
&lt;/h2&gt;

&lt;p&gt;If you intend to (re)implement the walkthrough, all you need to do right now is complete Part 1 (&lt;a href="https://dev.to/googleai/designing-ai-evals-clarity-now-and-visualization-next-4eii"&gt;Designing AI Evals: Clarity Now and Visualization Next&lt;/a&gt;) and then install &lt;code&gt;pandas&lt;/code&gt;; Part 2 (&lt;a href="https://dev.to/googleai/ai-evals-at-a-glance-heatmaps-for-stakeholders-2mki"&gt;AI Evals at a Glance: Heatmaps for Stakeholders&lt;/a&gt;) is illustrative and will give you a deeper understanding going forward both of AI evals and why the following may be useful but it’s more elective than anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to the story at hand
&lt;/h2&gt;

&lt;p&gt;While engineering teams might evaluate agents using test automations, non-technical stakeholders (product managers, business leads, or executive sponsors) often use spreadsheets as their lingua franca and as such it would make sense to speak to them in a way they are almost certain to understand and which enables you all to collaboratively make visualizations to further get out the word.&lt;/p&gt;

&lt;p&gt;To make this a reality, today we’ll use a &lt;code&gt;pandas&lt;/code&gt; pipeline that transforms raw Inspect AI evaluation logs into a rich CSV optimized for Google Sheets, allowing you or anyone on your team with a little spreadsheet knowledge to make boardroom-ready visuals.&lt;/p&gt;

&lt;p&gt;Drawing again from previous blog entries, the evals we are working on have three dimensions: model x skill x sample [and epoch] that cross to form the following matrix:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fosimtffx3o7lgyyj1jc2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fosimtffx3o7lgyyj1jc2.png" alt="A 3D grid matrix visualizing evaluation conditions: Models on the vertical axis, Skill Conditions on the depth axis, and Samples on the horizontal axis. A callout box shows a grader model evaluating four facts, resulting in a score on a meter." width="800" height="769"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/googleai/ai-evals-at-a-glance-heatmaps-for-stakeholders-2mki"&gt;Part 2&lt;/a&gt; we created a simplified heatmap reflecting a planar slice of this space as reduced over the sample dimension. While we will ultimately do something similar in a future blog post using Data Studio, for the time being we actually want to keep all 32 combinations separate and pull more of their underlying stats from the &lt;code&gt;inspect eval&lt;/code&gt; files and derive further information using &lt;code&gt;pandas&lt;/code&gt;. This information will be exported as a flat csv file we can then import and view in Google Sheets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The extractor &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/agents/inspect-agent-skills-eval/tocsv.py" rel="noopener noreferrer"&gt;&lt;code&gt;tocsv.py&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;This script is used to parse the eval logs using &lt;code&gt;Inspect AI&lt;/code&gt;’s python &lt;code&gt;pandas&lt;/code&gt; interface. The script is relatively self documenting, and you can &lt;a href="https://pandas.pydata.org/docs/user_guide/10min.html" rel="noopener noreferrer"&gt;read up more on pandas&lt;/a&gt; and &lt;a href="https://pandas.pydata.org/docs/user_guide/io.html#" rel="noopener noreferrer"&gt;pandas’ csv IO&lt;/a&gt; if you are unfamiliar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Heavy lifting in Python: Custom telemetry columns
&lt;/h3&gt;

&lt;p&gt;While most of the script deals with mapping scalar metrics the like of which we’ve already discussed, there are two that are purely logical/boolean. They represent specific statistical cases that we will allow end users to partition the data set over for analysis later. These boolean conditions are derived and since we want to pare away excess information and therefore not supply the full eval files to Google Sheets, &lt;code&gt;tocsv.py&lt;/code&gt; does the heavy lifting upstream during log ingestion.&lt;/p&gt;

&lt;p&gt;It pre-computes two key boolean columns to simplify downstream dashboard filtering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;completed&lt;/code&gt;&lt;/strong&gt;: Flags whether a sample finished cleanly (no API quota 429 errors, timeout truncations, or container crashes) with a valid score, cleanly isolating model capability from infrastructure noise.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;is_baseline_or_has_activated_skill&lt;/code&gt;&lt;/strong&gt;: Distinguishes &lt;strong&gt;Baseline control cohorts&lt;/strong&gt; (always &lt;code&gt;TRUE&lt;/code&gt;) and &lt;strong&gt;Active Skill interventions&lt;/strong&gt; (&lt;code&gt;TRUE&lt;/code&gt; when the skill tool was actually “activated” which in &lt;code&gt;inspect&lt;/code&gt; means uptaken) from &lt;strong&gt;Dormant runs&lt;/strong&gt; (&lt;code&gt;FALSE&lt;/code&gt;, where skills were available but never activated).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Google Sheets or Data Studio non-technical stakeholders can simply filter/toggle these pre-computed boolean keys to isolate clean, active performance (or otherwise) instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exporting to Google Sheets
&lt;/h2&gt;

&lt;p&gt;In order to execute the pipeline I called:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 tocsv.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output file &lt;code&gt;data_mega_export.csv&lt;/code&gt; is a standard comma-separated values file where each cell is surrounded by commas. You can preview the raw and unformatted contents in a standard text editor. While most of this is normal spreadsheet data such as strings and integers, you will also see fairly complicated Google Sheets formulas resulting from interpolating eval metrics into formula templates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnvzqm5o95s8ac43ups37.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnvzqm5o95s8ac43ups37.png" alt="A screenshot showing raw text lines of a CSV export, highlighting a complex Google Sheets formula involving SPARKLINE and PERCENTILE functions used to generate dynamic bar charts in the sheet." width="800" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The above formula for instance will be transformed into a bar graph normalized against the range of values in the same column as its source data and tiered into colors by tertile. &lt;/p&gt;

&lt;p&gt;How do you take this text and get it to show its stuff? By opening &lt;a href="https://sheets.new" rel="noopener noreferrer"&gt;sheets.new&lt;/a&gt; of course, and importing &lt;code&gt;data_mega_export.csv&lt;/code&gt;. When presented with the import dialog check &lt;strong&gt;Convert text to numbers, dates, and formulas&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr2m83vhwiinwg0cvcbqg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr2m83vhwiinwg0cvcbqg.png" alt="A screenshot of the Google Sheets Import file dialog for data\_mega\_export.csv, with the checkbox " width="799" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On clicking &lt;strong&gt;Import data&lt;/strong&gt; Google Sheets will instantly display the standard cells and render active, color-coded visual sparklines directly inside data cells!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8y9jhes042pv9emzwddo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8y9jhes042pv9emzwddo.png" alt="A screenshot of the imported data in Google Sheets, displaying columns like model, skill\_group, score, and latency, with inline yellow and green sparkline bar charts rendering directly inside the cells." width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What conclusions can you draw from what you see? One obvious one in this example is that the correlation between score and latency (how long an agent takes to answer a question) may not be very strict. But what if you wanted to get something deeper?&lt;/p&gt;

&lt;p&gt;Well, since this is a spreadsheet, obviously you can make pivot tables, and custom functions or sort on multiple keys… you know, the whole accountant’s playbook. Let’s say though that you want custom views into the data rather than to permanently mutate it. How would you go about doing that? &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgkanon14o38dtgmkh2os.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgkanon14o38dtgmkh2os.png" alt="A screenshot of the Google Sheets top menu under " width="788" height="594"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Google Sheets, unlike &lt;code&gt;inspect view&lt;/code&gt; and &lt;code&gt;inspect viz&lt;/code&gt; heatmaps, allows you to dynamically sort AND filter on more than one key. Not only that but you can sort on ANY key, so I elected to do so with the previously unavailable &lt;code&gt;score&lt;/code&gt; and &lt;code&gt;latency&lt;/code&gt; fields. As shown in the image above, use &lt;strong&gt;Data&lt;/strong&gt; &amp;gt; &lt;strong&gt;Create filter view&lt;/strong&gt; which presents you with a new set of controls at the top of the main columns.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Filter views do not mutate or change the underlying contents of a sheet; Instead they are (optionally named and shareable) configurations for restricting what rows are displayed and in what order.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxbjv901ugvtpwkkyroui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxbjv901ugvtpwkkyroui.png" alt="A screenshot showing an active " width="799" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If, as in the picture, you click the &lt;code&gt;cost&lt;/code&gt; column &amp;gt; &lt;strong&gt;Filter by condition&lt;/strong&gt;, select the  &lt;strong&gt;Less than&lt;/strong&gt; option and fill the comparison with 0.01, you can narrow the displayed rows to only those with costs less than a cent. This can be used (for instance) to only look over the samples that were “less costly” to try to analyze why.&lt;/p&gt;

&lt;p&gt;Filter views (despite their name) also allow you to sort the display of the data. Because the sort operations are cumulative, the “primary” filter needs to be applied last. Thus, after sorting the filter view first by &lt;strong&gt;latency&lt;/strong&gt; and then by &lt;strong&gt;score&lt;/strong&gt; (both ‘Z to A’ aka decreasing) gives us the following: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fddq6a27tij7c9cozbfdj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fddq6a27tij7c9cozbfdj.png" alt="A screenshot of " width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Optionally you can name and save this filter view to toggle it on and off as well as share it with other collaborators. To learn more about Filter views, you can read &lt;a href="https://support.google.com/docs/answer/3540681?hl=en&amp;amp;co=GENIE.Platform%3DDesktop" rel="noopener noreferrer"&gt;Sort &amp;amp; filter your data&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;This ability to sort and filter the display live without changing the underlying information, makes presenting the data and analyzing it during discussions with teammates significantly easier and faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why leave the cradle?
&lt;/h3&gt;

&lt;p&gt;Now, in spite of this tremendous flexibility, sheets have some drawbacks as visualization tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Spreadsheet formulas are cumbersome for nested IF statements and filters unless you define custom macros or JavaScript functions leading to code that's harder and harder to keep connected in one's mental model.
&lt;/li&gt;
&lt;li&gt;Flattened eval runs need to be inspected under multiple filter views to begin to understand complex trends or correlations across all of them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So, how can we take this to the next level and circumvent those issues? What about an environment, which while it still offers you the ability to custom script anything you need, comes with a lot of WYSIWYG controllable visualizations and graphs, enabling you, without code, to bring data to an even more vibrant life? Better yet, what if that environment comes with built in visualizations for 3 dimensions or more and has broad compatibility with multiple data source types?&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 4&lt;/strong&gt;, we’ll connect this Google Sheet directly to Data Studio to build a codeless executive summary dashboard and be able to quickly and intuitively display our findings! &lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>datascience</category>
      <category>productivity</category>
    </item>
    <item>
      <title>AI Evals at a Glance: Heatmaps for Stakeholders</title>
      <dc:creator>Katie McLaughlin</dc:creator>
      <pubDate>Tue, 25 Aug 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/googleai/ai-evals-at-a-glance-heatmaps-for-stakeholders-2mki</link>
      <guid>https://dev.to/googleai/ai-evals-at-a-glance-heatmaps-for-stakeholders-2mki</guid>
      <description>&lt;h2&gt;
  
  
  Visualizing AI evals with Inspect Viz
&lt;/h2&gt;

&lt;p&gt;Welcome back to our blog series on running, analyzing and visualizing AI evals. &lt;a href="https://dev.to/googleai/designing-ai-evals-clarity-now-and-visualization-next-4eii"&gt;Last time&lt;/a&gt; we discussed how to design and run evals for analysis and visualization using &lt;code&gt;inspect eval&lt;/code&gt; and &lt;code&gt;inspect view&lt;/code&gt;. Many of these methods, such as rearranging the dashboard columns and sorting on them to determine model vs model and skill vs no-skill differences in metrics, can give you a broad overview of the field. While this can inspire further and deeper inquiry, it runs into issues with how predictive it is and how to communicate findings to other people. Imagine the following:&lt;/p&gt;

&lt;h3&gt;
  
  
  The 40-log gridlock
&lt;/h3&gt;

&lt;p&gt;Imagine a data science lead needing to present a performance breakdown of four LLM candidates across ten internal tools during a high-stakes, time-crunched live meeting. Reordering and filtering the evals across multiple dimensions is hard to read on a presentation screen and requires doing mental math with an audience: riveting stuff that they definitely won’t fall asleep during of course.&lt;/p&gt;

&lt;p&gt;What if instead you had a tool that allowed you to easily automate rendering a comparison of the different configurations? What if lining them up by model or by skill showed clear and legible patterns?&lt;/p&gt;

&lt;p&gt;Well, let me introduce you to &lt;strong&gt;heatmaps&lt;/strong&gt;! A heatmap is a 2D visual matrix where color intensity represents numerical magnitude—in our case, quadratic curved accuracy scores. By leveraging the human eye's natural ability to distinguish color gradients, heatmaps instantly reveal performance spectrums at a single glance. This makes trends across dependent and independent variables clear, intuitive, and highly legible.&lt;/p&gt;

&lt;p&gt;While you can simply read along and learn about using &lt;code&gt;inspect viz&lt;/code&gt; to generate heatmaps for inspect AI evals, if you intend to follow along and recreate the charts, please start by replicating the steps in the &lt;a href="https://dev.to/googleai/designing-ai-evals-clarity-now-and-visualization-next-4eii"&gt;first entry&lt;/a&gt; and come back afterward.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: This blog series contains AI-generated diagrams alongside actual screenshots and hand-drawn edits of both. AI also assisted in minor copy editing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Follow-along requirements
&lt;/h2&gt;

&lt;p&gt;For those playing along at home, in order to perform the following, you will need the &lt;code&gt;inspect_viz&lt;/code&gt; library installed in your environment (alongside &lt;code&gt;inspect_ai&lt;/code&gt; and &lt;code&gt;pandas&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;From there, take a look at the &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/agents/inspect-agent-skills-eval/inspect_viz_heatmap.py" rel="noopener noreferrer"&gt;heatmap script&lt;/a&gt;. While there are some intricacies to the script (such as establishing its CLI API) the script mainly wraps &lt;code&gt;scores_heatmap(viz_data, orientation="horizontal", ...)&lt;/code&gt; and marshals our data to assemble a heatmap of it.&lt;/p&gt;

&lt;p&gt;If you run the script&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 inspect_viz_heatmap.py logs &lt;span class="nt"&gt;-o&lt;/span&gt; heatmap.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you will get a visual that looks like the following.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwahzh78quzwkcq5aer3t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwahzh78quzwkcq5aer3t.png" alt="A heatmap matrix with model names on the y-axis and skill names on the x-axis, using color intensity from purple to yellow to represent accuracy scores." width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This heatmap has two axes, one is the &lt;strong&gt;model&lt;/strong&gt; (vertical) which allows you to visually break down your analysis to just skill inclusions for the same model, the other is the &lt;strong&gt;skills&lt;/strong&gt; (horizontal) and going down the row allows you to demonstrate how changes to the model affect the evaluation.&lt;/p&gt;

&lt;p&gt;Stakeholders (like investors or your boss) can now spot skill gains and model ceilings at a glance without reading line-by-line log values. Some quick takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top-performing configurations (such as 3.6-flash + &lt;code&gt;gemini_api_skill&lt;/code&gt; at &lt;strong&gt;1.0&lt;/strong&gt;) stand out in high-intensity shades
&lt;/li&gt;
&lt;li&gt;Within the aggregated 2D heatmap view of this dataset, &lt;code&gt;3.6-flash&lt;/code&gt; trended higher in score on average across the evaluated skill configurations compared to &lt;code&gt;3.5-flash-lite&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;Skills tended to improve scores, all other things held equal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwvjbu92rzpou4aavxzu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwvjbu92rzpou4aavxzu.png" alt="A 3D grid matrix visualizing eval conditions: Models on the vertical axis, Skill Conditions on the depth axis, and Samples on the horizontal axis." width="800" height="769"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caption&lt;/strong&gt;: The &lt;code&gt;inspect viz&lt;/code&gt; heatmap projects our full Evaluation Matrix down to a 2D plane by collapsing individual task samples and epochs into aggregate accuracy cells along the Model and Skill axes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cognitive overhead and segue to BI analytics
&lt;/h3&gt;

&lt;p&gt;While matrix scoreboards provide quick visibility into accuracy differences across models, raw heatmaps suffer from key limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2D Constraint&lt;/strong&gt;: Heatmaps map only two axes to a single color intensity (in our case Accuracy).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing Business Context&lt;/strong&gt;: A single heatmap cannot simultaneously show token costs, system latency, or operational reliability.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden Variance&lt;/strong&gt;: Collapsing samples for a given configuration removes the information necessary to determine if a result is consistent or wildly erratic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To bridge CLI logs with interactive, multidimensional and in-depth reporting, we need a pipeline that exports the raw telemetry metrics into spreadsheets and BI dashboards.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 3&lt;/strong&gt;, we’ll  build a python script to convert output data for processing first in Google Sheets and ultimately Data Studio!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>datascience</category>
      <category>python</category>
    </item>
    <item>
      <title>Designing AI Evals: Clarity Now and Visualization Next</title>
      <dc:creator>Katie McLaughlin</dc:creator>
      <pubDate>Tue, 18 Aug 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/googleai/designing-ai-evals-clarity-now-and-visualization-next-4eii</link>
      <guid>https://dev.to/googleai/designing-ai-evals-clarity-now-and-visualization-next-4eii</guid>
      <description>&lt;h2&gt;
  
  
  AI evals and analysis
&lt;/h2&gt;

&lt;p&gt;Let's say you're testing out new AI tools. Perhaps you implement and run analytics for an Ad Agency and hope to automate deploying your standard event schema, or are a podcast producer automating generating social copy from your newest ep. While modern, newly trained LLMs can likely one-shot a lot of these tasks – this specificity might necessitate wasting tokens and time repeatedly prompting them with the same resources, descriptions and scripts. With this in mind, you investigate tooling whether that be an MCP server, an agent skill or an &lt;a href="https://agent-plugins.org/specification" rel="noopener noreferrer"&gt;agent plugin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The problem is, how do you know if a skill (whether developed by you or open sourced by someone else) is worth your time or, perhaps more importantly, your tokens and quota? How can one go about designing these (more) objective evaluations of AI tools and, from there, collecting and analyzing relevant metrics? That is exactly the context for using open source eval frameworks, like &lt;a href="https://inspect.aisi.org.uk/" rel="noopener noreferrer"&gt;Inspect AI&lt;/a&gt; and &lt;a href="https://github.com/harbor-framework/harbor" rel="noopener noreferrer"&gt;Harbor&lt;/a&gt; to &lt;a href="https://codelabs.developers.google.com/codelabs/evaluate-agent-skills-using-open-source-frameworks" rel="noopener noreferrer"&gt;evaluate agent skills using open source frameworks&lt;/a&gt;. But how do you &lt;a href="https://codelabs.developers.google.com/codelabs/evaluate-agent-skills-using-open-source-frameworks#4" rel="noopener noreferrer"&gt;"extend the evaluation"&lt;/a&gt;, use visualizations to spot trends and collaboratively explore alternative paths forward using Google Sheets and Data Studio?&lt;/p&gt;

&lt;p&gt;These questions and more are exactly what I’m hoping to demonstrate for you in this series!&lt;/p&gt;

&lt;p&gt;While you are more than welcome to simply read about how and why I conducted my own investigation, you can alternatively follow my lead and &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/agents/inspect-agent-skills-eval/README.md#reproduction-guide" rel="noopener noreferrer"&gt;run the benchmark scripts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, if you’re hoping to play along, take a moment to &lt;a href="https://codelabs.developers.google.com/codelabs/evaluate-agent-skills-using-open-source-frameworks" rel="noopener noreferrer"&gt;complete the aforementioned codelab&lt;/a&gt; and come back when you’re done. Don’t worry, we’ll still be here when you get back!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This blog series contains AI-generated diagrams&lt;br&gt;
alongside actual screenshots and hand-drawn edits of&lt;br&gt;
both. AI also assisted in minor copy editing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Level up your evals
&lt;/h2&gt;

&lt;p&gt;In the codelab, we learned how to run evals with Gemini CLI, Inspect and Inspect SWE in an isolated Docker Sandbox to understand how well each skill aids the agent in answering the same question.&lt;/p&gt;

&lt;p&gt;For those playing along at home, please additionally install &lt;code&gt;inspect view&lt;/code&gt; now before you need it to run the evals below; be prepared to have to wait for a couple minutes for evals to finish depending on your machine and quota usage.&lt;/p&gt;

&lt;p&gt;Regarding our new &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/tree/main/agents/inspect-agent-skills-eval#downloading-the-demo-giget" rel="noopener noreferrer"&gt;source files&lt;/a&gt; while they are heavily commented and hopefully written in a self describing way, I’ll explain further subsequently.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on models
&lt;/h3&gt;

&lt;p&gt;For the purpose of my investigation demo, I used three different models: &lt;code&gt;google/gemini-3.5-flash-lite&lt;/code&gt; and &lt;code&gt;google/gemini-3.6-flash&lt;/code&gt; as "solvers" (the models under evaluation), and &lt;code&gt;google/gemini-3.1-flash-lite&lt;/code&gt; as a "grader" (the model rating the runs). The three of them (and models more broadly) differ in many ways but more specifically on &lt;a href="https://ai.google.dev/gemini-api/docs/models" rel="noopener noreferrer"&gt;problem solving ability, speed and cost&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;We intentionally offloaded grading for this demo to a previous-generation model because by changing rubric criteria to strict binary decisions and applying a reduction programmatically, it delivers sufficiently robust evaluations without burning through solver quota. For a &lt;em&gt;production&lt;/em&gt; evaluation system, consider investigating using newer and more capable models as graders because they will likely have narrower confidence intervals.&lt;/p&gt;

&lt;p&gt;For a full technical breakdown of this decoupled grader architecture and to find out how you could substitute in your own choice of models, see the README section on &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/agents/inspect-agent-skills-eval/README.md#3-decoupled-grader--multidimensional-rubrics" rel="noopener noreferrer"&gt;Decoupled Grader &amp;amp; Multidimensional Rubrics&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the eval
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reproducibility &amp;amp; Local Setup Note&lt;/strong&gt;: If you are following along locally, clone the domain skill definitions into &lt;code&gt;google-skills/&lt;/code&gt; before running benchmark sweeps:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/google/skills.git google-skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;All evaluations in this series were benchmarked on &lt;strong&gt;Python 3.13&lt;/strong&gt; using &lt;code&gt;inspect-ai&lt;/code&gt; (&lt;code&gt;v0.3.247&lt;/code&gt;), &lt;code&gt;inspect-swe&lt;/code&gt; (&lt;code&gt;v0.2.66&lt;/code&gt;), &lt;code&gt;inspect-viz&lt;/code&gt; (&lt;code&gt;v0.4.1&lt;/code&gt;), and &lt;code&gt;pandas&lt;/code&gt; (&lt;code&gt;v3.0.3&lt;/code&gt;). If upstream PyPI releases introduce breaking changes, check the README's &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/agents/inspect-agent-skills-eval/README.md#environment--dependencies" rel="noopener noreferrer"&gt;Environment &amp;amp; Dependencies specification&lt;/a&gt; for exact version pins and instructions on how to reproduce the configuration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For this investigation I used a &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/agents/inspect-agent-skills-eval/skills-eval.py" rel="noopener noreferrer"&gt;new eval script&lt;/a&gt;; while the specifics of the script are important for anyone who hopes to make their own evals or run the code as we go, if you're primarily interested in analysis and visualization, feel free to skip to the next section.&lt;/p&gt;

&lt;p&gt;The original script ran a small batch of tests on a local machine. While this version can be and was run on a local machine, it prioritizes three architectural dimensions needed for running automated tests as part of a larger scale development flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;External Configs (providing the evals and solver system prompt as external files for separation of concerns).
&lt;/li&gt;
&lt;li&gt;Quota Management (for the GenAI API and package management).
&lt;/li&gt;
&lt;li&gt;Multidimensional Evaluations (expanding to arbitrary fact counts for graders).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a complete technical breakdown of these architectural pillars—including external configuration schemas (&lt;code&gt;questions.json&lt;/code&gt;, &lt;code&gt;thrifty_system_prompt.txt&lt;/code&gt;), solver rate-limiting defenses (&lt;code&gt;version="0.51.0"&lt;/code&gt; pinning), grader quota decoupling, and score curving math formulas see the README's &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/agents/inspect-agent-skills-eval/README.md#1-external-configurations" rel="noopener noreferrer"&gt;Evaluation Pipeline Architecture &amp;amp; Technical Reference&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the eval
&lt;/h2&gt;

&lt;p&gt;These commands create a matrix of evals subject to model x skill condition x sample x epoch. For a quick visual (that I’ll repeatedly reference throughout this), take a look below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fosimtffx3o7lgyyj1jc2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fosimtffx3o7lgyyj1jc2.png" alt="A 3D grid matrix visualizing eval conditions: Models on the vertical axis, Skill Conditions on the depth axis, and Samples on the horizontal axis. A callout box shows a grader model evaluating four facts, resulting in a score on a meter." width="800" height="769"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caption&lt;/strong&gt;: Visualization of the eval matrix that will be scored and compared. In Inspect AI, rubric elements, represented here as Facts, are graded Correct (C) or Incorrect (I).&lt;/p&gt;

&lt;p&gt;With all that said, it’s finally time for the rubber to meet the road. I ran our evals with the following command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;inspect &lt;span class="nb"&gt;eval &lt;/span&gt;skills-eval.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; google/gemini-3.5-flash-lite,google/gemini-3.6-flash &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--time-limit&lt;/span&gt; 300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--epochs&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-tasks&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nv"&gt;web_access&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command runs a 4-way parallel sweep across models and skill conditions while enforcing a 300-second task timeout and disabling web search to minimize token spend. See the README for a complete &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/agents/inspect-agent-skills-eval/README.md#cli-parameter-reference" rel="noopener noreferrer"&gt;CLI Parameter Reference&lt;/a&gt; and details on &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/agents/inspect-agent-skills-eval/README.md#1-external-configurations" rel="noopener noreferrer"&gt;External Configuration Template Mechanics&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run and wait
&lt;/h3&gt;

&lt;p&gt;If you intend to and haven't done so already, &lt;strong&gt;run the command line&lt;/strong&gt;. If you're interested in how to read over and interact with the Terminal User Interface, please refer to the &lt;a href="https://codelabs.developers.google.com/codelabs/evaluate-agent-skills-using-open-source-frameworks" rel="noopener noreferrer"&gt;previous codelab&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Watch the running command line long enough to determine it hasn’t crashed or hit an obvious error and then maybe take a moment to make some food, grab some water or take a walk around the block. Enjoy the small things in life, okay? Worst case, you get a 20 minute walk out of it, not so bad eh!?&lt;/p&gt;

&lt;h2&gt;
  
  
  Viewing the evals
&lt;/h2&gt;

&lt;p&gt;With that said and done, reading the terminal output is only the most basic way of understanding the evals. Now that you have the raw eval logs, what do you need to do to begin to use these analytically and, more importantly, persuasively?&lt;/p&gt;

&lt;p&gt;To start off, I simply ran&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;inspect view
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and clicked the link to open the browser to the GUI.&lt;/p&gt;

&lt;p&gt;For step-by-step diagnostic trace analysis in the browser GUI, see the README section on &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/agents/inspect-agent-skills-eval/README.md#local-diagnostic-trace-analysis-inspect-view" rel="noopener noreferrer"&gt;Local Diagnostic Trace Analysis (inspect view)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Mine came out like this (after a little bit of column drag-and-drop):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8mxn1iszjshhw6f1w7ki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8mxn1iszjshhw6f1w7ki.png" alt="A table showing the results of AI evaluation runs, with columns for status, task name, model used, score, token count, and duration." width="800" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While we covered the rudiments of looking over the inspect view GUI in &lt;a href="https://codelabs.developers.google.com/codelabs/evaluate-agent-skills-using-open-source-frameworks" rel="noopener noreferrer"&gt;Evaluate agent skills using open source frameworks&lt;/a&gt;, there is significantly more that can be done with it. Speaking of which, the &lt;strong&gt;TASK ARGS&lt;/strong&gt; column (not pictured) serves as a quick reference for what arguments were passed to a task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Macro view: Comparing runs
&lt;/h3&gt;

&lt;p&gt;While LLM’s are inherently stochastic (and given floating point hardware, nondeterministic), when running evals we are trying to use sampling to characterize the average observable metrics (e.g. correctness, latency and token usage) which our particular configuration model x skill scores on relevant questions. As such, the eval tasks were set up to characterize how a change to an independent variable (model or skill) affect dependent variables (the aforementioned metrics).&lt;/p&gt;

&lt;h4&gt;
  
  
  A. Comparing models head to head
&lt;/h4&gt;

&lt;p&gt;Grouping evaluation runs by model in &lt;code&gt;inspect view&lt;/code&gt; allows for a direct "eyeball" inspection of how skill inclusion alters accuracy across baseline controls—&lt;strong&gt;given a specific model, what is the impact of adding a skill?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I have added some annotations to make metric change from baseline to skilled a little more apparent:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvssywiosux6j1i7inxhe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvssywiosux6j1i7inxhe.png" alt="A table of AI evaluation results similar to the previous one, but with red up-arrows next to 'SCORE' values to indicate improvements when using skills compared to baseline." width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The above shows that&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In all cases, the addition of a skill improved or tied scores relative to the same model’s baseline.
&lt;/li&gt;
&lt;li&gt;All of these cases &lt;strong&gt;also&lt;/strong&gt; increased duration &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another simple measurement that can be done is to look at the &lt;code&gt;TOKENS&lt;/code&gt; column where gemini-api actually led to a decrease in usage relative to its baseline in &lt;code&gt;3.6-flash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;While the general relationship between tokens and score is difficult to parse visually, one notable result: for 3 out of 4 task x model configurations, gemini-3.6-flash used more tokens than the comparable 3.5-flash-lite run.&lt;/p&gt;

&lt;h3&gt;
  
  
  B. Comparing skill conditions (skill vs. baseline)
&lt;/h3&gt;

&lt;p&gt;Now to analyze the inverse; in more concrete terms: &lt;strong&gt;given a particular skill, what is the effect of changing the underlying model?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgo04b8da7cpec0hntywy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgo04b8da7cpec0hntywy.png" alt="A table of AI evaluation results comparing performance across models for a given skill condition, with annotations indicating score differences." width="799" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What was the effect of the change from &lt;code&gt;3.5-flash-lite&lt;/code&gt; to &lt;code&gt;3.6-flash&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;For 3 out of 4 of these, the changes between the two models amounted to an up to 45% increase in accuracy. For whatever reason, however, changing the &lt;code&gt;gcloud&lt;/code&gt; skill from using one model to another led to a slight decrease.&lt;/p&gt;

&lt;p&gt;While these are both valuable findings, they warrant further questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are these representative samples?
&lt;/li&gt;
&lt;li&gt;If these findings are repeatable, how can we characterize the relationship between the existing metrics?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the first of those, if you want to make more representative samples, know that you should conduct further research using more questions, samples and epochs and compare those metrics with these. For the sake of this blog series though, I’ll leave that for you to do.&lt;/p&gt;

&lt;p&gt;For the second though, we can and will endeavor to do so. That said, it might help in designing follow up analysis to think a little bit about common patterns encountered during evals. This is especially true for those running evals themselves (possibly on different skills or rubrics); you are likely to encounter very different metrics than mine, and thus I’ll lay out some common patterns and follow up actions to investigate them further.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mental models for eval comparison (skill vs. baseline)
&lt;/h3&gt;

&lt;p&gt;When analyzing evaluation runs, comparing skilled execution against baseline controls typically maps to five distinct diagnostic outcomes—ranging from &lt;strong&gt;High-Efficiency Capability Lift&lt;/strong&gt; (best) to &lt;strong&gt;Context Overload &amp;amp; Skill Regression&lt;/strong&gt; (worst).&lt;/p&gt;

&lt;p&gt;For a complete breakdown of this diagnostic taxonomy and actionable audit steps for each outcome, see the README breakdown of &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/agents/inspect-agent-skills-eval/README.md#diagnostic-mental-models-for-eval-comparison-skill-vs-baseline" rel="noopener noreferrer"&gt;Diagnostic Mental Models for Eval Comparison&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Micro view: Sample-level diagnostics
&lt;/h3&gt;

&lt;p&gt;While high-level metric summaries alert you to outcomes like cost bloat or skill regression, opening an individual sample surfaces the &lt;strong&gt;Sample Details&lt;/strong&gt; panel for granular trace analysis:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fivqmzntm7aij2fjsvjht.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fivqmzntm7aij2fjsvjht.png" alt="A panel showing a list of evaluation samples, each with an ID, epoch number, and the full input prompt text used for the evaluation." width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Transcript tab: How to read the LLM's conversation as it happened
&lt;/h4&gt;

&lt;p&gt;This tab shows the exact multi-turn conversation between the solver agent, the sandbox shell, and external tools. Use it to diagnose model reasoning versus environment noise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System Prompt Verification&lt;/strong&gt;: Confirm that web search rules (&lt;code&gt;-T web_access=false&lt;/code&gt;) and automated time limits (&lt;code&gt;300 seconds&lt;/code&gt;) were correctly injected into the container environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu67zc1uf65f256l54qz1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu67zc1uf65f256l54qz1.png" alt="A screenshot of a system instruction block labeled " width="800" height="273"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skill Ingestion Check&lt;/strong&gt;: Verify whether the agent activated the skill. If activation didn’t occur, your eval is &lt;em&gt;mostly&lt;/em&gt; testing baseline model knowledge rather than skill utility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is an example of one case where the skill was activated.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatx7xwn28rskhb5bj1t8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatx7xwn28rskhb5bj1t8.png" alt="A screenshot of a terminal-like interface showing a model call turn and a specific action: 'activate_skill' name: 'gemini-api' being executed." width="799" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This means:&lt;/strong&gt; The agent actually ingested &lt;code&gt;gemini-api&lt;/code&gt; instead of relying on baseline pre-training memory.&lt;/p&gt;

&lt;p&gt;Of note though, some of the tasks where skills were made available did NOT activate the skill. If you’re a skill author, you may want to rewrite the “activation criteria” (aka what situation calls for using the skill) such that it's more applicable to the specifics of relevant tasks.&lt;/p&gt;

&lt;h5&gt;
  
  
  Errors and reasoning loops
&lt;/h5&gt;

&lt;p&gt;Auditing transcript details allows you to distinguish model reasoning loops (e.g., repeated redundant tool calls) from sandbox environment noise (e.g., container timeouts or missing binary dependencies). For step-by-step diagnostic trace auditing procedures, see the README reference on &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/agents/inspect-agent-skills-eval/README.md#sandbox-noise-vs-model-reasoning" rel="noopener noreferrer"&gt;Sandbox Noise vs. Model Reasoning&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Scoring tab: Multi-fact verification breakdown
&lt;/h4&gt;

&lt;p&gt;Clicking the &lt;strong&gt;Scoring Tab&lt;/strong&gt; displays the empirical breakdown of our custom &lt;code&gt;multi_scorer&lt;/code&gt; on a sample:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqz48wt27uoj2ncldgzll.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqz48wt27uoj2ncldgzll.png" alt="A screenshot of a scoring interface showing the input prompt, a multi-scorer score of 0.65, and a breakdown of six individual fact scores, five of which are marked 'C' (Correct) and one 'I' (Incorrect)." width="799" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;multi_scorer&lt;/code&gt; aggregates the results of checking the sample’s answer against individual binary yes/no Facts using  &lt;code&gt;model_graded_qa&lt;/code&gt;. The generated list of scores in the range [0.0, 1.0] is supplied to our &lt;code&gt;custom_reducer&lt;/code&gt; which calculates their arithmetic mean. From there, it applies quadratic score curving (

&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord text"&gt;&lt;span class="mord"&gt;mean&lt;/span&gt;&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
) to ensure that the further from correct the mean of the supplied answers are, the lower the score is pulled. This allows the most correct answers to stand out immediately. In the &lt;strong&gt;Scoring Tab&lt;/strong&gt;, this downward curve maps a raw 5/6 fact score (
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;≈&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;0.8333&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
) down to a normalized 0.65 sample score. For complete formulas and math derivations, see the README section on &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/agents/inspect-agent-skills-eval/README.md#3-decoupled-grader--multidimensional-rubrics" rel="noopener noreferrer"&gt;Atomic Fact Verification &amp;amp; Quadratic Curving&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Visual milestone: The transition to cohort analytics
&lt;/h2&gt;

&lt;p&gt;While &lt;code&gt;inspect view&lt;/code&gt; provides outstanding deep-dive diagnostics for individual sample traces, evaluating dozens of models across multiple skill domains gets confusing fast (as you may have seen above). Weighing the correlations between skill inclusion vs exclusion and model changes against each other can be confounding and more art than science if we don’t find better ways to reason over them; it requires a structured matrix overview.&lt;/p&gt;

&lt;p&gt;Remember that mess of sectors of a 3d space I showed you earlier representing all the configs? Well, unless we find numeric ways to collapse that or quantify deeper comparison, you won’t get the granular information you need to choose between two similarly capable alternatives. Worse yet, you won’t be able to communicate this to the people with their hands at the purse strings (unless of course, that's you too).&lt;/p&gt;

&lt;p&gt;To make this easier, next time we’ll start down the road of understanding and displaying these things visually: As visualized in the cohort matrix linked above, understanding agent capability factor-by-factor requires cohort slicing across multi-dimensional metrics.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 2&lt;/strong&gt;, we scale our analysis from single log UI inspection to aggregate scoreboards with &lt;code&gt;inspect viz&lt;/code&gt;!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>Codegolf: Build a container in Cloud Build</title>
      <dc:creator>Katie McLaughlin</dc:creator>
      <pubDate>Wed, 27 Mar 2024 01:19:28 +0000</pubDate>
      <link>https://dev.to/googlecloud/codegolf-build-a-container-in-cloud-build-4okn</link>
      <guid>https://dev.to/googlecloud/codegolf-build-a-container-in-cloud-build-4okn</guid>
      <description>&lt;p&gt;&lt;small&gt;&lt;em&gt;Banner Photo by &lt;a href="https://unsplash.com/@vki?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Viktor Kiryanov&lt;/a&gt; on Unsplash&lt;/em&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;I read Adam Ross's &lt;a href="https://dev.to/googlecloud/modernizing-cloudbuildyaml-for-container-builds-1je0"&gt;Modernizing cloudbuild.yaml for Container Builds&lt;/a&gt; and saw all his mentions of "character counts" and it got me thinking: &lt;/p&gt;

&lt;p&gt;How codegolf can we get with this? &lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;Context: like regular golf, codegolf asks you to implement something in the least amount of character possible. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Starting with Adam's finishing iteration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Image'&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest'&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build . --tag "${_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_IMAGE}:latest"&lt;/span&gt;

&lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_IMAGE}:latest"&lt;/span&gt;

&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;automapSubstitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;substitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;_IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service&lt;/span&gt;
  &lt;span class="na"&gt;_LOCATION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us&lt;/span&gt;
  &lt;span class="na"&gt;_REPO&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;container&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Initial character count: 358&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It appears his character count doesn't inline new lines, which is effectively the same as the &lt;code&gt;wc -c&lt;/code&gt; minus the &lt;code&gt;wc -l&lt;/code&gt;. I'll be using the following for my character counts: &lt;br&gt;
&lt;code&gt;expr $(wc -c cloudbuild.yaml | cut -d' ' -f1) - $(wc -l cloudbuild.yaml | cut -d ' ' -f1)&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He notes that the introduction of &lt;a href="https://cloud.google.com/artifact-registry/docs/" rel="noopener noreferrer"&gt;Artifact Registry&lt;/a&gt; make this a bit longer than it would be in Container Registry, but we'll sit with that since it's the supported registry. &lt;/p&gt;

&lt;p&gt;We'll also keep with this last iteration's requirements: a project might have more than one registry, so we'll keep some parameterization. &lt;/p&gt;

&lt;p&gt;Each iteration I'll make sure is a valid build. For testing, I'll ensure &lt;code&gt;gcloud builds submit&lt;/code&gt; runs clean in a folder containing just this &lt;code&gt;cloudbuild.yaml&lt;/code&gt; file and my go-to &lt;a href="https://github.com/GoogleCloudPlatform/devrel-demos/blob/main/app-dev/python-frameworks-cloudrun/easteregg-shell/Dockerfile" rel="noopener noreferrer"&gt;codegolf Dockerfile&lt;/a&gt; (that's also a valid Cloud Run service, just for fun).&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in an ID?
&lt;/h2&gt;

&lt;p&gt;To start, Cloud Build steps don't require an ID, so we can remove that line entirely. Without an ID, the Cloud Build build details in the Google Cloud console will use the image name as the step details, so there is a slight usability disadvantage to this change, but nothing that affects functionality.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-- id: 'Build Container Image'
-  name: 'gcr.io/cloud-builders/docker:latest'
&lt;/span&gt;&lt;span class="gi"&gt;+- name: 'gcr.io/cloud-builders/docker:latest'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Character count: 329.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Deduplicate with dynamics
&lt;/h2&gt;

&lt;p&gt;We've also got the same image ID used multiple times. We want to keep the dynamic nature of this, but we also want to use substitutions in the value itself. We can use &lt;code&gt;dynamicSubstitutions&lt;/code&gt; to allow use of substitutions within other substitutions, but adding this adds ~27 characters to our file (and we're removing 45x2 characters so this is a win!)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest'&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build . --tag "$_IMAGE_URI"&lt;/span&gt;

&lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$_IMAGE_URI"&lt;/span&gt;

&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;automapSubstitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;dynamicSubstitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;substitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;_IMAGE_URI&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${_LOCATION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_IMAGE}:latest&lt;/span&gt;
  &lt;span class="na"&gt;_IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service&lt;/span&gt;
  &lt;span class="na"&gt;_LOCATION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us&lt;/span&gt;
  &lt;span class="na"&gt;_REPO&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;container&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Character count: 326.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At this point we can also remove the superfluous quotation marks, saving &lt;strong&gt;6&lt;/strong&gt; characters, bringing us down to &lt;strong&gt;320.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Shortest values
&lt;/h2&gt;

&lt;p&gt;Now we get to the part where we work out: What's the minimum value for the variables we're using? &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;$PROJECT_ID&lt;/code&gt; is a &lt;a href="https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values" rel="noopener noreferrer"&gt;default substitution&lt;/a&gt;, so we should keep that as is, and it will be dynamic for our project. &lt;/p&gt;

&lt;p&gt;We're already using the shortest Artifact Registry region, the &lt;a href="https://cloud.google.com/artifact-registry/docs/repositories/repo-locations#location-mr" rel="noopener noreferrer"&gt;US multi-region&lt;/a&gt; (the longest Google Cloud region could be up to 24 characters, so 2 is great!)&lt;/p&gt;

&lt;p&gt;But we can possibly do better with the Artifact Registry name and the Container Name. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;d&lt;/code&gt; (for "Docker") is a valid Artifact Registry repository name, and &lt;code&gt;i&lt;/code&gt; (for "image") are valid, so we can save ~15 characters there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;...
&lt;span class="gd"&gt;-  _IMAGE: service
&lt;/span&gt;&lt;span class="gi"&gt;+  _IMAGE: i
&lt;/span&gt;   _LOCATION: us
&lt;span class="gd"&gt;-  _REPO: container
&lt;/span&gt;&lt;span class="gi"&gt;+  _REPO: c
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Character count: 306.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal variables
&lt;/h2&gt;

&lt;p&gt;Any of our underscore substitutions we can reduce down to two letters, the underscore and some useful character. (We're replacing "Image &lt;strong&gt;U&lt;/strong&gt;RL", "&lt;strong&gt;I&lt;/strong&gt;mage", "&lt;strong&gt;R&lt;/strong&gt;epo", and "&lt;strong&gt;L&lt;/strong&gt;ocation", respectively.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcr.io/cloud-builders/docker:latest&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build . --tag $_U&lt;/span&gt;

&lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$_U&lt;/span&gt;

&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;automapSubstitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;dynamicSubstitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;substitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;_U&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${_L}-docker.pkg.dev/${PROJECT_ID}/${_R}/${_I}:latest&lt;/span&gt;
  &lt;span class="na"&gt;_I&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;i&lt;/span&gt;
  &lt;span class="na"&gt;_L&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us&lt;/span&gt;
  &lt;span class="na"&gt;_R&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Character count: 254.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;(We're now down below what Adam was before migrating to Artifact Registry! 😅)&lt;/p&gt;




&lt;h2&gt;
  
  
  Remove tags
&lt;/h2&gt;

&lt;p&gt;Another reduction we can make is removing the &lt;code&gt;:latest&lt;/code&gt; tags. "Latest" is used in Docker where there is &lt;a href="https://cloud.ibm.com/docs/Registry?topic=Registry-troubleshoot-docker-latest" rel="noopener noreferrer"&gt;a lack of tag&lt;/a&gt;, so this is implicit in our call (and it just so happens that the Cloud Builder we are referencing has a "latest" tag. (You can check this by going to &lt;a href="//gcr.io/cloud-builders/docker"&gt;gcr.io/cloud-builders/docker&lt;/a&gt; in the browser, which will show you information about this image in its registry. This image in particular will direct you to its Artifact Registry information, because &lt;a href="https://github.com/GoogleCloudPlatform/cloud-builders?tab=readme-ov-file#container-registry-deprecation" rel="noopener noreferrer"&gt;this registry has been automatically redirected&lt;/a&gt;.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcr.io/cloud-builders/docker&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build . --tag $_U&lt;/span&gt;

&lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$_U&lt;/span&gt;

&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;automapSubstitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;dynamicSubstitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;substitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;_U&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${_L}-docker.pkg.dev/${PROJECT_ID}/${_R}/${_I}&lt;/span&gt;
  &lt;span class="na"&gt;_I&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;i&lt;/span&gt;
  &lt;span class="na"&gt;_L&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us&lt;/span&gt;
  &lt;span class="na"&gt;_R&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Character count: 240.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the args back
&lt;/h2&gt;

&lt;p&gt;The last step we can do to save space is to actually re-introduce the &lt;code&gt;args&lt;/code&gt; parameter, rather than using &lt;code&gt;script&lt;/code&gt;. In this case, we will be adding some characters for the separation of the arguments, but the biggest win is being able to remove the ~27 characters that the &lt;code&gt;automapSubstitutions&lt;/code&gt; config setting adds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcr.io/cloud-builders/docker&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;build'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--tag'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$_U'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$_U&lt;/span&gt;

&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dynamicSubstitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;substitutions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;_U&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${_L}-docker.pkg.dev/${PROJECT_ID}/${_R}/${_I}&lt;/span&gt;
  &lt;span class="na"&gt;_I&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;i&lt;/span&gt;
  &lt;span class="na"&gt;_L&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us&lt;/span&gt;
  &lt;span class="na"&gt;_R&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Character count: 213.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tip: Make sure you check what your default entrypoint is for images! In this case, I have also removed &lt;code&gt;docker&lt;/code&gt;, as my args are appended to the default entrypoint. The script needs the full command (I may have spent too long debugging this issue&lt;/em&gt; 😅&lt;em&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Twist
&lt;/h2&gt;

&lt;p&gt;Actually you only need &lt;strong&gt;22 characters&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gcloud run deploy a
[ENTER]
[ENTER]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First time you run this you'll need 1 extra character (ENTER) to enable APIs, and also 1 extra character (ENTER) to create the Cloud Run Source Deploy image registry. (You can also use this method to automatically create the Artifact Registry for you, read more about this on my blog post: "&lt;a href="https://glasnt.com/blog/auto-provisioning-artifact-registry/" rel="noopener noreferrer"&gt;Auto-provisioning Artifact Registry though Cloud Run Source Deploys (glasnt.com)&lt;/a&gt;").&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;You can make a very short Cloud Build configuration to build images, but you should consider the readability of your scripts as you go. Addressing any cruft and modernizing your scripts can be an interesting exercise for you and your infrastructure. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;With thanks to Adam Ross for the review!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>codegolf</category>
      <category>cloudbuild</category>
    </item>
    <item>
      <title>Migrating from Secret Manager API to built-in secrets</title>
      <dc:creator>Katie McLaughlin</dc:creator>
      <pubDate>Sun, 19 Jun 2022 23:17:12 +0000</pubDate>
      <link>https://dev.to/googlecloud/migrating-from-secret-manager-api-to-built-in-secrets-4k06</link>
      <guid>https://dev.to/googlecloud/migrating-from-secret-manager-api-to-built-in-secrets-4k06</guid>
      <description>&lt;p&gt;Secret Manager is directly integrated into a number of products, including Cloud Run and Cloud Build. &lt;/p&gt;

&lt;p&gt;Many samples I've authored use the Secret Manager API by calling the API from within the code. This can be &lt;a href="https://cloud.google.com/secret-manager/docs/best-practices#coding_practices" rel="noopener noreferrer"&gt;more secure&lt;/a&gt;, but variable leakage is something that needs to be considered in any deployment. Removing the direct API calls can help with portability, and reduce the amount of dependencies in the deployment, and the complexity of the code itself. &lt;/p&gt;

&lt;p&gt;So let's look at how we can migrate to built-in secrets. &lt;/p&gt;




&lt;h2&gt;
  
  
  Integrating Secret Manager API
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5i6owi4brzc6hfkm1ub8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5i6owi4brzc6hfkm1ub8.png" alt="Secret value: Input your secret value or import it directly from a file." width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Screenshot: "Secret value: Input your secret value or import it directly from a file.", showing a file upload dialog, or a text area input field.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When you create secrets, you can create them from a file or from direct input. When you retrieve these secrets, you may presume they're a single value, or a file of content you have to process. &lt;/p&gt;

&lt;p&gt;Once you create the secret, you can &lt;a href="https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets#secretmanager-access-secret-version-python" rel="noopener noreferrer"&gt;access it with the Secret Manager API&lt;/a&gt; using a client library for Secret Manager available in many different languages. &lt;/p&gt;

&lt;p&gt;This post will use Python examples and packages, but the same patterns can be applied to other languages. &lt;/p&gt;




&lt;p&gt;For example, this code snippet will retrieve the latest version of the secret &lt;code&gt;mySecret&lt;/code&gt; in the current project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;google.auth&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;secretmanager&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;

&lt;span class="c1"&gt;# trick to detect current project
&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SecretManagerServiceClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mySecret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;projects/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/secrets/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/versions/latest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;secret_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;access_secret_version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTF-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;secret_value&lt;/code&gt; can then be used as required. This presumes that &lt;code&gt;mySecret&lt;/code&gt; is a single value, for example an API key.&lt;/p&gt;

&lt;p&gt;One way developers can include multiple secrets in one format is with &lt;code&gt;.env&lt;/code&gt; files. These files contain one or more key/value pairs which mean you can store multiple secrets in one file, then use helper packages to parse these values into your code. This prevents you from having to define multiple separate secret values. &lt;/p&gt;

&lt;p&gt;If &lt;code&gt;mySecret&lt;/code&gt; was a &lt;code&gt;.env&lt;/code&gt; file, I could load it using &lt;code&gt;python-dotenv:&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;## pip install python-dotenv
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret_value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or I could to similar with &lt;code&gt;django-environ&lt;/code&gt;, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;## pip install django_environ
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;environ&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret_value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In both of these examples, these packages presume that the values are in a &lt;code&gt;.env&lt;/code&gt; file within the same directory as the running process. Using &lt;code&gt;StringIO&lt;/code&gt; means that a string variable can be sent to these methods in a file-like object, so the values can be read as though they were a file.&lt;/p&gt;

&lt;p&gt;When using the Secret Manager API, you can run the application in any place where Google Cloud authentication works. That is, any Google Cloud service, or your local machine if you have set up &lt;code&gt;gcloud&lt;/code&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Consuming built-in secrets
&lt;/h2&gt;

&lt;p&gt;If you have your secret configured as an environment variable, then you only have to retrieve it as an environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="n"&gt;secret_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MYSECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;This method prevents KeyErrors if the key is not found.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For products with built-in secrets, they may be made available in additional ways. &lt;/p&gt;

&lt;p&gt;In Cloud Build you can connect secrets &lt;a href="https://cloud.google.com/build/docs/securing-builds/use-secrets" rel="noopener noreferrer"&gt;directly to environment variables&lt;/a&gt;. But Cloud Run additionally allows &lt;a href="https://cloud.google.com/run/docs/configuring/secrets" rel="noopener noreferrer"&gt;mounting the secret as a volume&lt;/a&gt;, which means it can be read as a file. Any time the file is read, if you specify &lt;code&gt;latest&lt;/code&gt;, the current &lt;code&gt;latest&lt;/code&gt; will be received! However, you must mount the file in a new volume so you can't mount it directly in the default &lt;code&gt;.env&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;If you want to adapt your code to handle a local &lt;code&gt;.env&lt;/code&gt; file, or a separately mounted file, or an environment variable, you will have to ensure all methods are possible in your code. &lt;/p&gt;

&lt;p&gt;You also need to consider which configuration takes priority, as by default both &lt;code&gt;python-dotenv&lt;/code&gt; and &lt;code&gt;django-environ&lt;/code&gt; accept the first declared value as the value they use. You can override this by using the &lt;code&gt;&lt;a href="https://github.com/theskumar/python-dotenv#variable-expansion" rel="noopener noreferrer"&gt;--override&lt;/a&gt;&lt;/code&gt; or &lt;code&gt;&lt;a href="https://github.com/theskumar/python-dotenv#variable-expansion" rel="noopener noreferrer"&gt;--overwrite&lt;/a&gt;&lt;/code&gt; respectively.&lt;/p&gt;

&lt;p&gt;When developing applications, I might choose to say my local &lt;code&gt;.env&lt;/code&gt; file takes priority, then any mounted secrets, and then any declared variables. &lt;/p&gt;

&lt;p&gt;Another point to mention is that using &lt;code&gt;python-dotenv&lt;/code&gt;, if a file is not found or a value is empty, it silently continues. This means you can include the method calls without having to explicitly handle errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/secrets/.env&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MYSECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same code works for &lt;code&gt;django-environ&lt;/code&gt;, where you can just import in the order of priority without having to worry about missing files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;environ&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_env&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/secrets/.env&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MYSECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that in these examples, I'm choosing &lt;code&gt;/secrets/&lt;/code&gt;as my volume, and keeping the path the same name as the original file. You can choose any volume and path, as long as the volume is not already used by the application (for example, if you choose &lt;code&gt;/app/&lt;/code&gt; as your working directory, you cannot mount secrets there.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying built-in secrets
&lt;/h2&gt;

&lt;p&gt;To run this code locally, you'd create a &lt;code&gt;.env&lt;/code&gt; file with the contents &lt;code&gt;MYSECRET=serkitValue&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;If you're committing this code to git, ensure you're not committing the secret file! Make sure you add &lt;code&gt;.env&lt;/code&gt; to your &lt;code&gt;.gitignore&lt;/code&gt; file!&lt;/p&gt;

&lt;p&gt;You can also choose to ignore any contents of your &lt;code&gt;.gitignore&lt;/code&gt; file in your Google Cloud commands by adding &lt;code&gt;.gitignore&lt;/code&gt;'s contents to &lt;code&gt;&lt;a href="https://cloud.google.com/sdk/gcloud/reference/topic/gcloudignore" rel="noopener noreferrer"&gt;.gcloudignore&lt;/a&gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.git
#!include:.gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then create the secret from this file with &lt;code&gt;gcloud&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud secrets create mySecret &lt;span class="nt"&gt;--data-file&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Cloud Build, you will need to ensure the secret is available in the environment (&lt;a href="https://cloud.google.com/build/docs/securing-builds/use-secrets#example_accessing_secrets_from_scripts_and_processes" rel="noopener noreferrer"&gt;which your script can then use&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
 &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:slim&lt;/span&gt;
   &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip&lt;/span&gt;
   &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;install'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-r'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;requirements.txt'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--user'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

 &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:slim&lt;/span&gt;
   &lt;span class="na"&gt;secretEnv&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MYSECRET'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
   &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python&lt;/span&gt;
   &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;main.py'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;availableSecrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;secretManager&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;versionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;projects/$PROJECT_ID/secrets/mySecret/versions/latest&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MYSECRET'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also reference secrets in the args call itself, &lt;a href="https://cloud.google.com/build/docs/securing-builds/use-secrets#access-utf8-secrets" rel="noopener noreferrer"&gt;using bash variables&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For Cloud Run, you'll have to deploy the service specifying either a mount or an environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# for mounted volume&lt;/span&gt;
gcloud run deploy myservice &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;  &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--update-secrets&lt;/span&gt; /secrets/.env&lt;span class="o"&gt;=&lt;/span&gt;mySecret:latest

&lt;span class="c"&gt;# for environment variable&lt;/span&gt;
gcloud run deploy myservice &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;  &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--update-secrets&lt;/span&gt; &lt;span class="nv"&gt;MYSECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mySecret:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  And never forget IAM!
&lt;/h2&gt;

&lt;p&gt;Don't forget: you'll also need to make sure the service account you're using has &lt;a href="https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets#access" rel="noopener noreferrer"&gt;permissions&lt;/a&gt; to access your secret!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Katie is a Developer Advocate for Google Cloud, focusing on Python and Serverless. She tweets &lt;a href="https://twitter.com/glasnt" rel="noopener noreferrer"&gt;@glasnt&lt;/a&gt; about clouds, crafts, and cats.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>secretmanager</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Using Workflows to dynamically update a Datasette website</title>
      <dc:creator>Katie McLaughlin</dc:creator>
      <pubDate>Sun, 20 Feb 2022 22:04:32 +0000</pubDate>
      <link>https://dev.to/googlecloud/using-workflows-to-dynamically-update-a-datasette-website-1bjk</link>
      <guid>https://dev.to/googlecloud/using-workflows-to-dynamically-update-a-datasette-website-1bjk</guid>
      <description>&lt;p&gt;A while ago I spoke with &lt;a href="https://twitter.com/simonw" rel="noopener noreferrer"&gt;Simon Willison&lt;/a&gt; about the sorts of things he'd like to have in a cloud provider in order to deploy &lt;a href="https://datasette.io/" rel="noopener noreferrer"&gt;Datasette&lt;/a&gt; nicely and repeatedly. A few of the things mentioned were being able to attach to object storage, especially when databases were particularly large and it didn't make sense to mount them into the container image itself. &lt;/p&gt;

&lt;p&gt;With the preview of the &lt;a href="https://cloud.google.com/run/docs/about-execution-environments" rel="noopener noreferrer"&gt;second generation execution environment&lt;/a&gt; with Cloud Run, one of the many new features is the ability to &lt;a href="https://cloud.google.com/run/docs/tutorials/network-filesystems-fuse" rel="noopener noreferrer"&gt;mount Cloud Storage as a network filesystem&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With this, you can attach any Cloud Storage bucket and have a Datasette web service serve data from that source. &lt;/p&gt;

&lt;p&gt;This works very well when you have one database that you want to serve. However, one of the &lt;a href="https://github.com/simonw/datasette/issues/43#issuecomment-344180866" rel="noopener noreferrer"&gt;known limitations of Datasette&lt;/a&gt;, where if you serve a folder of content, the glob of files in that folder is loaded at server start time, and not refreshed unless the server is restarted.&lt;/p&gt;

&lt;p&gt;There are few ways we can work around this issue. You could have the process within the container restart when the mounted filesystem changes, using something like &lt;code&gt;inotify&lt;/code&gt; (&lt;code&gt;fsevent&lt;/code&gt; on macOS). You could also consider periodically starting, but that may mean restarting when not required, or not often enough. For a sufficiently low traffic system, you'd end up automatically booting from cold after a time of inactivity, anyway. &lt;/p&gt;

&lt;p&gt;The way I chose to solve this was to use the event of an object being uploaded to the bucket to trigger an update of the service, thus restarting Datasette (albeit dramatically). &lt;/p&gt;

&lt;p&gt;And since I was now listening to the upload event, I thought, why not do more processing while I'm here?&lt;/p&gt;

&lt;p&gt;I'd recently learnt from &lt;a href="https://codelabs.developers.google.com/codelabs/cloud-picadaily-lab6#0" rel="noopener noreferrer"&gt;Lab 6&lt;/a&gt; of the &lt;a href="https://g.co/codelabs/serverless-workshop" rel="noopener noreferrer"&gt;Pic-A-Daily Workshop&lt;/a&gt; how to use &lt;a href="https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop/tree/master/workflows" rel="noopener noreferrer"&gt;Workflows to handle events&lt;/a&gt;, so I thought I'd try developing one of my own. &lt;/p&gt;

&lt;p&gt;The result is &lt;a href="https://github.com/glasnt/dynamic-datasette" rel="noopener noreferrer"&gt;github.com/glasnt/dynamic-datasette&lt;/a&gt;, and once deployed it works like this: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you upload an new file to the upload bucket&lt;/li&gt;
&lt;li&gt;the event of that file being uploaded triggers a function, that starts a Workflow&lt;/li&gt;
&lt;li&gt;the workflow then follows through a number of steps:

&lt;ul&gt;
&lt;li&gt;sends the file for processing:

&lt;ul&gt;
&lt;li&gt;if the file is already an SQLite database, then it sends it off to the serving bucket&lt;/li&gt;
&lt;li&gt;if it's one of the types of files it knows about, it uses one of the many &lt;code&gt;*-to-sqlite&lt;/code&gt; tools Simon has made. For example, &lt;code&gt;csvs-to-sqlite&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;Once converted, the resulting SQLite database file is uploaded to the serving bucket&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;updates &lt;a href="https://docs.datasette.io/en/stable/metadata.html" rel="noopener noreferrer"&gt;metadata&lt;/a&gt; for Datasette:

&lt;ul&gt;
&lt;li&gt;this step mostly just ensures that the Datasette service displays the datetime when this workflow was run, so users can know how new their data is that's being served&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;restarts the web service

&lt;ul&gt;
&lt;li&gt;This step uses the &lt;code&gt;replaceService&lt;/code&gt; API to force a 'restart', deploying a new revision of the Datasette service, ensuring that the latest information in the serving bucket is being relayed to users. &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The Datasette container mounts the serving bucket, and metadata.json in that same bucket, then just calls "&lt;code&gt;datasette serve&lt;/code&gt;" on the mounted folder.&lt;/p&gt;

&lt;p&gt;You can try this out on your own project by deploying the code with Terraform, following the instructions in the README.md. &lt;/p&gt;

&lt;p&gt;Try extending the functionality of the &lt;code&gt;process-upload&lt;/code&gt; function to do more steps, or adding more metadata to the hosted service.&lt;/p&gt;




&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@victor_g?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Victor&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/pipes?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Open Source Credit Survey</title>
      <dc:creator>Katie McLaughlin</dc:creator>
      <pubDate>Tue, 01 Feb 2022 21:31:08 +0000</pubDate>
      <link>https://dev.to/glasnt/open-source-credit-survey-3jm0</link>
      <guid>https://dev.to/glasnt/open-source-credit-survey-3jm0</guid>
      <description>&lt;p&gt;&lt;em&gt;The University of Vermont is conducting a survey to help understand how people receive credit for tasks in open source, and are seeking your participation!&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Survey: &lt;a href="https://qualtrics.uvm.edu/jfe/form/SV_1zUs19oVcZJ0SPA" rel="noopener noreferrer"&gt;https://qualtrics.uvm.edu/jfe/form/SV_1zUs19oVcZJ0SPA&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Today's open source world has similarities to the world of open source when I &lt;a href="https://opensource.com/life/15/10/octohat-github-non-code-contribution-tracker" rel="noopener noreferrer"&gt;first wrote about this problem in 2015&lt;/a&gt;. I wrote a tool that helps discover who is contributing to GitHub projects based on the contributions that aren't code. In 2022, we are still contributing countless hours of time towards communities to drive and improve open source across many industries, but we don't know how much of that work is credited beyond automated tools like this. &lt;/p&gt;

&lt;p&gt;I'm a member of the &lt;a href="https://vermontcomplexsystems.org/partner/OCEAN/" rel="noopener noreferrer"&gt;OCEAN&lt;/a&gt;, a research team from the University of Vermont Complex Systems Center, and we're collaborating with industry experts from Google, OSI, IEEE, PSF, and OSSF, among others. You may have recently heard about our project on &lt;a href="https://podcast.chaoss.community/50" rel="noopener noreferrer"&gt;CHAOSSCast&lt;/a&gt;, or being shout-out on &lt;a href="https://podcast.sustainoss.org/98" rel="noopener noreferrer"&gt;SustainOSS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As we've written in our correspondence in &lt;a href="https://www.nature.com/articles/s43588-020-00011-w" rel="noopener noreferrer"&gt;Nature Computational Science&lt;/a&gt;, we believe that having a recognition model for the work done in any open source community can help more in just gaining credit for code, but providing clear signaling of the work done for the community. &lt;/p&gt;

&lt;p&gt;As part of this work, the University of Vermont team is conducting a survey about how people receive credit for the tasks they do as part of open source projects.&lt;/p&gt;

&lt;p&gt;The survey asks about your experiences with receiving credit on open source projects. The survey asks multiple choice questions about how often you received credit for the tasks you did, and written response questions about what did and did not go well on open source projects.&lt;/p&gt;

&lt;p&gt;We hope that with this information, we can improve the  equity of contributions across all of open source.&lt;/p&gt;




&lt;p&gt;Click on this link to participate in the survey: &lt;a href="https://qualtrics.uvm.edu/jfe/form/SV_1zUs19oVcZJ0SPA" rel="noopener noreferrer"&gt;https://qualtrics.uvm.edu/jfe/form/SV_1zUs19oVcZJ0SPA&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://vermontcomplexsystems.org/partner/OCEAN/" rel="noopener noreferrer"&gt;OCEAN&lt;/a&gt; is a partnership between the Google Open Source Program Office and the Vermont Complex Systems Center. Google does not have any influence on the survey, and will be unable to see any individual responses.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>When you're not around: trigger Cloud Run on a schedule</title>
      <dc:creator>Katie McLaughlin</dc:creator>
      <pubDate>Fri, 22 Jan 2021 00:00:31 +0000</pubDate>
      <link>https://dev.to/googlecloud/when-you-re-not-around-trigger-cloud-run-on-a-schedule-53p4</link>
      <guid>https://dev.to/googlecloud/when-you-re-not-around-trigger-cloud-run-on-a-schedule-53p4</guid>
      <description>&lt;p&gt;On this edition of Serverless Expeditions, we take a look at scheduling automated Cloud Run jobs with Cloud Scheduler.&lt;/p&gt;

&lt;p&gt;Check out the video version of this blog post.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/XIwbIimM49Y"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Source code for this blog post is available on GitHub under the "scheduled-cloud-run" folder.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/GoogleCloudPlatform" rel="noopener noreferrer"&gt;
        GoogleCloudPlatform
      &lt;/a&gt; / &lt;a href="https://github.com/GoogleCloudPlatform/serverless-expeditions" rel="noopener noreferrer"&gt;
        serverless-expeditions
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;&lt;a href="http://cloud.run/" rel="noopener noreferrer"&gt;Cloud Run&lt;/a&gt; is designed to host a containerised web service -- any language you like, as long as the service &lt;a href="https://cloud.google.com/run/docs/reference/container-contract#port" rel="noopener noreferrer"&gt;listens on port 8080&lt;/a&gt; -- but your code can run on events other than a user visiting your site. &lt;/p&gt;

&lt;p&gt;An example of this design pattern would be a nightly billing job: you want to process billing data nightly at 1am, but you don't want to be the person to click a button in the middle of the night. &lt;/p&gt;

&lt;p&gt;If you wanted to implement such a receiving service in NodeJS, you could implement it like in our example code: at the top of the code, create and initialize an &lt;a href="https://expressjs.com/" rel="noopener noreferrer"&gt;Express app&lt;/a&gt; that listens for incoming HTTP calls. Note that Cloud Scheduler uses the “text/plain” content type, so you'll have to tell body-parser to parse more than the default “application/json” type. The POST handler parses the minimum balance that was sent to it from Cloud Scheduler, and then sends that off to the billing method later in the code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloud.google.com/scheduler" rel="noopener noreferrer"&gt;Cloud Scheduler&lt;/a&gt; is a managed service offered on Google Cloud, where you can create jobs on a cron schedule that either makes a HTTPS call, or publishes a Pub/Sub message.  In this example, you can setup a scheduled job that runs at 1am every night by entering the &lt;a href="https://crontab.guru/#0_1_*_*_*" rel="noopener noreferrer"&gt;cron value&lt;/a&gt; &lt;code&gt;0 1 * * *&lt;/code&gt; in the Frequency field, and setting the HTTP Target to be the URL of your Cloud Run service you created earlier.&lt;/p&gt;

&lt;p&gt;However, you need to make sure that this billing service is private, and not accessible by anyone on the internet. To convert a &lt;a href="https://cloud.google.com/run/docs/authenticating/public" rel="noopener noreferrer"&gt;public service&lt;/a&gt; to private, you need to remove the &lt;code&gt;allUsers&lt;/code&gt; member from the service -- that is, deny anyone from accessing it. To then allow Cloud Scheduler to access the service, create a new service account, assigning it the Cloud Run Invoker role. Then set that service account's email identifier in the &lt;a href="https://cloud.google.com/scheduler/docs/http-target-auth" rel="noopener noreferrer"&gt;Auth Header setting&lt;/a&gt; in Cloud Scheduler, under "Add OIDC token". OIDC, or &lt;a href="https://openid.net/connect/" rel="noopener noreferrer"&gt;OpenID Connect&lt;/a&gt;, is a small layer on top of OAuth that handles identity. This field tells Cloud Scheduler to use the new service account's identity for running the job. &lt;/p&gt;

&lt;p&gt;The free tier of Cloud Scheduler allows you &lt;a href="https://cloud.google.com/scheduler/pricing" rel="noopener noreferrer"&gt;3 jobs for free&lt;/a&gt;, no matter how many times the job is run. Each job after that is USD$0.10 each. You may also incur costs for excess &lt;a href="https://cloud.google.com/run/pricing" rel="noopener noreferrer"&gt;Cloud Run&lt;/a&gt; execution time, database costs, etc.&lt;/p&gt;

&lt;p&gt;By using Google Cloud managed services that connect to Cloud Run, you can build more complex architectures, and automate manual processes to make your developer live easier. &lt;/p&gt;




&lt;h2&gt;
  
  
  About Serverless Expeditions
&lt;/h2&gt;

&lt;p&gt;Serverless Expeditions is a fun and cheeky video series that looks at what serverless means and how to build serverless apps with Google Cloud. &lt;/p&gt;

&lt;p&gt;Follow these hosts on Twitter at &lt;a href="https://twitter.com/glasnt" rel="noopener noreferrer"&gt;@glasnt&lt;/a&gt; and &lt;a href="https://twitter.com/martinomander" rel="noopener noreferrer"&gt;@martinomander&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>serverlessexpeditions</category>
      <category>googlecloudrun</category>
      <category>cloudrun</category>
    </item>
    <item>
      <title>Running multiple versions of Python in Cloud Run</title>
      <dc:creator>Katie McLaughlin</dc:creator>
      <pubDate>Fri, 11 Dec 2020 01:05:06 +0000</pubDate>
      <link>https://dev.to/googlecloud/running-multiple-versions-of-python-in-cloud-run-29me</link>
      <guid>https://dev.to/googlecloud/running-multiple-versions-of-python-in-cloud-run-29me</guid>
      <description>&lt;p&gt;On this episode of Serverless Expeditions, we take a look at how to deploy multiple versions of Python in Cloud Run.&lt;/p&gt;

&lt;p&gt;Check out the video version of this blog post.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/-fbcOnrN-Eo"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;Running multiple versions of Python in Cloud Run - Serverless Expeditions (YouTube.com)&lt;p&gt;&lt;/p&gt;




&lt;p&gt;As new versions of Python are released, trying to choose between the new and shiny or the old and stable can be an issue. But if you are deploying on &lt;a href="http://cloud.run/" rel="noopener noreferrer"&gt;Cloud Run&lt;/a&gt;, you don't have to choose between the two, you can run both. &lt;/p&gt;

&lt;p&gt;Imagine this hypothetical scenario: you are an SRE for a company with a custom storefront, maintaining a fleet of virtual machines running various components, all with their own versions of Python. To improve reliability, you can upgrade all the components to use one version of Python, and move away from virtual machines, and into something more scalable, like serverless. &lt;/p&gt;

&lt;p&gt;This upgrade won't happen overnight, and the storefront must keep running in the meantime. So your plan is multi-step: migrate from virtual machines to serverless first, then make the language upgrades. But before migrating the existing systems, a new service coming online could be deployed serverless first, skipping the virtual machine all together. &lt;/p&gt;

&lt;p&gt;When choosing which serverless platform to upgrade to, limitations may exist on the languages available. For example, Cloud Functions only supports &lt;a href="https://cloud.google.com/functions/docs/concepts/python-runtime" rel="noopener noreferrer"&gt;specific versions of Python&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;However, the newest serverless offering from Google Cloud -- Cloud Run -- doesn't have this limitation. Cloud Run defines services as container images, which are defined by Dockerfiles, and can specify any base image. There is an entire repository of &lt;a href="https://hub.docker.com/_/python" rel="noopener noreferrer"&gt;official Python images on Docker Hub&lt;/a&gt; which you can source from, by specifying the base image in the FROM line of your Dockerfile.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM python:2.7
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM python:3.9
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you migrate each service from its original virtual machine, you can set the exact version of Python it was originally running. You can also upgrade each service in isolation, as your team has time and resources. &lt;/p&gt;

&lt;p&gt;Once your dependencies have been upgraded and you want to test your service, you can deploy your new revision to only a limited number of users, using the &lt;a href="https://cloud.google.com/run/docs/rollouts-rollbacks-traffic-migration#gradual" rel="noopener noreferrer"&gt;Traffic Splitting feature of Cloud Run.&lt;/a&gt; You can set your new revision to serve, say, only 5% of requests, so if there is something wrong, only some of your users are affected, and you can quickly rollback if there are any issues. &lt;/p&gt;

&lt;p&gt;By migrating each service in turn, you are able to control each service in isolation, allowing for a more incremental upgrade process, and a more reliable environment. &lt;/p&gt;

&lt;p&gt;By being able to specify the exact version of Python being used in your project, you can run multiple services each with their own isolated Python environment, which allows you more time to upgrade each service to supported versions of Python. &lt;/p&gt;

&lt;p&gt;It also allows you to try the newest versions of Python -- &lt;a href="https://hub.docker.com/_/python?tab=tags&amp;amp;page=1&amp;amp;name=3.10&amp;amp;ordering=last_updated" rel="noopener noreferrer"&gt;such as prereleases of Python 3.10&lt;/a&gt; -- as soon as their images are available on Docker Hub.&lt;/p&gt;

&lt;h2&gt;
  
  
  About Serverless Expeditions
&lt;/h2&gt;

&lt;p&gt;Serverless Expeditions is a fun and cheeky video series that looks at what serverless means and how to build serverless apps with Google Cloud. &lt;/p&gt;

&lt;p&gt;Follow these hosts on Twitter at &lt;a href="https://twitter.com/glasnt" rel="noopener noreferrer"&gt;@glasnt&lt;/a&gt; and &lt;a href="https://twitter.com/martinomander" rel="noopener noreferrer"&gt;@martinomander&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>serverlessexpeditions</category>
      <category>googlecloudrun</category>
      <category>python</category>
    </item>
    <item>
      <title>"Point-and-click" continuous deployment with Cloud Run</title>
      <dc:creator>Katie McLaughlin</dc:creator>
      <pubDate>Thu, 10 Dec 2020 20:36:47 +0000</pubDate>
      <link>https://dev.to/googlecloud/point-and-click-continuous-deployment-with-cloud-run-4e4</link>
      <guid>https://dev.to/googlecloud/point-and-click-continuous-deployment-with-cloud-run-4e4</guid>
      <description>&lt;p&gt;On this edition of Serverless Expeditions, we take a look at how to deploy a GitHub repo on GitHub, though Cloud Run's new "point-and-click" integration, no Dockerfile required. &lt;/p&gt;

&lt;p&gt;Check out the video version of this blog post.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/Nf3KAY-i6zw"&gt;
&lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;Say you've created a website for a travel blog, and you want to host it online. You've got your code on GitHub, and you have a README of instructions of how a friend could deploy a copy of your website: they have to have your programming language installed on their machine, install some package dependencies, then run a command to start a web server. &lt;/p&gt;

&lt;p&gt;If you want to host your website, you need a way that you can run these steps any time you want to update your code. This process is known as Continuous Deployment: when you commit a change to code a process is started that takes the new version of your code, performs the prerequisite tasks, and then starts a web server of your new code. &lt;/p&gt;

&lt;p&gt;Cloud Run, a serverless platform from Google Cloud, allows you to to bring any container to serve your website, no matter the programming language used inside, as long as it follows the &lt;a href="https://cloud.google.com/run/docs/reference/container-contract" rel="noopener noreferrer"&gt;Runtime Container contract&lt;/a&gt;: a web server must be listening for requests on &lt;code&gt;0.0.0.0&lt;/code&gt;, on port 8080 (or the configured &lt;code&gt;PORT&lt;/code&gt; environment variable). &lt;/p&gt;

&lt;p&gt;So, if you have a website, how do you turn it into a container? You could write a &lt;a href="https://docs.docker.com/engine/reference/builder/" rel="noopener noreferrer"&gt;Dockerfile&lt;/a&gt;, a set of instructions to build a container image based on your setup. But if you're using a common language like Python, Go, or Ruby, the instructions are going to be similar for many websites: copy your code into the container, install your dependencies, and start the web server. &lt;/p&gt;

&lt;p&gt;You can setup your Cloud Run service through the Google Cloud Console using the new &lt;a href="https://cloud.google.com/run/docs/continuous-deployment-with-cloud-build" rel="noopener noreferrer"&gt;walkthrough integration with Cloud Build&lt;/a&gt;: you connect your GitHub repo with Cloud Run, and set up your website to be built any time your code updates. &lt;/p&gt;

&lt;p&gt;However, instead of having to write a Dockerfile, you can now use &lt;a href="https://github.com/GoogleCloudPlatform/buildpacks" rel="noopener noreferrer"&gt;Buildpacks&lt;/a&gt;, a set of common instructions for popular programming languages that help you build your code into a container image that Cloud Run can deploy. &lt;/p&gt;

&lt;p&gt;If you're using a language like Go, Node.js, Python, Java or .NET Core, this will automatically be detected when building a container using Buildpacks. These common languages and their respective package installation processes are handled for you; all you need to do is provide the command to start your web server in a special file called Procfile. &lt;/p&gt;

&lt;p&gt;In the example in the video, Martin is using Flask, a Python-based web framework. To start his application, he runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 -m flask run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So his Procfile will be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;web: python3 -m flask run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "web" in this case means "web server", and is the special term Buildpack looks for in a Procfile to start the web server. &lt;/p&gt;

&lt;p&gt;He should also configure his Flask app to listen on the expected IP and port. He can either add that to his app.run command, or in the Procfile itself as part of starting Flask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;web: python3 -m flask run --host 0.0.0.0 --port $PORT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As long as he adds this file to his codebase, he doesn't need to change anything else about his website to host it on Cloud Run. &lt;/p&gt;

&lt;p&gt;Once configured, any commits made to the GitHub repo -- from the command line, VSCode, GitHub's website, or anywhere else -- are automatically detected and deployed.&lt;/p&gt;




&lt;p&gt;To learn more about the concepts discussed in this video, read more: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://cloud.google.com/run/docs/reference/container-contract" rel="noopener noreferrer"&gt;Container runtime contract | Cloud Run Documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://cloud.google.com/run/docs/continuous-deployment-with-cloud-build" rel="noopener noreferrer"&gt;Continuous deployment from Git using Cloud Build&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://github.com/GoogleCloudPlatform/buildpacks" rel="noopener noreferrer"&gt;GoogleCloudPlatform/buildpacks | GitHub.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can try some of the sample buildpack apps at &lt;a href="https://github.com/GoogleCloudPlatform/buildpack-samples" rel="noopener noreferrer"&gt;https://github.com/GoogleCloudPlatform/buildpack-samples&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  About Serverless Expeditions
&lt;/h2&gt;

&lt;p&gt;Serverless Expeditions is a fun and cheeky video series that looks at what serverless means and how to build serverless apps with Google Cloud. &lt;/p&gt;

&lt;p&gt;Follow these hosts on dev.to at &lt;a href="https://dev.to/glasnt"&gt;@glasnt&lt;/a&gt; and &lt;a href="https://dev.to/momander"&gt;@martinomander&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>serverlessexpeditions</category>
      <category>cloudrun</category>
      <category>continuousdeployment</category>
      <category>buildpacks</category>
    </item>
    <item>
      <title>Deploying Django and Wagtail on Google Cloud</title>
      <dc:creator>Katie McLaughlin</dc:creator>
      <pubDate>Thu, 01 Oct 2020 23:03:15 +0000</pubDate>
      <link>https://dev.to/googlecloud/deploying-django-and-wagtail-on-google-cloud-17h0</link>
      <guid>https://dev.to/googlecloud/deploying-django-and-wagtail-on-google-cloud-17h0</guid>
      <description>&lt;p&gt;On this edition of Serverless Expeditions, we take a look at how to deploy more complex applications on Google Cloud, using Django as an example. &lt;/p&gt;

&lt;p&gt;Check out the video version of this blog post.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/Pk21gaNO6ag"&gt;
&lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;When moving legacy applications into serverless infrastructure, there is going to be some sort of state involved: there will be a database, media, and other components. Traditionally these can all be run within the confines of a virtual machine, where all the services can talk to each other in that environment. But when moving to serverless, all these components need to be specifically provisioned and connected together. &lt;/p&gt;

&lt;p&gt;While Cloud Run itself is stateless, it can be connected to other stateful services such as &lt;a href="https://cloud.google.com/sql/" rel="noopener noreferrer"&gt;Cloud SQL&lt;/a&gt; for relational databases, or &lt;a href="https://cloud.google.com/storage" rel="noopener noreferrer"&gt;Cloud Storage&lt;/a&gt; for static media hosting. &lt;/p&gt;

&lt;p&gt;In today's episode, we'll discuss &lt;a href="https://www.youtube.com/watch?v=AoNulKfMl_Q&amp;amp;list=PLIivdWyY5sqKiWvnaA5A8F3UQ0Xu5i49U" rel="noopener noreferrer"&gt;containerizing&lt;/a&gt; and deploying a typical Django application using Cloud Run and other Google Cloud services. The steps described can be applied to any website whose CMS uses the &lt;a href="http://djangoproject.com/" rel="noopener noreferrer"&gt;Django web framework&lt;/a&gt;, such as &lt;a href="https://wagtail.io/" rel="noopener noreferrer"&gt;Wagtail&lt;/a&gt; or &lt;a href="https://www.django-cms.org/en/" rel="noopener noreferrer"&gt;Django CMS&lt;/a&gt;. &lt;/p&gt;




&lt;p&gt;Django supports both PostgreSQL and MySQL, both of which are available in Cloud SQL, a managed database service. When connecting to a Cloud Run service, specifying &lt;code&gt;&lt;a href="https://cloud.google.com/sql/docs/postgres/connect-run#command-line" rel="noopener noreferrer"&gt;--add-cloudsql-instances&lt;/a&gt;&lt;/code&gt; allows the service and the database to communicate over an automatically encrypted connection. The authentication from the application to the database is achieved by specifying a connection URL, stored securely in &lt;a href="https://cloud.google.com/secret-manager" rel="noopener noreferrer"&gt;Secret Manager&lt;/a&gt;, loaded using the &lt;a href="https://pypi.org/project/google-cloud-secret-manager/" rel="noopener noreferrer"&gt;Python API&lt;/a&gt;. The database can then be populated by running &lt;code&gt;&lt;a href="https://docs.djangoproject.com/en/3.0/ref/django-admin/#django-admin-migrate" rel="noopener noreferrer"&gt;./manage.py migrate&lt;/a&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Static and media assets can be migrated to use Cloud Storage, with help from the &lt;code&gt;&lt;a href="https://pypi.org/project/django-storages/" rel="noopener noreferrer"&gt;django-storages&lt;/a&gt;&lt;/code&gt; package. By creating a new storage bucket and specifying the name in the django settings.py, then running &lt;code&gt;&lt;a href="https://docs.djangoproject.com/en/3.0/ref/contrib/staticfiles/#collectstatic" rel="noopener noreferrer"&gt;./manage.py collectstatic&lt;/a&gt;&lt;/code&gt;, the images and assets are uploaded to the bucket. &lt;/p&gt;

&lt;p&gt;However, those two manage.py commands -- &lt;code&gt;migrate&lt;/code&gt; and &lt;code&gt;collectstatic&lt;/code&gt; -- are typically run within an interactive terminal. Cloud Run has no such terminal, so these commands need to be run elsewhere. One method is to run these commands as part of the continuous integration process with &lt;a href="https://cloud.google.com/cloud-build" rel="noopener noreferrer"&gt;Cloud Build&lt;/a&gt;. Adding a 'migrate' step using the &lt;a href="https://github.com/GoogleCloudPlatform/ruby-docker/tree/master/app-engine-exec-wrapper" rel="noopener noreferrer"&gt;app-engine-exec-wrapper&lt;/a&gt; between the typical build and deploy steps allows migration to happen as part of deployment. &lt;/p&gt;

&lt;p&gt;If database migrations need to be manual, Cloud SQL proxy can be used to interact with a &lt;a href="https://cloud.google.com/sql/docs/postgres/connect-admin-proxy" rel="noopener noreferrer"&gt;Cloud SQL database locally&lt;/a&gt;. Details of how to set this up with Docker Compose are &lt;a href="https://github.com/GoogleCloudPlatform/django-demo-app-unicodex/blob/latest/docs/70-manual-deployments.md" rel="noopener noreferrer"&gt;available here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With a managed database, highly-available static assets, automated deployment pipelines and robust containerisation solutions, you can migrate your Django applications to Google Cloud with confidence.&lt;/p&gt;




&lt;p&gt;If you want to get started with these concepts, check out the Django Codelabs: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://codelabs.developers.google.com/codelabs/cloud-run-django/#0" rel="noopener noreferrer"&gt;Django on Cloud Run&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://codelabs.developers.google.com/codelabs/cloud-run-djangocms/#0" rel="noopener noreferrer"&gt;Wagtail on Cloud Run&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://codelabs.developers.google.com/codelabs/cloud-run-djangocms/#0" rel="noopener noreferrer"&gt;Django CMS on Cloud Run&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also find a sample implementation of these concepts at &lt;a href="https://github.com/GoogleCloudPlatform/django-demo-app-unicodex" rel="noopener noreferrer"&gt;GoogleCloudPlatform/django-demo-app-unicodex&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  About Serverless Expeditions
&lt;/h2&gt;

&lt;p&gt;Serverless Expeditions is a fun and cheeky video series that looks at what serverless means and how to build serverless apps with Google Cloud. &lt;/p&gt;

&lt;p&gt;Follow these hosts on Twitter at &lt;a href="https://twitter.com/glasnt" rel="noopener noreferrer"&gt;@glasnt&lt;/a&gt; and &lt;a href="https://twitter.com/martinomander" rel="noopener noreferrer"&gt;@martinomander&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>serverlessexpeditions</category>
      <category>django</category>
      <category>cloudrun</category>
    </item>
  </channel>
</rss>
