<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.0.1">Jekyll</generator><link href="https://dlite.cc/feed.xml" rel="self" type="application/atom+xml" /><link href="https://dlite.cc/" rel="alternate" type="text/html" /><updated>2024-09-05T19:51:16+00:00</updated><id>https://dlite.cc/feed.xml</id><title type="html">dlite.cc</title><subtitle>I'm Derek Haynes. Past: co-founded Scout APM, Scout Server Monitoring (both acquired).</subtitle><entry><title type="html">Streamlining RAG evaluation</title><link href="https://dlite.cc/2023/10/04/2023-eval-rag-apps.html" rel="alternate" type="text/html" title="Streamlining RAG evaluation" /><published>2023-10-04T11:00:00+00:00</published><updated>2023-10-04T11:00:00+00:00</updated><id>https://dlite.cc/2023/10/04/2023-eval-rag-apps</id><content type="html" xml:base="https://dlite.cc/2023/10/04/2023-eval-rag-apps.html">&lt;p&gt;&lt;img src=&quot;/img/posts/rag_eval/llm_product_curve.png&quot; alt=&quot;llm product curve&quot; /&gt;&lt;/p&gt;

&lt;p&gt;My first weeks working with GPT-4 were &lt;em&gt;magical&lt;/em&gt;. I was doing things that I previously thought were &lt;em&gt;impossible&lt;/em&gt;. However, as it went from promising proof-of-concept to something I wanted to share, I fell into a trough of sorrow. Every live demo felt like a YOLO moment … who knew what would happen?&lt;/p&gt;

&lt;p&gt;My app - a &lt;a href=&quot;https://github.com/opstower-ai/llm-opstower&quot;&gt;DevOps AI Assistant&lt;/a&gt; called OpsTower.ai - could perform a few mic-drop tasks, but getting it to &lt;em&gt;reliably reproduce&lt;/em&gt; those results was a frustrating, non-deterministic nightmare. A public release was perpetually one week away.&lt;/p&gt;

&lt;p&gt;Fast forward to today: OpsTower.ai is State of the Art (SOTA) in the three categories it competes in on the &lt;a href=&quot;https://github.com/opstower-ai/devops-ai-open-leaderboard&quot;&gt;DevOps AI Assistant Open Leaderboard&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/posts/rag_eval/leaderboard.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/img/posts/rag_eval/leaderboard.png&quot; width=&quot;500px&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;caption&quot;&gt;The DevOps AI Assistant Open Leaderboard is a set evaluation datasets for AWS Services, AWS Cloudwatch metrics, AWS Billing, and kubectl commands. The evaluation procedure is open-source and available on GitHub. Disclaimer! I created this to evaluate DevOps AI tools ... and yes, OpsTower (the tool I created) is on top...&lt;/div&gt;

&lt;p&gt;In this post, I share how I emerged from my AI trough of sorrow via a streamlined form of &lt;a href=&quot;https://eugeneyan.com/writing/llm-patterns/#how-to-apply-evals&quot;&gt;Eval Driven Development (EDD)&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;#what-is-eval-driven-development-edd&quot;&gt;What is Eval Driven Development (EDD)?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#how-to-make-edd-fast-eliminate-human-eval&quot;&gt;How to make EDD fast? Eliminate human eval.&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#dynamic-ground-truth&quot;&gt;Dynamic ground truth&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#model-based-eval&quot;&gt;Model-based eval&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#implementing-my-streamlined-edd-flow&quot;&gt;Implementing my streamlined EDD flow&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#creating-a-dynamic-ground-truth-dataset&quot;&gt;Creating a dynamic ground truth dataset&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#model-based-eval-1&quot;&gt;Model-based eval&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#edd-resources&quot;&gt;EDD Resources&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;what-is-eval-driven-development-edd&quot;&gt;What is Eval Driven Development (EDD)?&lt;/h2&gt;

&lt;p&gt;I first saw this term in Eugene Yan’s seminal blog post &lt;a href=&quot;https://eugeneyan.com/writing/llm-patterns&quot;&gt;&lt;em&gt;Patterns for Building LLM-based Systems &amp;amp; Products&lt;/em&gt;&lt;/a&gt;. I define EDD as:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Eval Driven Development (EDD) is a process that uses an evaluation suite to guide which levers (prompt, context, model params) to pull (and how far) to improve accuracy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;how-does-edd-compare-to-ml-evaluation-and-test-driven-development-tdd&quot;&gt;How does EDD compare to ML evaluation and Test Driven Development (TDD)?&lt;/h3&gt;

&lt;p&gt;EDD combines elements of machine learning model evaluation and software Test Driven Development (TDD). In the table below, I’ve summarized how model eval and TDD compare across several aspects. I indicate which approach is most applicable to EDD via the &lt;strong&gt;✅ EDD&lt;/strong&gt; label:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Aspect&lt;/th&gt;
      &lt;th&gt;ML Evaluation&lt;/th&gt;
      &lt;th&gt;TDD&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Nature&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Experimental: involves preprocessing, training, and tuning.&lt;/td&gt;
      &lt;td&gt;Deterministic: involves writing tests, then the code, and getting immediate feedback. &lt;strong&gt;✅ EDD&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Feedback Type&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Probabilistic: results can vary with slight changes. &lt;strong&gt;✅ EDD&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Deterministic: code either passes or fails the test.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Duration&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Can be long, especially with large datasets or complex models.&lt;/td&gt;
      &lt;td&gt;Typically short, as unit tests are designed to be quick and focused. &lt;strong&gt;✅ EDD&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Requires significant computational resources for complex models.&lt;/td&gt;
      &lt;td&gt;Minimal resources needed for most tests. &lt;strong&gt;✅ EDD&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Evaluation&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Might involve multiple metrics and can be context-dependent. &lt;strong&gt;✅ EDD&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Immediate and binary: pass or fail.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Tooling&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Advanced platforms available for prototyping but can be resource-intensive.&lt;/td&gt;
      &lt;td&gt;Wide range of tools for rapid development and continuous integration. &lt;strong&gt;✅ EDD&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Determinism&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Results can vary between runs; uncertainty is inherent. &lt;strong&gt;✅ EDD&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Results are consistent; code behavior is expected to be deterministic.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h4 id=&quot;an-evaluation-wrinkle-for-llm-backed-apps-external-systems&quot;&gt;An evaluation wrinkle for LLM-backed apps: external systems&lt;/h4&gt;

&lt;p&gt;LLM-backed apps - especially autonomous agents - are often deployed in environments where the underlying data they access is changing frequently. My autonomous agent, &lt;a href=&quot;https://github.com/opstower-ai/llm-opstower&quot;&gt;OpsTower.ai&lt;/a&gt;, interacts with AWS to retrieve real-time data about a customer’s cloud infrastructure. The data is not static and there are multiple approaches the LLM can take to assemble API calls to fetch information that can lead to the same result. It’s not feasible to build a static test suite or &lt;a href=&quot;https://microsoft.github.io/code-with-engineering-playbook/automated-testing/unit-testing/mocking/&quot;&gt;mock&lt;/a&gt; all of the possible API calls that the generated code may trigger.&lt;/p&gt;

&lt;p&gt;Summarizing the key elements:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;The EDD feedback cycle should be fast&lt;/strong&gt; (like TDD) so you can quickly iterate on prompts, context retrieval, and model parameters.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The feedback is probabilistic&lt;/strong&gt; (like ML evaluation) as the natural language responses you receive from an LLM may range from incorrect, partially correct, to correct.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The evaluation is dynamic&lt;/strong&gt; (unique to LLM apps) as the data they access changes frequently and we can’t mock all of the approaches LLM-generated code may use to access it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;how-to-make-edd-fast-eliminate-human-eval&quot;&gt;How to make EDD fast? Eliminate human eval.&lt;/h2&gt;

&lt;p&gt;The slowest part of EDD is human evaluation. It takes me 30 minutes to human evaluate a test run. Because it’s slow, most AI engineers revert to “vibe checks” for evaluation.&lt;/p&gt;

&lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;And less of this. Industry can&amp;#39;t depend on &amp;quot;vibes.&amp;quot; 🙃 &lt;a href=&quot;https://t.co/arkfedt1kq&quot;&gt;https://t.co/arkfedt1kq&lt;/a&gt;&lt;/p&gt;&amp;mdash; Ian Cairns (@cairns) &lt;a href=&quot;https://twitter.com/cairns/status/1712544833959768416?ref_src=twsrc%5Etfw&quot;&gt;October 12, 2023&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async=&quot;&quot; src=&quot;https://platform.twitter.com/widgets.js&quot; charset=&quot;utf-8&quot;&gt;&lt;/script&gt;

&lt;p&gt;A vibe check is just running your LLM app and evaluating the result by hand. This is slow, unlikely to have good coverage, and gets tedious quickly.&lt;/p&gt;

&lt;p&gt;Why do AI engineers resort to vibe checks when we likely come from backgrounds that value automated testing? It’s hard to come up with an automated system to evaluate an LLM-backed app. For example, here’s an example question and answer flow from OpsTower.ai:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/posts/rag_eval/flow.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/img/posts/rag_eval/flow.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s why evaluating this is hard:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;What’s the ground truth?&lt;/strong&gt; If I run this now, the CPU utilization will be different. I can’t mock every possible API call the LLM provides via code generation that delivers a correct result.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;How to evaluate the natural language response?&lt;/strong&gt; Variations in the text are likely fine, but small variations in referenced metrics can be a big deal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s see how I’ve approached a solution for the above problems.&lt;/p&gt;

&lt;h2 id=&quot;dynamic-ground-truth&quot;&gt;Dynamic ground truth&lt;/h2&gt;

&lt;p&gt;What if rather than using static ground truth like below:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Question&lt;/strong&gt;: What is the average cpu utilization of our RDS instances over the past hour?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ground Truth&lt;/strong&gt;: The average CPU utilization of our RDS instances over the past hour is approximately 3.71%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I instead reference a function that generates this context:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/posts/rag_eval/context_result.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/img/posts/rag_eval/context_result.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For this to work, we need to be confident that our LLM app will return a correct answer if we provide it with the correct context. Thankfully, this is a reasonable assumption. Here’s a &lt;a href=&quot;https://drive.google.com/file/d/1dB-RQhZC_Q1iAsHkNNdkqtxxXqYODFYy/view&quot;&gt;slide&lt;/a&gt; from Colin Jarvis of OpenAI with a matrix of typical RAG evaluation results:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/posts/rag_eval/moat.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/img/posts/rag_eval/moat.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;caption&quot;&gt;In this example, incorrect retrieval is responsible for 4x the number of incorrect answers vs. other sources.&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Colin shows that only 5% of answers are incorrect when the retrieval is correct.&lt;/strong&gt; While there are no guarantees with LLMs, the surest one I’ve found for getting an inaccurate response: feed the LLM bad context. For example, if you ask for the weather &lt;em&gt;today&lt;/em&gt; but you actually insert the forecast for &lt;em&gt;tomorrow&lt;/em&gt; into the context, the LLM will not magically change your context and fetch the weather for the correct date.&lt;/p&gt;

&lt;p&gt;So, we can assemble dynamic ground truth like this:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;User question&lt;/li&gt;
  &lt;li&gt;Execute reference function to generate context&lt;/li&gt;
  &lt;li&gt;Insert context into LLM prompt&lt;/li&gt;
  &lt;li&gt;LLM generates answer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next, we need to evaluate test answers versus our dynamic ground truth.&lt;/p&gt;

&lt;h2 id=&quot;model-based-eval&quot;&gt;Model-based eval&lt;/h2&gt;

&lt;p&gt;When we’re working with an LLM, we’re evaluating natural language responses. Natural Language Processing (NLP) is a classical machine learning domain and these models have their own evaluation techniques and metrics like BLEU, ROUGE, BERTScore, and MoverScore. Why don’t we just use those metrics?&lt;/p&gt;

&lt;p&gt;There’s actually poor correlation between these NLP evaluation metrics and human judgments. From Eugene Yan’s excellent post &lt;em&gt;&lt;a href=&quot;https://eugeneyan.com/writing/llm-patterns/&quot;&gt;Patterns for Building LLM-based Systems &amp;amp; Products&lt;/a&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;BLEU, ROUGE, and others have had negative correlation with how humans evaluate fluency. They also showed moderate to less correlation with human adequacy scores. In particular, BLEU and ROUGE have low correlation with tasks that require creativity and diversity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, lets compare two responses to the question “What is the average CPU utilization of our RDS instances over the past hour?”:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Ground truth: The average CPU utilization of our RDS instances over the past hour is approximately 3.71%.&lt;/li&gt;
  &lt;li&gt;Prediction: The average CPU utilization of our RDS instances over the past hour is approximately 37.1%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I moved the decimal point in the prediction, resulting in a far different answer with a small change in the text. Here’s the eval metrics:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Metric&lt;/th&gt;
      &lt;th&gt;Value/Sub-metric&lt;/th&gt;
      &lt;th&gt;Score&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;BLEU&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;BLEU-1&lt;/td&gt;
      &lt;td&gt;0.9333&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;BLEU-2&lt;/td&gt;
      &lt;td&gt;0.9286&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;BLEU-3&lt;/td&gt;
      &lt;td&gt;0.9231&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;BLEU-4&lt;/td&gt;
      &lt;td&gt;0.9167&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;ROUGE-1&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Recall&lt;/td&gt;
      &lt;td&gt;0.9333&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Precision&lt;/td&gt;
      &lt;td&gt;0.9333&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;F1&lt;/td&gt;
      &lt;td&gt;0.9333&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;ROUGE-2&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Recall&lt;/td&gt;
      &lt;td&gt;0.9286&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Precision&lt;/td&gt;
      &lt;td&gt;0.9286&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;F1&lt;/td&gt;
      &lt;td&gt;0.9286&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;ROUGE-3&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Recall&lt;/td&gt;
      &lt;td&gt;0.9231&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Precision&lt;/td&gt;
      &lt;td&gt;0.9231&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;F1&lt;/td&gt;
      &lt;td&gt;0.9231&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;ROUGE-L&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Recall&lt;/td&gt;
      &lt;td&gt;0.9333&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Precision&lt;/td&gt;
      &lt;td&gt;0.9333&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;F1&lt;/td&gt;
      &lt;td&gt;0.9333&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Cosine Similarity&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
      &lt;td&gt;0.8901&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;div class=&quot;caption&quot;&gt;A small change in the text (moving a decimal point) has minimal impact on the eval metrics but a significant impact on human judgement.&lt;/div&gt;

&lt;p&gt;The scores are close to 1, which indicates a high similarity between the reference and the prediction even though the a human would judge the response as incorrect.&lt;/p&gt;

&lt;p&gt;Could an LLM fill in as a human evaluator?&lt;/p&gt;

&lt;p&gt;It turns out, LLMs are good substitutes for human evaluators:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;GPT-4 as an evaluator had a high Spearman correlation with human judgments (0.514), outperforming all previous methods. It also outperformed traditional metrics on aspects such as coherence, consistency, fluency, and relevance. On topical chat, it did better than traditional metrics such as ROUGE-L, BLEU-4, and BERTScore across several criteria such as naturalness, coherence, engagingness, and groundedness.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Overall, they found that GPT-4 not only provided consistent scores but could also give detailed explanations for those scores. Under the single answer grading paradigm, GPT-4 had higher agreement with humans (85%) than the humans had amongst themselves (81%). This suggests that GPT-4’s judgment aligns closely with the human evaluators.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Personally, I saw almost identical scoring when switching from human eval to LLM eval:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/rag_eval/human_model_eval.png&quot; alt=&quot;human_vs_llm&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;implementing-my-streamlined-edd-flow&quot;&gt;Implementing my streamlined EDD flow&lt;/h2&gt;

&lt;p&gt;So these are the components we need to implement for a streamlined EDD flow:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Creating a dynamic ground truth dataset.&lt;/li&gt;
  &lt;li&gt;Implementing an LLM-based eval.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;creating-a-dynamic-ground-truth-dataset&quot;&gt;Creating a dynamic ground truth dataset&lt;/h3&gt;

&lt;h4 id=&quot;1-generate-dataset-questions-can-use-an-llm-to-assist&quot;&gt;1. Generate dataset questions (can use an LLM to assist)&lt;/h4&gt;

&lt;p&gt;To start, I use ChatGPT to generate a few initial questions for a new evaluation dataset. I’m working on &lt;a href=&quot;https://github.com/opstower/llm-opstower&quot;&gt;OpsTower.ai&lt;/a&gt;, a DevOps AI Assistant, so lets create a dataset of questions about AWS CloudWatch Logs. &lt;a href=&quot;https://chat.openai.com/share/8ca6e589-e0f0-4c70-a7a7-e96c0b5d76fb&quot;&gt;Here&lt;/a&gt; is my transcript.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/rag_eval/cloudwatch_logs_eval.png&quot; alt=&quot;cloudwatch logs&quot; /&gt;&lt;/p&gt;
&lt;div class=&quot;caption&quot;&gt;I use ChatGPT to generate dataset questions. This is for an AWS Cloudwatch Logs dataset to test &lt;a href=&quot;https://github.com/opstower/llm-opstower&quot;&gt;OpsTower.ai&lt;/a&gt;.&lt;/div&gt;

&lt;p&gt;I then paste these questions in a &lt;code class=&quot;highlighter-rouge&quot;&gt;aws_cloudwatch_logs.csv&lt;/code&gt; file:&lt;/p&gt;

&lt;script src=&quot;https://gist.github.com/itsderek23/be29a12b3b565a7918004807e440d387.js&quot;&gt;&lt;/script&gt;

&lt;h4 id=&quot;2-programmatically-generate-responses-for-each-question&quot;&gt;2. Programmatically generate responses for each question&lt;/h4&gt;

&lt;p&gt;Next I programmatically generate responses to each of these questions using the current AI agent. For my app, it looks like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;demo_source&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Eval&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;VendorTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;save!&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;AgentTest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;source: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;demo_source&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;dataset_file: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;aws_cloudwatch_logs.csv&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;run!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;When the test completes, I view the answers in the UI:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/posts/rag_eval/aws_cloudwatch_logs_results.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/img/posts/rag_eval/aws_cloudwatch_logs_results.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;caption&quot;&gt;Above is a screenshot of the custom evaluation results page within my app. It provides summary stats and details on each question-answer pair.&lt;/div&gt;

&lt;h4 id=&quot;3-save-ground-truth-functions-for-generating-context&quot;&gt;3. Save ground truth functions for generating context&lt;/h4&gt;

&lt;p&gt;I then review the results. Results generally fall into three buckets:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Works as-is - the agent generated valid context and answered the question correctly. I’ll reuse the functions that generated the context as ground truth.&lt;/li&gt;
  &lt;li&gt;Hybrid - the agent did not generate valid context, but the code it created to generate context is a solid starting point. I can modify the code then save it as a reference function.&lt;/li&gt;
  &lt;li&gt;Fully human-generated - the agent failed miserably at code generation. I’ll write a new function from scratch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I like to go from easy to hard. The easiest ones are “works as-is” as I can simply copy and save the generated code.&lt;/p&gt;

&lt;p&gt;To create a reference function from a “works as-is” result, I generate a saved method from the code the agent generated then reference that function by ID in the &lt;code class=&quot;highlighter-rouge&quot;&gt;aws_cloudwatch_logs.csv&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;For example, the question &lt;em&gt;“How many CloudWatch Log Groups do I have?”&lt;/em&gt; is correctly answered below:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/posts/rag_eval/how_many_logs_chat.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/img/posts/rag_eval/how_many_logs_chat.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I want to save the code the LLM generated, starting with the &lt;code class=&quot;highlighter-rouge&quot;&gt;get_cloudwatch_log_groups_count&lt;/code&gt; method. In my app, I can do this by executing:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;saved_methods&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;SavedMethod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create_from_chat!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;482273fb-4c79-4cd2-bc4d-382945c38e42&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;saved_methods&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;74294dde-8cd0-4803-a0c2-7c117b8b15de&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I then paste the saved method ID into the &lt;code class=&quot;highlighter-rouge&quot;&gt;aws_cloudwatch_logs.csv&lt;/code&gt; file:&lt;/p&gt;

&lt;script src=&quot;https://gist.github.com/itsderek23/3faa7f4f7e677362e3eab7f87cec617b.js&quot;&gt;&lt;/script&gt;

&lt;p&gt;I repeat this process for each “works as-is” result. Hybrid and fully human-generated results are handled similarly, but with more changes to the code.&lt;/p&gt;

&lt;h5 id=&quot;ground-truth-prompt-template&quot;&gt;Ground Truth Prompt Template&lt;/h5&gt;

&lt;p&gt;I use the prompt to below to generate the ground truth answer from the referenced function we saved earlier. The prompt template looks like this:&lt;/p&gt;

&lt;script src=&quot;https://gist.github.com/itsderek23/b74956714d8f3303c9e77de89e98a707.js&quot;&gt;&lt;/script&gt;

&lt;p&gt;An evaluated prompt example:&lt;/p&gt;

&lt;script src=&quot;https://gist.github.com/itsderek23/c53bce75ad844e6519563bcd2b54594e.js&quot;&gt;&lt;/script&gt;

&lt;p&gt;This will return text similar to “You have 23 Cloudwatch Log groups in your AWS account.”&lt;/p&gt;

&lt;h3 id=&quot;model-based-eval-1&quot;&gt;Model-based eval&lt;/h3&gt;

&lt;p&gt;The prompt template below is my evaluation prompt. It generates a confidence score, comparing the answer from the agent vs the response generated from the dynamic ground truth prompt:&lt;/p&gt;

&lt;script src=&quot;https://gist.github.com/itsderek23/8a1fde245a24889b68b521cdf3e0f09a.js&quot;&gt;&lt;/script&gt;

&lt;p&gt;Here’s an evaluated prompt example (just focusing on template variables):&lt;/p&gt;

&lt;script src=&quot;https://gist.github.com/itsderek23/5b5c224527dc1fbe2e5afa1fa96a7768.js&quot;&gt;&lt;/script&gt;

&lt;p&gt;When the eval is ran, I’ll see output like the following for each question:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/posts/rag_eval/incorrect.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/img/posts/rag_eval/incorrect.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;caption&quot;&gt;This shows the evaluation result on a single question-answer pair. In this case, the result did not pass evaluation.&lt;/div&gt;

&lt;h3 id=&quot;rinse-and-repeat&quot;&gt;Rinse and repeat&lt;/h3&gt;

&lt;p&gt;Once I’m getting acceptable accuracy on an evaluation dataset (ex: 80% or greater), I’ll repeat the process outlined above:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Add questions with ChatGPT.&lt;/li&gt;
  &lt;li&gt;Add reference functions.&lt;/li&gt;
  &lt;li&gt;Run evaluation, tweak the app, run evaluation, etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;I’ve gone all-in on the LLM.&lt;/p&gt;

&lt;p&gt;I’ve dramatically increased the accuracy, capabilities, and reliability of my LLM-backed app by leveraging a more automated, streamlined form of Eval Driven Development (EDD). My flavor of EDD leans on the LLM to generate the question dataset, uses human-evaluated reference functions to generate context, re-assembles ground truth answers via the LLM, and finally uses the LLM again to simulate human evaluation.&lt;/p&gt;

&lt;p&gt;For my app, the resulting confidence scores of this approach are typically within 5% of human-eval scoring at a fraction of the time spent.&lt;/p&gt;

&lt;h3 id=&quot;edd-resources&quot;&gt;EDD Resources&lt;/h3&gt;

&lt;p&gt;To see example datasets, reference functions, and evaluation prompts, checkout the &lt;a href=&quot;https://github.com/opstower-ai/devops-ai-open-leaderboard&quot;&gt;DevOps AI Assistant Open Leaderboard&lt;/a&gt; on GitHub.&lt;/p&gt;

&lt;p&gt;See &lt;a href=&quot;https://github.com/itsderek23/awesome-eval-driven-development&quot;&gt;Awesome Eval Driven Development&lt;/a&gt; on GitHub a continually updated set of resources related to Eval Driven Development.&lt;/p&gt;</content><author><name></name></author><summary type="html"></summary></entry><entry><title type="html">Implementing an LLM Agent to complete tasks using Google with Ruby</title><link href="https://dlite.cc/2023/05/08/2023-boxcars-ruby-train-intro.html" rel="alternate" type="text/html" title="Implementing an LLM Agent to complete tasks using Google with Ruby" /><published>2023-05-08T11:00:00+00:00</published><updated>2023-05-08T11:00:00+00:00</updated><id>https://dlite.cc/2023/05/08/2023-boxcars-ruby-train-intro</id><content type="html" xml:base="https://dlite.cc/2023/05/08/2023-boxcars-ruby-train-intro.html">&lt;p&gt;&lt;img src=&quot;/img/posts/boxcars_react/cover.png&quot; height=&quot;600&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Being lazy, I’m &lt;em&gt;very&lt;/em&gt; interested in using agents powered by LLMs to accomplish tasks for me. In this post, I explore how this is done with &lt;a href=&quot;github.com/boxcarsAI/boxcars&quot;&gt;Boxcars&lt;/a&gt;, a Ruby gem inspired by &lt;a href=&quot;https://github.com/hwchase17/langchain&quot;&gt;Langchain&lt;/a&gt; for building LLM apps.&lt;/p&gt;

&lt;h2 id=&quot;quick-intro-to-boxcars&quot;&gt;Quick intro to Boxcars&lt;/h2&gt;

&lt;p&gt;Boxcars is a Ruby gem that makes it easy to build applications with LLMs. I’ve found it much easier to use than Langchain as it provides “just enough” abstractions to interact with an LLM and act on the output. See the &lt;a href=&quot;https://github.com/BoxcarsAI/boxcars/wiki/Getting-Started&quot;&gt;getting started docs&lt;/a&gt; to get going on your own.&lt;/p&gt;

&lt;h2 id=&quot;a-single-boxcar-train-for-realtime-weather&quot;&gt;A single boxcar train for realtime weather&lt;/h2&gt;

&lt;p&gt;In this example, I’ll setup a train with a custom Boxcar, &lt;code class=&quot;highlighter-rouge&quot;&gt;GoogleAnswerBox&lt;/code&gt; (&lt;a href=&quot;https://gist.github.com/itsderek23/7fabe8ec9d1874593ccec015b0f92b5c&quot;&gt;source&lt;/a&gt;). &lt;code class=&quot;highlighter-rouge&quot;&gt;GoogleAnswerBox&lt;/code&gt; returns the answer box in Google search results (the box at the top of the page that is displayed if Google can answer your question directly) as JSON.&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;boxcars = [GoogleAnswerBox.new]
train = Boxcars.train.new(boxcars: boxcars)
train.run(&quot;what is the temperature in Fort Collins?&quot;)
 =&amp;gt; &quot;The current temperature in Fort Collins is 45 degrees Fahrenheit.&quot; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This matches the result when I run the same query direct on Google in my web browser:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/boxcars_react/temp_answer_box.jpg&quot; height=&quot;300&quot; /&gt;&lt;/p&gt;

&lt;p&gt;How about a stock price?&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;train.run(&quot;what is the Tesla stock price?&quot;)
=&amp;gt; &quot;The current Tesla stock price in USD is $169.36.\nNext Actions:\n1. What was the opening price of Tesla stock today?\n2. How has the Tesla stock price changed over the past week?\n3. What is the market capitalization of Tesla?&quot; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Or a holiday?&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;train.run(&quot;When is Memorial Day?&quot;)
 =&amp;gt; &quot;Memorial Day is on Monday, May 29, 2023.\nNext Actions: None, as the answer is straightforward.&quot; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Or the time?&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;train.run(&quot;what time is it in Denver?&quot;)
 =&amp;gt; &quot;The current time in Denver is 08:41 AM. \nNext Actions: None, as the user's question has been answered.&quot; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;How does Boxcars take my query, interact with an external tool (Google Search), and generate an answer?&lt;/p&gt;

&lt;h2 id=&quot;react-reason--act-on-ruby&quot;&gt;ReAct (Reason + Act) on Ruby&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/boxcars_react/tools.png&quot; height=&quot;400&quot; /&gt;&lt;/p&gt;

&lt;p&gt;If I asked you for the current temperature, time, or score of an NBA playoff game, you would need an external tool to provide me with this information. It’s not stored in your brain, but your brain can determine which tool to use, interact with the tool, process the data displayed in the tool, and finally provide me with an answer.&lt;/p&gt;

&lt;p&gt;Just like your brain, an LLM cannot provide you with information on current events, but you can give an LLM information on external tools they can use to fetch realtime data. Perhaps the most popular approach for having an LLM reason and use external tools is the ReAct (Reason + Act) framework, introduced in &lt;a href=&quot;https://arxiv.org/abs/2210.03629&quot;&gt;this paper&lt;/a&gt; (Shunyu et al., 2022). In the example above, Boxcars uses a Zero Shot (no training) ReAct prompt to provide answers.&lt;/p&gt;

&lt;p&gt;Let’s walk through how Boxcars implements ReAct when using the &lt;code class=&quot;highlighter-rouge&quot;&gt;GoogleAnswerBox&lt;/code&gt; tool.&lt;/p&gt;

&lt;h3 id=&quot;first-prompt&quot;&gt;First prompt&lt;/h3&gt;

&lt;p&gt;I’ll start by looking at the LLM prompt generated by the Boxcars Train:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: system &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
Answer the following questions as best you can. You have access to the following actions:

AnswerBox: useful for when you need to answer questions that require realtime data.You should ask targeted questions

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one from this list: [AnswerBox]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation sequence can repeat N times)
Thought: I know the final answer
Final Answer: the final answer to the original input question
Next Actions: Up to 3 logical suggested next questions for the user to ask after getting this answer.
Remember to start a line with &quot;Final Answer:&quot; to give me the final answer.
Begin!
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: user &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
Question: what is the temperature in Fort Collins?
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: assistant &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
Thought: 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Taking a step back: this is fascinating. There’s no training involved. It takes under 140 words of system instructions to get the answer to our question. The prompt is first broken down into three ChatGPT-specific roles:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: system &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt; - these instructions guide the model throughout the conversation.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: user &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt; - the person asking questions to ChatGPT.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: assistant &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt; - responses from ChatGPT to questions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can &lt;a href=&quot;https://help.openai.com/en/articles/7437211-chat-beta&quot;&gt;learn more about ChatGPT roles&lt;/a&gt; from their docs.&lt;/p&gt;

&lt;p&gt;Note how &lt;code class=&quot;highlighter-rouge&quot;&gt;Thought:&lt;/code&gt; (the last line) is empty. This is the start of the Thought/Action/Action Input/Observation we’re asking ChatGPT to complete.&lt;/p&gt;

&lt;p&gt;ChatGPT responds with the following:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Thought: I need to use the AnswerBox action to get the current temperature in Fort Collins.
Observation: I need to use the AnswerBox action to get the current temperature in Fort Collins
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ChatGPT generates this reasoning from the GoogleAnswerBox boxcar description that is included in the prompt:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;AnswerBox: useful for when you need to answer questions that require realtime data.You should ask targeted questions
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If I omit the actions portion of the prompt and send the prompt to ChatGPT, I’ll get a response like this:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Thought: I should look up the current temperature in Fort Collins.
Action: Use a search engine to find the current temperature in Fort Collins.
Action Input: &quot;Fort Collins current temperature&quot; in a search engine.
Observation: The current temperature in Fort Collins is displayed on a search results page.
Thought: I should provide the temperature to the user.
Final Answer: The current temperature in Fort Collins is [insert current temperature].
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ChatGPT realizes it should use a search to collect current information but it doesn’t have access to action to fetch the current temperature.&lt;/p&gt;

&lt;h3 id=&quot;second-prompt&quot;&gt;Second prompt&lt;/h3&gt;

&lt;p&gt;The Boxcars train is now ready to continue the thought/action/action input/observation loop by sending a second prompt. For brevity, I’ve omitted the system and user roles which remain the same:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: assistant &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
Thought:  I need to use the AnswerBox action to get the current temperature in Fort Collins.
Observation: I need to use the AnswerBox action to get the current temperature in Fort Collins.
Thought:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ChatGPT responds with:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;I should ask for the current temperature in Fort Collins.
Action: AnswerBox
Action Input: &quot;What is the current temperature in Fort Collins?&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;Boxcars::Train object&lt;/code&gt; takes the ChatGPT response and parses out the &lt;code class=&quot;highlighter-rouge&quot;&gt;Action&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;Action Input&lt;/code&gt;, mapping these to the available actions (just AnswerBox for now). &lt;code class=&quot;highlighter-rouge&quot;&gt;GoogleAnswerBox#run&lt;/code&gt; is called with the &lt;code class=&quot;highlighter-rouge&quot;&gt;Action Input&lt;/code&gt;, returning the text below:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Answer: {&quot;type&quot;:&quot;weather_result&quot;,&quot;temperature&quot;:&quot;49&quot;,&quot;unit&quot;:&quot;Fahrenheit&quot;,&quot;precipitation&quot;:&quot;0%%&quot;,&quot;humidity&quot;:&quot;65%%&quot;,&quot;wind&quot;:&quot;3 mph&quot;,&quot;location&quot;:&quot;Weather&quot;,&quot;date&quot;:&quot;Monday 7:00 AM&quot;,&quot;weather&quot;:&quot;Mostly sunny&quot;,&quot;thumbnail&quot;:&quot;https://serpapi.com/searches/6458f883ce87f81e4d7973c2/images/dccefb93a84c042f2c5d64fd510927300f2ceedeb076f4ff.png&quot;,&quot;forecast&quot;:[{&quot;day&quot;:&quot;Monday&quot;,&quot;weather&quot;:&quot;Partly cloudy&quot;,&quot;temperature&quot;:{&quot;high&quot;:&quot;73&quot;,&quot;low&quot;:&quot;45&quot;},&quot;thumbnail&quot;:&quot;https://serpapi.com/searches/6458f883ce87f81e4d7973c2/images/dccefb93a84c042f85705f9091acff95c6669fc2faa0664facd6d2464297ada0.png&quot;},{&quot;day&quot;:&quot;Tuesday&quot;,&quot;weather&quot;:&quot;Mostly sunny&quot;,&quot;temperature&quot;:{&quot;high&quot;:&quot;78&quot;,&quot;low&quot;:&quot;48&quot;},&quot;thumbnail&quot;:&quot;https://serpapi.com/searches/6458f883ce87f81e4d7973c2/images/dccefb93a84c042f85705f9091acff9581bc24fa381ee107ae5c17df0e61b10a.png&quot;},{&quot;day&quot;:&quot;Wednesday&quot;,&quot;weather&quot;:&quot;Scattered thunderstorms&quot;,&quot;temperature&quot;:{&quot;high&quot;:&quot;72&quot;,&quot;low&quot;:&quot;50&quot;},&quot;thumbnail&quot;:&quot;https://serpapi.com/searches/6458f883ce87f81e4d7973c2/images/dccefb93a84c042f85705f9091acff9502e1868124e15c11dc1beb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Yes, that is just an ugly truncated JSON representation of the Google answer box in the search result. &lt;code class=&quot;highlighter-rouge&quot;&gt;GoogleAnswerBox&lt;/code&gt; does not parse the contents of the answer box: there are many variations of answer box formats. Why not let ChatGPT parse this for me?&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;third-and-final-prompt&quot;&gt;Third and final prompt&lt;/h3&gt;

&lt;p&gt;The Train now sends off a prompt with the answer box JSON above in the &lt;code class=&quot;highlighter-rouge&quot;&gt;Observation&lt;/code&gt; to ChatGPT:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: assistant &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
Thought:  I need to use the AnswerBox action to get the current temperature in Fort Collins.
Observation: I need to use the AnswerBox action to get the current temperature in Fort Collins.
Thought: I should ask for the current temperature in Fort Collins by using the AnswerBox action.
Action: AnswerBox
Action Input: &quot;What is the current temperature in Fort Collins?&quot;
Observation: Answer:{&quot;type&quot;:&quot;weather_result&quot;,&quot;temperature&quot;:&quot;49&quot;,&quot;unit&quot;:&quot;Fahrenheit&quot;,&quot;precipitation&quot;:&quot;0%%&quot;,&quot;humidity&quot;:&quot;65%%&quot;,&quot;wind&quot;:&quot;3 mph&quot;,&quot;location&quot;:&quot;Weather&quot;,&quot;date&quot;:&quot;Monday 7:00 AM&quot;,&quot;weather&quot;:&quot;Mostly sunny&quot;,&quot;thumbnail&quot;:&quot;https://serpapi.com/searches/6458f883ce87f81e4d7973c2/images/dccefb93a84c042f2c5d64fd510927300f2ceedeb076f4ff.png&quot;,&quot;forecast&quot;:[{&quot;day&quot;:&quot;Monday&quot;,&quot;weather&quot;:&quot;Partly cloudy&quot;,&quot;temperature&quot;:{&quot;high&quot;:&quot;73&quot;,&quot;low&quot;:&quot;45&quot;},&quot;thumbnail&quot;:&quot;https://serpapi.com/searches/6458f883ce87f81e4d7973c2/images/dccefb93a84c042f85705f9091acff95c6669fc2faa0664facd6d2464297ada0.png&quot;},{&quot;day&quot;:&quot;Tuesday&quot;,&quot;weather&quot;:&quot;Mostly sunny&quot;,&quot;temperature&quot;:{&quot;high&quot;:&quot;78&quot;,&quot;low&quot;:&quot;48&quot;},&quot;thumbnail&quot;:&quot;https://serpapi.com/searches/6458f883ce87f81e4d7973c2/images/dccefb93a84c042f85705f9091acff9581bc24fa381ee107ae5c17df0e61b10a.png&quot;},{&quot;day&quot;:&quot;Wednesday&quot;,&quot;weather&quot;:&quot;Scattered thunderstorms&quot;,&quot;temperature&quot;:{&quot;high&quot;:&quot;72&quot;,&quot;low&quot;:&quot;50&quot;},&quot;thumbnail&quot;:&quot;https://serpapi.com/searches/6458f883ce87f81e4d7973c2/images/dccefb93a84c042f85705f9091acff9502e1868124e15c11dc1beb
Thought:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ChatGPT returns:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Final Answer: The current temperature in Fort Collins is 49 degrees Fahrenheit.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Our ZeroShot train sees the magic &lt;code class=&quot;highlighter-rouge&quot;&gt;Final Answer&lt;/code&gt; text, exits, and returns the result.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: the temperature increased from 45 to 49 degrees while writing this blog post.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;going-deeper-a-two-boxcar-train-to-query-both-a-rails-db-and-google&quot;&gt;Going deeper: a two boxcar train to query both a Rails DB and Google&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/boxcars_react/train.png&quot; height=&quot;400&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Just like you’ll use multiple tools to accomplish a task, the ReAct framework can do the same. Let’s setup a Boxcar train with two boxcars, ActiveRecord for searching my Rails database and Google Answer Box. I’ll find out the time in a user’s timezone:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;boxcars&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Boxcars&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;GoogleAnswerBox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Boxcars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;boxcars: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boxcars&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;train&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;what country does the user derek@glass.io user's timezone belong to?&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;The timezone belonging to the user derek@glass.io belongs to the United States.&quot;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;first-prompt--completion-cycle-highlights&quot;&gt;First prompt =&amp;gt; completion cycle highlights&lt;/h3&gt;

&lt;p&gt;There are now two actions listed in the prompt (note that Glass is the name of the Rails application):&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;You have access to the following actions:

Glass: useful for when you need to query a database for an application named .
AnswerBox: useful for when you need to answer questions that require realtime data.You should ask targeted questions
Use the following format:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ChatGPT returns:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;We need to use a database to query the user's timezone and then determine the country it belongs to.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;second-prompt--completion-cycle-highlights&quot;&gt;Second prompt =&amp;gt; completion cycle highlights&lt;/h3&gt;

&lt;p&gt;The following is now appended to the second prompt:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: assistant &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
Thought:  We need to use a database to query the user's timezone and then determine the country it belongs to.
Observation: We need to use a database to query the user's timezone and then determine the country it belongs to.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ChatGPT responds with:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;We can use Glass to query the database for the user's timezone and then use that information to determine the country it belongs to.
Action: Glass
Action Input: &quot;Query timezone for derek@glass.io user&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Our ZeroShot Train parses the output above and calls &lt;code class=&quot;highlighter-rouge&quot;&gt;Boxcars::Boxcar::ActiveRecord#run&lt;/code&gt; with the &lt;code class=&quot;highlighter-rouge&quot;&gt;Action Input&lt;/code&gt; above. This uses ChatGPT to take a natural language query and convert it to ActiveRecord code (see &lt;a href=&quot;/2023/05/06/2023-boxcars-ruby-intro.html&quot;&gt;this post&lt;/a&gt; for more on how the ActiveRecord boxcar works). ChatGPT returns:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ARCode: `User.where(email: 'derek@glass.io').pluck(:time_zone)` 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The boxcar executes this code, returning:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;America/Denver
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;third-prompt--completion-cycle-highlights&quot;&gt;Third prompt =&amp;gt; completion cycle highlights&lt;/h3&gt;

&lt;p&gt;Our assistant role now looks like:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: assistant &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
Thought:  We need to use a database to query the user's timezone and then determine the country it belongs to.
Observation: We need to use a database to query the user's timezone and then determine the country it belongs to.
Thought: We can use Glass to query the database for the user's timezone and then use that information to determine the country it belongs to.
Action: Glass
Action Input: &quot;Query timezone for derek@glass.io user&quot;
Observation: America/Denver
Thought:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ChatGPT responds with:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Now that we have the user's timezone, we can use AnswerBox to determine the country it belongs to.
Action: AnswerBox
Action Input: &quot;What country does the timezone America/Denver belong to?&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Our train now executes the &lt;code class=&quot;highlighter-rouge&quot;&gt;GoogleAnswerBox&lt;/code&gt; boxcar, returning the following truncated JSON result:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{&quot;type&quot;:&quot;organic_result&quot;,&quot;title&quot;:&quot;America/Denver - Time zone in United States - TRAVEL GUIDE&quot;,&quot;link&quot;:&quot;https://www.travelguide.de/en/timezone/america-denver/#:~:text=America%%2FDenver%%20is%%20a%%20time,Mountain%%20Standard%%20Time%%20(MST).&quot;,&quot;displayed_link&quot;:&quot;https://www.travelguide.de › Home › Time zones&quot;,&quot;hover_info&quot;:[{&quot;text&quot;:&quot;Mountain Standard Time&quot;,&quot;info&quot;:&quot;United States Six states are fully contained in the Mountain Time Zone: Colorado. Montana. New Mexico.&quot;,&quot;title&quot;:&quot;Mountain Time Zone - Wikipedia&quot;,&quot;link&quot;:&quot;https://en.wikipedia.org/wiki/Mountain_Time_Zone&quot;,&quot;displayed_link&quot;:&quot;https://en.wikipedia.org › wiki › Mountain_Time_Zone&quot;}],&quot;snippet&quot;:&quot;America/Denver is a time zone in United States. It corresponds to the Mountain Standard Time (MST).&quot;,&quot;snippet_highlighted_words&quot;:[&quot;United States&quot;],&quot;thumbnail&quot;:&quot;https://serpapi.com/searches/64590f407f836163a84eff66/images/6d20911aeb0d64ec26e1f63ef2e6c3f28ed8ae8265617e28.png&quot;,&quot;about_this_result&quot;:{&quot;source&quot;:{&quot;description&quot;:&quot;travelguide.de was first indexed by
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;fourth-prompt--completion-cycle-highlights&quot;&gt;Fourth prompt =&amp;gt; completion cycle highlights&lt;/h3&gt;

&lt;p&gt;We’re now most of the way through. The train appends the JSON result above as observation to the prompt:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: assistant &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
Thought:  We need to use a database to query the user's timezone and then determine the country it belongs to.
Observation: We need to use a database to query the user's timezone and then determine the country it belongs to.
Thought: We can use Glass to query the database for the user's timezone and then use that information to determine the country it belongs to.
Action: Glass
Action Input: &quot;Query timezone for derek@glass.io user&quot;
Observation: America/Denver
Thought: Now that we have the user's timezone, we can use AnswerBox to determine the country it belongs to.
Action: AnswerBox
Action Input: &quot;What country does the timezone America/Denver belong to?&quot;
Observation: Answer: {&quot;type&quot;:&quot;organic_result&quot;,&quot;title&quot;:&quot;America/Denver - Time zone in United States - TRAVEL GUIDE&quot;,&quot;link&quot;:&quot;https://www.travelguide.de/en/timezone/america-denver/#:~:text=America%%2FDenver%%20is%%20a%%20time,Mountain%%20Standard%%20Time%%20(MST).&quot;,&quot;displayed_link&quot;:&quot;https://www.travelguide.de › Home › Time zones&quot;,&quot;hover_info&quot;:[{&quot;text&quot;:&quot;Mountain Standard Time&quot;,&quot;info&quot;:&quot;United States Six states are fully contained in the Mountain Time Zone: Colorado. Montana. New Mexico.&quot;,&quot;title&quot;:&quot;Mountain Time Zone - Wikipedia&quot;,&quot;link&quot;:&quot;https://en.wikipedia.org/wiki/Mountain_Time_Zone&quot;,&quot;displayed_link&quot;:&quot;https://en.wikipedia.org › wiki › Mountain_Time_Zone&quot;}],&quot;snippet&quot;:&quot;America/Denver is a time zone in United States. It corresponds to the Mountain Standard Time (MST).&quot;,&quot;snippet_highlighted_words&quot;:[&quot;United States&quot;],&quot;thumbnail&quot;:&quot;https://serpapi.com/searches/64590f407f836163a84eff66/images/6d20911aeb0d64ec26e1f63ef2e6c3f28ed8ae8265617e28.png&quot;,&quot;about_this_result&quot;:{&quot;source&quot;:{&quot;description&quot;:&quot;travelguide.de was first indexed by
Thought:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ChatGPT responds with a final answer. The train sees the magic &lt;code class=&quot;highlighter-rouge&quot;&gt;Final Answer:&lt;/code&gt; text and returns the result:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Based on the AnswerBox response, the timezone America/Denver belongs to the United States.
Final Answer: The timezone belonging to the user derek@glass.io belongs to the United States.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;

&lt;p&gt;Just like how an LLM can generate text for a blog post (not this one though!), it can can also generate a plan to answer a question that requires using external tools. The most popular framework for this is ReAct (Reason + Act), which we can use in Ruby via the &lt;a href=&quot;github.com/boxcarsAI/boxcars&quot;&gt;Boxcars gem&lt;/a&gt;. Boxcars handles generating the ZeroShot ReAct prompt, parsing the model completions for actions, and running those actions.&lt;/p&gt;</content><author><name></name></author><summary type="html"></summary></entry><entry><title type="html">Using Boxcars - the lightweight Ruby Langchain alternative - to query a Rails DB with natural language</title><link href="https://dlite.cc/2023/05/06/2023-boxcars-ruby-intro.html" rel="alternate" type="text/html" title="Using Boxcars - the lightweight Ruby Langchain alternative - to query a Rails DB with natural language" /><published>2023-05-06T11:00:00+00:00</published><updated>2023-05-06T11:00:00+00:00</updated><id>https://dlite.cc/2023/05/06/2023-boxcars-ruby-intro</id><content type="html" xml:base="https://dlite.cc/2023/05/06/2023-boxcars-ruby-intro.html">&lt;p&gt;You may have heard of &lt;a href=&quot;https://github.com/hwchase17/langchain&quot;&gt;Langchain&lt;/a&gt;, the Python library for creating LLM-powered apps with nearly 35k GitHub stars. Despite the large following, &lt;a href=&quot;https://news.ycombinator.com/item?id=35820931&quot;&gt;Langchain can be difficult to use&lt;/a&gt; when you want to go deeper than “hello world” tutorials. This experience (and my background as a Rubyist) led me to &lt;a href=&quot;https://github.com/BoxcarsAI/boxcars&quot;&gt;Boxcars&lt;/a&gt;, a Langchain-inspired Ruby gem but with fewer abstactions.&lt;/p&gt;

&lt;p&gt;You might be asking: why venture outside the Python ML ecosystem? Well, building apps with LLMs doesn’t require Ruby equivalents for NumPy, SciPy and Pandas. Rather than working with numbers, I find that most of my time is spent manipulating string templates and interacting with outside systems (like realtime search). The readability of Ruby is great for this use case.&lt;/p&gt;

&lt;p&gt;In this post, I’ll use Boxcar to query my Rails database using natural language. I’ll take a look at how Boxcars creates ChatGPT prompts, handles errors, and how it compares to Langchain’s &lt;code class=&quot;highlighter-rouge&quot;&gt;SQLDatabaseChain&lt;/code&gt; to solve the same problem.&lt;/p&gt;

&lt;h2 id=&quot;querying-activerecord-with-natural-language&quot;&gt;Querying ActiveRecord with natural language&lt;/h2&gt;

&lt;p&gt;The Ruby ecosystem is Rails-centric, so it’s great to see that Boxcars plays well with Rails apps out of the box. Just add the &lt;code class=&quot;highlighter-rouge&quot;&gt;boxcars&lt;/code&gt; gem to your &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt;, set the &lt;code class=&quot;highlighter-rouge&quot;&gt;OPENAI_ACCESS_TOKEN&lt;/code&gt; to your OpenAI API key, and you can start querying your database with natural language inside &lt;code class=&quot;highlighter-rouge&quot;&gt;rails console&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;boxcar&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Boxcars&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;boxcar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;How many users?&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:&quot;ok&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;answer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;33&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;explanation&quot;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:&quot;Answer: 33&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:&quot;User.count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;33&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You may have used ChatGPT to generate code for you, but this goes a step beyond: it executes the code!&lt;/p&gt;

&lt;p&gt;To see how the magic happens, I’ll call &lt;code class=&quot;highlighter-rouge&quot;&gt;Boxcars.configuration.log_prompts = true&lt;/code&gt; and re-run the above code to inspect the generated ActiveRecord prompt:&lt;/p&gt;

&lt;pre&gt;
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: system &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;                                                   
You are a Ruby on Rails Active Record code generator                         
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: system &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;                                                   
Given an input question, first create a syntactically correct Rails Active Record code to run, then look at the results of the code and return the answer. Unless the user specifies in her question a specific number of examples she wishes to obtain, limit your code to at most 5 results.
Never query for all the columns from a specific model, only ask for the relevant attributes given the question.
Also, pay attention to which attribute is in which model.                    
                                                                             
Use the following format:
Question: $
ARChanges: $ - Only add this line if the ARCode on the next line will make data changes.
ARCode: $ - make sure you use valid code
Answer: $

Only use the following Active Record models: []
Pay attention to use only the attribute names that you can see in the model description.
Do not make up variable or attribute names, and do not share variables between the code in ARChanges and ARCode
Be careful to not query for attributes that do not exist, and to use the format specified above.
Finally, try not to use print or puts in your code
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: user &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
Question: How many users?
&lt;/pre&gt;

&lt;p&gt;The model responds with:&lt;/p&gt;

&lt;pre&gt;
ARCode: User.count
&lt;/pre&gt;

&lt;p&gt;If you copy and paste the prompt above into ChatGPT you should see a very similar response.&lt;/p&gt;

&lt;h2 id=&quot;how-does-the-activerecord-boxcar-execute-the-query&quot;&gt;How does the ActiveRecord Boxcar execute the query?&lt;/h2&gt;

&lt;p&gt;The ActiveRecord Boxcar checks to see if &lt;code class=&quot;highlighter-rouge&quot;&gt;ARCode&lt;/code&gt; is in the response. If it is (and after some security checks) it executes the code returning the result of the ActiveRecord query.&lt;/p&gt;

&lt;h2 id=&quot;what-about-adjusting-queries-if-the-first-attempt-is-malformed&quot;&gt;What about adjusting queries if the first attempt is malformed?&lt;/h2&gt;

&lt;p&gt;Let’s say I ask Boxcar to run a query for an ActiveRecord model that does not exist. What does it? Does it immediately exit, attempt to fix the issue, or just raise an exception?&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;boxcar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;how many ClassDoesNotExist records were created this year?&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ChatGPT responds with a valid-looking query:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;ARCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`ClassDoesNotExist.where(&quot;created_at &amp;gt;= ?&quot;, Time.zone.now.beginning_of_year).count`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The Boxcar runs the query and captures the exception:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Error&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;running&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;code: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uninitialized&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;constant&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Boxcars&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ClassDoesNotExi&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It will then re-try up to 3 additional times. Notice how Boxcar appends (1) the code that was excecuted (2) the error that resulted from running the query:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;...
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: user &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
Question: how many ClassDoesNotExist records  were created this year?
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: assistant &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
ARCode: `ClassDoesNotExist.where(&quot;created_at &amp;gt;= ?&quot;, Time.zone.now.beginning_of_year).count`
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: user &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
ARCode Error: uninitialized constant Boxcars::ActiveRecord::ClassDoesNotExist - please fix &quot;ARCode:&quot; to not have this error
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ChatGPT then returns a response, but it lacks the &lt;code class=&quot;highlighter-rouge&quot;&gt;ARCode&lt;/code&gt; section:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;I apologize for that. It seems like there is no `ClassDoesNotExist` model in the list of available models. Please let me know which model you would like to use instead.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The boxcar appends this error to the prompt and re-runs:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: assistant &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
I apologize for that. It seems like there is no `ClassDoesNotExist` model in the list of available models. Please let me know which model you would like to use instead.
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; Role: user &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;
Your answer wasn't formatted properly - try again. I expected your answer to start with &quot;ARChanges:&quot; or &quot;ARCode:&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ChatGPT attempts to help, but we’re not going to get anywhere:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;I apologize for the mistake. Here is the correct format:

ARCode: `ModelName.where(&quot;created_at &amp;gt;= ?&quot;, Time.zone.now.beginning_of_year).count`

Please replace `ModelName` with the name of the model you would like to use.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is smart usage of updating the ChatGPT prompt with additional context around errors.&lt;/p&gt;

&lt;h2 id=&quot;how-does-it-do-with-complex-queries&quot;&gt;How does it do with complex queries?&lt;/h2&gt;

&lt;p&gt;At first, it struggled in my development environment and I almost wrote it off as another ML “hello world” demo that quickly fails when you try to take it farther. Then I realized that the default list of models and their attributes in the prompt is likely to be empty (or contain just a small number of columns) &lt;a href=&quot;https://stackoverflow.com/questions/516579/is-there-a-way-to-get-a-collection-of-all-the-models-in-your-rails-app&quot;&gt;per this SO question&lt;/a&gt;. After running &lt;code class=&quot;highlighter-rouge&quot;&gt;Rails.application.eager_load!&lt;/code&gt; and re-initializing my boxcar I was very impressed. It correctly executed queries like these:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Which org has the most users?&lt;/li&gt;
  &lt;li&gt;Order the orgs by the number of users in each org and show the orgs with the most users. list the org id, name, and number of users.&lt;/li&gt;
  &lt;li&gt;How many users were created by month?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;how-does-the-activerecord-boxcar-compare-to-langchains-sql-toolkit&quot;&gt;How does the ActiveRecord Boxcar compare to Langchain’s SQL Toolkit?&lt;/h2&gt;

&lt;p&gt;This is a very small sample size, but the ActiveRecord Boxcar provided more accurate results for me than the Langchain’s &lt;code class=&quot;highlighter-rouge&quot;&gt;SQLDatabaseChain&lt;/code&gt;. For example, it returned a result when I provided an invalid table name and returned an incorrect value in query 2 above due to a missing join.&lt;/p&gt;

&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;

&lt;p&gt;If you are a Rubyist, don’t let Langchain’s large following sway you away from trying Boxcars when creating an LLM application. If you’re like me, you’ll enjoy the smaller footprint, fewer abstractions, and a faster timeline to production usage (assuming you already have a deployed Rails app) that Boxcars offers.&lt;/p&gt;</content><author><name></name></author><summary type="html">You may have heard of Langchain, the Python library for creating LLM-powered apps with nearly 35k GitHub stars. Despite the large following, Langchain can be difficult to use when you want to go deeper than “hello world” tutorials. This experience (and my background as a Rubyist) led me to Boxcars, a Langchain-inspired Ruby gem but with fewer abstactions.</summary></entry><entry><title type="html">Drains, sprinklers, and sidewalk edges: behind the development of Greenzie’s first ML Model</title><link href="https://dlite.cc/2022/05/22/2022-image-segmentation-at-greenzie.html" rel="alternate" type="text/html" title="Drains, sprinklers, and sidewalk edges: behind the development of Greenzie's first ML Model" /><published>2022-05-22T11:00:00+00:00</published><updated>2022-05-22T11:00:00+00:00</updated><id>https://dlite.cc/2022/05/22/2022-image-segmentation-at-greenzie</id><content type="html" xml:base="https://dlite.cc/2022/05/22/2022-image-segmentation-at-greenzie.html">&lt;p&gt;&lt;em&gt;Originally published on the Greenzie blog, this covers a project I led to deploy an image segmentation model on autonomous commercial lawnmowers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A never-ending problem for mobile robotics is funneling a petabyte-dense visual world into just enough megabytes to help the robot act correctly in realtime. One example of this for autonomous mowing: identifying and navigating around small obstacles like sprinklers that could be damaged (or damage the mower).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our team at Greenzie decided that the best way to identify small obstacles was to develop a custom image segmentation model.&lt;/strong&gt; Here’s a look at how we’re developing and deploying this model to our fleet of autonomous lawnmowers.&lt;/p&gt;

&lt;h2 id=&quot;the-problem&quot;&gt;The problem&lt;/h2&gt;

&lt;p&gt;Greenzie autonomous lawnmowers use a set of stereo cameras to generate a 3D point cloud of its surroundings. We group points into clusters to identify obstacles that the robot needs to avoid. However, it’s not easy identifying the classification of an object from a point cloud. For example, below is a side-by-side display of the depth cloud output and the image display from a stereo camera (&lt;a href=&quot;https://www.calvert.ch/maurice/2018/06/12/intel-realsense-d435-review/&quot;&gt;source&lt;/a&gt;):&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/greenzie_ml_image_seg/flower_pot.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It’s not possible to identify the object in the depth display as a flower. It’s easy using the photo. In fact, &lt;a href=&quot;https://www.google.com/search?q=computer+vision+model+flowers&quot;&gt;off-the-shelf computer vision models&lt;/a&gt; can identify these as flowers. A point cloud cannot.&lt;/p&gt;

&lt;p&gt;So, if Greenzie can already navigate around obstacles, why do we need an additional layer in our perception stack? Maybe we’re just trying to add “AI-powered™” stickers to our robots? Well, many obstacles are aligned close enough to the ground plane that they can be difficult to identify from a point cloud. Some of these objects could be damaged if the mower were to travel over them with the blades on. Some could damage the mower. For example, the objects in the image below can be difficult to detect in a 3D point cloud:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/greenzie_ml_image_seg/small_objects.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Adding the ability for our robots to sense additional objects via an image segmentation model gives us two big wins:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Avoid an additional class of collision events (impacts with small obstacles).&lt;/li&gt;
  &lt;li&gt;Increase the ROI our customers experience by increasing our robot’s confidence to navigate near small obstacles, reducing manual cleanup work.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;deciding-on-the-ml-model-type&quot;&gt;Deciding on the ML model type&lt;/h2&gt;

&lt;p&gt;In the introduction, I quickly jumped to our decision to create an image segmentation model. Let’s take a quick look at the 3 types of models we considered.&lt;/p&gt;

&lt;h2 id=&quot;1-object-detection&quot;&gt;1. Object detection&lt;/h2&gt;

&lt;p&gt;This approach generates a bounding box around identified objects. For example, we could train a model to identify obstacles and their classification much like how this model identifies dogs, bikes, and other objects:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/greenzie_ml_image_seg/object_detection.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://towardsdatascience.com/deep-learning-method-for-object-detection-r-cnn-explained-ecdadd751d22&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;2-image-segmentation&quot;&gt;2. Image segmentation&lt;/h2&gt;

&lt;p&gt;This classifies the category of items at the pixel-level rather than using a bounding box. For example, here’s an image segmentation result showing the sky, grass, and not-grass:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/greenzie_ml_image_seg/image_segmentation.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;3-instance-segmentation&quot;&gt;3. Instance segmentation&lt;/h2&gt;

&lt;p&gt;This combines both of the above approaches, allowing the model to identify individual instances of items in an image at the pixel-level. For example, you could count the number of mulch beds.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/greenzie_ml_image_seg/instance_segmentation.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.cs.princeton.edu/courses/archive/spring18/cos598B/public/outline/Instance%20Segmentation.pdf&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;why-did-we-go-with-image-segmentation&quot;&gt;Why did we go with image segmentation?&lt;/h2&gt;

&lt;p&gt;We would like precise contours of “blades on”, “obstacle”, and “sky” regions (rules out object detection). We don’t need to identify individual instances of objects that an instance segmentation model offers.&lt;/p&gt;

&lt;p&gt;With the problem defined and the ML model type chosen, development was ready to begin.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/greenzie_ml_image_seg/ml_timeline.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;step-1-pick-the-deployment-platform-luxonis-oak-d-camera&quot;&gt;Step 1: pick the deployment platform (Luxonis OAK-D camera)&lt;/h2&gt;

&lt;p&gt;Our current platform uses a set of Intel Realsense cameras to capture images and depth data. However, we were concerned about the resource usage of running ML models on the host. We just happened to have a convenient mounting location for a &lt;a href=&quot;https://shop.luxonis.com/products/oak-d&quot;&gt;Luxonis OAK-D camera&lt;/a&gt;. We can deploy the model to the OAK-D, keeping resources open on the computer for our other robotic work.&lt;/p&gt;

&lt;h2 id=&quot;step-2-create-a-training-dataset-for-the-image-segmentation-model&quot;&gt;Step 2: create a training dataset for the image segmentation model&lt;/h2&gt;

&lt;p&gt;We used an iPhone to photograph areas similar to what our robotic workers mow. This included extensive photos of obstacles that could be within the mowing map and not seen by our depth cloud obstacle logic. These photos were sent to an image annotation service.&lt;/p&gt;

&lt;p&gt;There’s no getting around it: assembling and reviewing annotation results is a tedious process. I felt more like a bookkeeper than a developer. That said, it was a valuable introspective process that showed where our annotation guidance was not clear. For example:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Do you label every visible piece of grass visible behind a chainlink fence as “blades on”? Or the entire fence itself as an obstacle?&lt;/li&gt;
  &lt;li&gt;Do you label grass visible behind a set of tree branches as “blades on”?&lt;/li&gt;
  &lt;li&gt;What about small dirt patches within a grass area?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s look at an example of annotation instructions gone wrong. Our initial instructions to the annotation service asked them to classify images into three classes: sky, obstacles, and grass. Look at some of these initial model results in photos from the field:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/greenzie_ml_image_seg/dirt.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In the above model inference results, the overlays show how worn-down, high-traffic areas of a soccer field are classified as “not grass”. This is technically correct, but would be annoying when operating the mower. The mower will stop and/or navigate around these areas. It’s OK to have blades on over these areas.&lt;/p&gt;

&lt;p&gt;Based on this experience, we updated our instructions changing the “grass” classification to “blades on”. The classification “blades on” now includes dirt patches.&lt;/p&gt;

&lt;h2 id=&quot;step-3-deploy-the-depthai-robotics-code&quot;&gt;Step 3: deploy the DepthAI Robotics Code&lt;/h2&gt;

&lt;p&gt;Greenzie’s robotic lawnmowers use the &lt;a href=&quot;https://www.ros.org/&quot;&gt;Robot Operating System&lt;/a&gt; (ROS). We worked with the Luxonis team to integrate the sensor data from the OAK-D camera into our system via their &lt;a href=&quot;https://github.com/luxonis/depthai-ros&quot;&gt;ROS DepthAI package&lt;/a&gt;. To start, we deployed an off-the shelf model (TinyYOLO4) to verify the end-to-end functionality.&lt;/p&gt;

&lt;h2 id=&quot;step-4-training-a-baseline-image-segmentation-model&quot;&gt;Step 4: training a baseline image segmentation model&lt;/h2&gt;

&lt;p&gt;We again worked with Luxonis to develop the image segmentation model. Their team selected images for the training dataset, sent the images off for annotation, and trained the first several versions of the model. Our team focused on building a reproducible training environment on AWS Deep Learning Images once Luxonis completed a couple ad-hoc training sessions.&lt;/p&gt;

&lt;h2 id=&quot;step-5-basic-image-sampling-from-the-field&quot;&gt;Step 5: basic image sampling from the field&lt;/h2&gt;

&lt;p&gt;Our training dataset was collected via an iPhone, not image data from robots in the field. This is convenient (and there isn’t a lot of mowing in the winter), but it was a risk. It’s not the same environment.&lt;/p&gt;

&lt;p&gt;We developed sampling logic that saves an image from the OAK-D camera once-per minute. These are sent to the cloud where we can perform later analysis of the results.&lt;/p&gt;

&lt;h2 id=&quot;step-6-monitoring-ml-inference-results&quot;&gt;Step 6: monitoring ML inference results&lt;/h2&gt;

&lt;p&gt;With image sampling in place, we developed a pipeline to transfer the images to S3 and generate side-by-side comparisons of the robot image with the model inference results. This identifies areas where the model was confused. For example, notice that the model struggled with identifying grass in blurred regions:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/greenzie_ml_image_seg/blur.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Now that mowing season is in-full swing, the size of our monitoring dataset will increase by several orders of magnitude and comes from the source-of-truth: the production sensors, running real customer mowing jobs.&lt;/p&gt;

&lt;h2 id=&quot;step-7-the-refinement-cycle&quot;&gt;Step 7: the refinement cycle&lt;/h2&gt;

&lt;p&gt;With monitoring of production ML inference in place, we’ll move faster refining the model to meet the criteria for acting on the results. This will help us address the rough edges faster. We’ll be cycling through previous steps: evaluating real-world inference results, augmenting our training dataset with new scenarios, and retraining the model.&lt;/p&gt;

&lt;h2 id=&quot;step-8-collecting-data-on-false-positives-and-false-negatives&quot;&gt;Step 8: collecting data on false positives and false negatives&lt;/h2&gt;

&lt;p&gt;Once per-minute sampling is great, but it comes at a low signal to noise ratio. The vast majority of the time there are no obstacles in the mowing area. To collect more fine-grained data, we implemented the following in our ROS codebase:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Collect false positives&lt;/strong&gt; - save an image when the robot travels over an area that the ML model believes is an obstacle.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Collect false negatives&lt;/strong&gt; - save an image when our depth-cloud logic identifies an obstacle but the ML model does not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We’re at the beginning of this more nuanced approach to gathering data.&lt;/p&gt;

&lt;h2 id=&quot;step-9-setting-a-threshold-for-enabling-the-model&quot;&gt;Step 9: setting a threshold for enabling the model&lt;/h2&gt;

&lt;p&gt;It’s important to define “good enough” model performance or the cycle of retraining would never end. The model won’t be perfect at identifying small obstacles, but neither is a human operator. We’ve established the following criteria prior to enable the robot to act on the model inference results:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Fewer than 1 false positive per-hour of autonomous mowing&lt;/strong&gt; - a false positive results in frustrating scenarios for operators: missed patches if the mower navigates around an obstacle or a full-stop if the mower is unable to find a path around an obstacle.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;70% true positive rate&lt;/strong&gt; - wait a minute…this means the robot will collide with 3 of every 10 obstacles! How is that acceptable? Well, we’re phasing in the robot’s actions. Initially, the ML model results only augment what we’re already doing via the point cloud-based obstacle logic. Many of these low-lying objects won’t be seen by the point cloud logic, so this means we’re significantly reducing collisions at this threshold.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once these are met, we’ll set new metrics (or just update these thresholds) for enabling the mower to interact more precisely around the boundary areas between “blades on” and “obstacles” (such as mowing more precisely around a mulch bed).&lt;/p&gt;

&lt;h2 id=&quot;whats-next&quot;&gt;What’s next?&lt;/h2&gt;

&lt;p&gt;We’re beginning the final push: that last 10% of refinement that will take a bit of time to fully enable in production. These are possible areas we could explore next:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;A more efficient model refinement flow&lt;/strong&gt; - collecting images, noting poor classification, sending off new images for labeling, and re-training is a tedious process. We’ll look at ways to make this more enjoyable.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Exploring new areas&lt;/strong&gt; - we are impressed with how well the model performed with our baseline training images. This gives us a lot of hope for other perception-related problems that could be solved with ML.&lt;/li&gt;
&lt;/ol&gt;</content><author><name></name></author><summary type="html">Originally published on the Greenzie blog, this covers a project I led to deploy an image segmentation model on autonomous commercial lawnmowers.</summary></entry><entry><title type="html">Effortlessly Investigating Robot Anomalies at Greenzie</title><link href="https://dlite.cc/2022/02/25/2022-robot-anomalies.html" rel="alternate" type="text/html" title="Effortlessly Investigating Robot Anomalies at Greenzie" /><published>2022-02-25T11:00:00+00:00</published><updated>2022-02-25T11:00:00+00:00</updated><id>https://dlite.cc/2022/02/25/2022-robot-anomalies</id><content type="html" xml:base="https://dlite.cc/2022/02/25/2022-robot-anomalies.html">&lt;p&gt;&lt;em&gt;Originally published on the Greenzie blog, this covers a project I led to make it easier to debug problems on a fleet of autonomous commercial lawnmowers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The life of a &lt;a href=&quot;http://greenzie.com/product&quot;&gt;Greenzie-equipped autonomous mower&lt;/a&gt; is 97% boring punctuated by small anomalies. This presents two debugging challenges for our ROS developers:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;There’s little need for rich data the vast majority of time, but when there is an anomaly, there’s a thirst for ALL the data.&lt;/li&gt;
  &lt;li&gt;Our mobile fleet of robotic workers are customer-operated (not managed by robotics technicians), do not dock to a fast Internet connection, and are frequently turned on and off. Getting this data from a robot to a developer’s laptop is a challenge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Easy access to debugging data is important to us as we’re big believers in &lt;a href=&quot;https://en.wikipedia.org/wiki/Kaizen&quot;&gt;Kaizen&lt;/a&gt;. It should be easy for developers to continually improve our software, and a big part of that is making it effortless to view debugging data. We’ve recently rolled out an update to our developer tooling that makes obtaining and viewing high-fidelity ROS data silky-smooth. Let’s see how it works.&lt;/p&gt;

&lt;h2 id=&quot;end-user-experience-overview&quot;&gt;End-user experience overview&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/greenzie_rosbag/foxglove_screens.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The end user of our anomaly data fetch system is either a ROS developer or a support engineer. Here’s how the system is typically used:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;A support engineer reviews a robot job and notices an anomaly. For example: the robot generated an obstacle alert. Robots post alerts to our platform and these are rendered as markers on a satellite map. The user clicks the alert marker for details and the “Get ROS Bag” button.&lt;/li&gt;
  &lt;li&gt;A new &lt;a href=&quot;https://foxglove.dev&quot;&gt;Foxglove&lt;/a&gt; marker is added to the map along with the path of the robot over the duration of the ROS bag file.&lt;/li&gt;
  &lt;li&gt;The ROS bag data is uploaded to the cloud via rsync, then uploaded to &lt;a href=&quot;https://foxglove.dev/data-platform&quot;&gt;Foxglove’s data platform&lt;/a&gt;. The end user is notified via email when the upload is complete. The user can click on a link from the marker popup to view the data on Foxglove.&lt;/li&gt;
  &lt;li&gt;The user views the data with &lt;a href=&quot;https://foxglove.dev/studio&quot;&gt;Foxglove Studio&lt;/a&gt;. Foxglove Studio can be used in the browser or via their desktop apps. The ROS stack does need to be installed on the user’s computer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;‍&lt;/p&gt;

&lt;h2 id=&quot;technical-details&quot;&gt;Technical Details&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/greenzie_rosbag/foxglove_diagram.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Delivering the end user experience above involves a couple pieces of custom work and one of our favorite new robotics developer tools, &lt;a href=&quot;https://foxglove.dev&quot;&gt;Foxglove&lt;/a&gt;. Here’s how it works:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Our custom admin web app running in the cloud creates a database record with the robot identifier, timestamp, and duration of the data fetch.&lt;/li&gt;
  &lt;li&gt;The app checks every minute if the robot is online. If so, any unsent requests for ROS data are sent to the robot.&lt;/li&gt;
  &lt;li&gt;The robot marks ROS bag files that occurred during the requested timeframe for transfer to the cloud. Bag files are split in 30 second increments. An rsync transfer is started via Cron to send the requested ROS bags to the cloud. Cron is used so that partially transferred files will resume upload automatically when the robot is power cycled.&lt;/li&gt;
  &lt;li&gt;Back in the cloud, the app monitors for completed rsync file transfers. Once completed, files are uploaded to the Foxglove Data Platform via their API. The user that requested the ROS bag data is notified via email of the completed data request. They can then immediately view the data within Foxglove Studio.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Some notes on the technical details:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Keep on-robot logic simple&lt;/strong&gt; - we try to limit the complexity of utilities that run on our robots in the field. The greatest complexity of this flow resides in the cloud. In the cloud, we can deploy fixes in minutes and easily debug issues (always-on fast internet, always-on servers, no disk space concerns, and a mature ecosystem of monitoring and debugging tools).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Limit resource usage on the robot&lt;/strong&gt; - there are few cases where the ROS data is needed to debug an immediate problem. Instead, we use the data from these anomalies to prevent future problems. We limit the resource usage of the on-robot parts of this flow in several ways: flock to prevent a thundering heard of rsync processes, limits on rsync transfer size, deriving the ROS bags to collect based on the last modified time of the file and not loading and inspecting files, etc.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://foxglove.dev/&quot;&gt;&lt;strong&gt;Foxglove&lt;/strong&gt;&lt;/a&gt; - this SaaS fulfills two important needs: (1) storage of ROS bag data (2) effortless viewing of ROS bag data (no ROS stack required). I’m very excited about their vision to continue improving both of these areas.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;potential-for-automated-anomaly-data-collection&quot;&gt;Potential for automated anomaly data collection&lt;/h2&gt;

&lt;p&gt;A developer must manually trigger the ROS data fetch: we’ll always need this ability. However, I’m excited that this flow for fetching data can work in an automated fashion too: these are just basic HTTP APIs. For example:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Anomaly detection in the cloud&lt;/strong&gt; - we send a limited amount of data to the cloud every minute. This could be analyzed with an anomaly detection algorithm, automatically triggering a data fetch on an anomaly event.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Anomaly detection on the robot&lt;/strong&gt; - it’s also easy for a ROS Node to trigger the API if it notices an anomaly. For example:  trigger a data fetch if a computer vision ML Model appears to be confused.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;

&lt;p&gt;We’re excited to have a lean, “right-sized” stack for easily investigating incidents from the field on our robots. Web developers have leveraged these kinds of easy-to-use tools for years (ex: Sentry, Scout, DataDog, etc). It’s great to see more of these as mobile robots move beyond prototypes to the real world.&lt;/p&gt;</content><author><name></name></author><summary type="html">Originally published on the Greenzie blog, this covers a project I led to make it easier to debug problems on a fleet of autonomous commercial lawnmowers.</summary></entry><entry><title type="html">html-sketchapp: under-the-hood of an HTML to Sketch export solution</title><link href="https://dlite.cc/2020/11/15/html-sketchapp.html" rel="alternate" type="text/html" title="html-sketchapp: under-the-hood of an HTML to Sketch export solution" /><published>2020-11-15T11:00:00+00:00</published><updated>2020-11-15T11:00:00+00:00</updated><id>https://dlite.cc/2020/11/15/html-sketchapp</id><content type="html" xml:base="https://dlite.cc/2020/11/15/html-sketchapp.html">&lt;p&gt;&lt;img src=&quot;/img/posts/html-sketchapp/screenshot.gif&quot; alt=&quot;screenshot&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;small text-muted&quot;&gt;Taking the output of html-sketchapp and importing into Sketch.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Designers and developers continue to work in entirely different mediums. As a result, without constant, manual effort to keep them in sync, our code and design assets are constantly drifting further and further apart.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p class=&quot;small text-muted&quot;&gt;Mark Dalgleish in &lt;i&gt;&lt;a href=&quot;https://medium.com/seek-blog/sketching-in-the-browser-33a7b7aa0526&quot;&gt;Sketching in the Browser&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;

&lt;p&gt;Even-though the dawn of &lt;a href=&quot;https://en.wikipedia.org/wiki/Web_2.0&quot;&gt;Web 2.0&lt;/a&gt; 15 years ago, we still can’t auto-translate HTML to our design tools. A huge number of improvements to to the product development flow have made the lives of designers and devs easier, but the common task of going from code to design (and back) remains manual.&lt;/p&gt;

&lt;p&gt;One NodeJS package that makes it easier to keep your coded components in sync with your design team is the excellent &lt;a href=&quot;https://github.com/html-sketchapp/html-sketchapp&quot;&gt;html-sketchapp&lt;/a&gt;. Released in 2018 and inspired by the more limited &lt;a href=&quot;https://github.com/airbnb/react-sketchapp&quot;&gt;react-sketchapp&lt;/a&gt;, html-sketchapp lets you export HTML to Sketch (with some &lt;a href=&quot;https://github.com/html-sketchapp/html-sketchapp/wiki/What's-supported%3F&quot;&gt;limitations&lt;/a&gt;). In this post, I’ll show how html-sketchapp works and how it can benefit DesignOps at your organization.&lt;/p&gt;

&lt;h2 id=&quot;is-html-sketchapp-for-designers&quot;&gt;Is html-sketchapp for designers?&lt;/h2&gt;

&lt;p&gt;Despite the designer being the end-user of an HTML to Sketch solution, html-sketchapp is really a tool for developers. html-sketchapp provides the export engine and requires another tool to provide the user interface. I think it’s unlikely a designer will get much value out of downloading the html-sketchapp source code. If you are designer, I suggest you checkout one of the following for a more ready-to-go solution:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/seek-oss/html-sketchapp-cli&quot;&gt;html-sketchapp-cli&lt;/a&gt; - A tool that allows you to export an HTML doc to Sketch from the command line. Distributed as a NodeJS package.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/KimDal-hyeong/html-to-sketch-electron&quot;&gt;html-to-sketch-electron&lt;/a&gt; - An electron app that allows you to provides a graphical interface versus the command-line tool option provided by html-sketchapp-cli.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/html-sketchapp/html-to-sketch-electron.gif&quot; alt=&quot;html-to-sketch-electron&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;small text-muted&quot;&gt;html-to-sketch-electron in action.&lt;/p&gt;

&lt;p&gt;Note that both of these utilities don’t have a lot of recent commits (and I haven’t tested them). This post focuses more on how developers can use html-sketchapp within their own internal tools to help push coded components to design tools.&lt;/p&gt;

&lt;h2 id=&quot;how-does-a-developer-leverage-html-sketchapp&quot;&gt;How does a developer leverage html-sketchapp?&lt;/h2&gt;

&lt;p&gt;If you are a developer creating a tool that uses html-sketchapp, your flow might look a bit like this:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Install the NodeJS package within your project: &lt;code class=&quot;highlighter-rouge&quot;&gt;npm i @brainly/html-sketchapp&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Use &lt;a href=&quot;https://www.npmjs.com/package/puppeteer&quot;&gt;puppeteer&lt;/a&gt; to launch a headless browser session opening a URL of your choice.&lt;/li&gt;
  &lt;li&gt;Feed the loaded &lt;code class=&quot;highlighter-rouge&quot;&gt;document.body&lt;/code&gt; to html-sketchapp’s &lt;code class=&quot;highlighter-rouge&quot;&gt;nodeTreeToSketchPage&lt;/code&gt; function.&lt;/li&gt;
  &lt;li&gt;Save the output file with an &lt;code class=&quot;highlighter-rouge&quot;&gt;*.asketch.json&lt;/code&gt; extension.&lt;/li&gt;
  &lt;li&gt;Within Sketch, install the &lt;a href=&quot;https://github.com/html-sketchapp/html-sketchapp#import-asketch-files-to-sketch&quot;&gt;Almost &lt;em&gt;Sketch&lt;/em&gt; to Sketch Plugin&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Use the Almost &lt;em&gt;Sketch&lt;/em&gt; to Sketch Plugin to import the &lt;code class=&quot;highlighter-rouge&quot;&gt;*.asketch.json&lt;/code&gt; file and create a new Sketch document of the HTML export.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;See &lt;a href=&quot;https://github.com/html-sketchapp/html-sketchapp-example/blob/master/src/inject.js&quot;&gt;html-sketchapp-example&lt;/a&gt; for a complete example of steps 1-4.&lt;/p&gt;

&lt;h2 id=&quot;why-cant-html-sketchapp-just-export-a-sketch-file&quot;&gt;Why can’t html-sketchapp just export a Sketch file?&lt;/h2&gt;

&lt;p&gt;You may have noticed that in the steps above, we’re able to export a JSON file from html-sketchapp but not an actual Sketch file. We take that file and use a Sketch plugin to take the export over the finish line, converting JSON to the final Sketch file format.&lt;/p&gt;

&lt;p&gt;Why is there an extra moving part? At the time html-sketchapp was written, some parts of the Sketch file format stored data as a binary blob (like text styling information). This is not easy to generate from Javascript. Additionally, as the html-sketchapp functions run in the browser, it is limited by &lt;a href=&quot;https://en.wikipedia.org/wiki/Cross-origin_resource_sharing&quot;&gt;CORS&lt;/a&gt; and may not be able to access all images on an HTML page. While text information is no longer &lt;a href=&quot;https://github.com/html-sketchapp/html-sketchapp/issues/99&quot;&gt;stored as a binary blob&lt;/a&gt;, sadly a Mac is still required in order to use &lt;a href=&quot;https://github.com/html-sketchapp/html-sketchapp/issues/99#issuecomment-695957754&quot;&gt;NSAttributedString&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, the almost-sketch file format remains as cocoascript is still required for the final conversion step. This final step is a bit of a pain: most recently, an issue with an Almost &lt;em&gt;Sketch&lt;/em&gt; to Sketch Plugin dependency &lt;a href=&quot;https://github.com/html-sketchapp/html-sketchapp/issues/196#issuecomment-696052103&quot;&gt;broke exports&lt;/a&gt;. That said, it doesn’t appear there’s anything html-sketchapp can do about the plugin requirement given the need to call &lt;code class=&quot;highlighter-rouge&quot;&gt;NSAttributedString&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;how-does-html-sketchapp-generate-the-asketchjson-file&quot;&gt;How does html-sketchapp generate the *.asketch.json file?&lt;/h2&gt;

&lt;p&gt;When &lt;code class=&quot;highlighter-rouge&quot;&gt;nodeTreeToSketchPage&lt;/code&gt; is called, it creates a Sketch group representation of the node and its child nodes. This group is added to a Sketch page with a width and height set to the same dimensions as the root node.&lt;/p&gt;

&lt;p&gt;The meat of the HTML to Sketch translation is in the &lt;code class=&quot;highlighter-rouge&quot;&gt;nodeToSketchLayers&lt;/code&gt; function. This function is responsible for taking the style properties of an HTML element and mapping those to Sketch styles. The flow works a bit like this:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Call &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle&quot;&gt;Window.getComputedStyle()&lt;/a&gt; to get an object that contains all of the CSS properties of the HTML element. Remember, html-sketchapp is run within a browser session so we’re able to call functions within the Javascript &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API&quot;&gt;Web API&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Run a series of checks to determine if any layers should be created for this HTML node:
    &lt;ul&gt;
      &lt;li&gt;Is the HTML node a descendent of a parent &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/SVGElement&quot;&gt;SVGElement&lt;/a&gt;? If so, don’t create any layers (more on this later).&lt;/li&gt;
      &lt;li&gt;Is the node is visible? It’s actually pretty complex to detect this and the logic for determining visibility is in the &lt;code class=&quot;highlighter-rouge&quot;&gt;isNodeVisible&lt;/code&gt; function. If the node isn’t visible don’t create any layers.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Create a rectangle &lt;a href=&quot;https://www.sketch.com/docs/shapes/&quot;&gt;Sketch shape&lt;/a&gt; to represent this HTML node.&lt;/li&gt;
  &lt;li&gt;If the node is an image:
    &lt;ul&gt;
      &lt;li&gt;Set the shape background color to the HTML node’s background color&lt;/li&gt;
      &lt;li&gt;If the node is an HTML &lt;code class=&quot;highlighter-rouge&quot;&gt;IMG&lt;/code&gt; element, apply an &lt;a href=&quot;https://www.sketch.com/docs/styling/#how-to-add-an-image-fill&quot;&gt;image fill&lt;/a&gt; using the url of the image. This image will need to be downloaded and can’t be loaded from the url dynamically (more later).&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;If the node has a &lt;code class=&quot;highlighter-rouge&quot;&gt;box-shadow&lt;/code&gt; generate appropriate Sketch inner and outer shadows.&lt;/li&gt;
  &lt;li&gt;If the HTML node has borders, apply these using Sketch inner shadows as Sketch does not support side-specific borders.&lt;/li&gt;
  &lt;li&gt;Apply the opacity.&lt;/li&gt;
  &lt;li&gt;Create a new Sketch Rectangle shape, applying the HTML node border-radius to each corner. Note that only % values are supported.&lt;/li&gt;
  &lt;li&gt;If the HTML node has a background image, applies an image fill similar to the earlier step where the node is an actual HTML &lt;code class=&quot;highlighter-rouge&quot;&gt;IMG&lt;/code&gt; element. If the background image doesn’t fit entirely within the HTML element, create a Sketch rectangle shape and use an image fill to correctly position the background image.&lt;/li&gt;
  &lt;li&gt;If the HTML has a background image that is a linear gradient, apply a Sketch gradient fill.&lt;/li&gt;
  &lt;li&gt;If the node is an &lt;code class=&quot;highlighter-rouge&quot;&gt;SVG&lt;/code&gt; element, creates a Sketch SVG layer and generates the SVG path string by walking through the child &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/SVGElement&quot;&gt;SVGElement&lt;/a&gt; HTML nodes.&lt;/li&gt;
  &lt;li&gt;If the HTML element text is visible, iterates over text nodes (see &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType&quot;&gt;nodeType&lt;/a&gt;) and creates a Sketch layer for each text node.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can see what HTML properties are supported in the Sketch conversion on the &lt;a href=&quot;https://github.com/html-sketchapp/html-sketchapp/wiki/What's-supported%3F&quot;&gt;html-sketchapp wiki&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;how-does-html-sketchapp-perform-on-real-world-examples&quot;&gt;How does html-sketchapp perform on real-world examples?&lt;/h2&gt;

&lt;p&gt;The easiest way to try html-sketchapp is to clone &lt;a href=&quot;https://github.com/html-sketchapp/html-sketchapp-example&quot;&gt;html-sketchapp-example&lt;/a&gt;, follow the setup instructions, and run the following:&lt;/p&gt;

&lt;div class=&quot;highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;npm run inject YOUR_URL
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;On right I’m comparing a screenshot of npr.org versus the html-sketchapp output. Click for a full-scale version. I’m impressed with the output:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/img/posts/html-sketchapp/npr_example.png&quot;&gt;
  &lt;img src=&quot;/img/posts/html-sketchapp/npr_example_small.png&quot; /&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p class=&quot;small text-muted&quot;&gt;
  Comparing the output of npr.org using html-sketchapp.
&lt;/p&gt;

&lt;p&gt;The most significant differences:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Fonts (I don’t have the NPR fonts installed)&lt;/li&gt;
  &lt;li&gt;Missing images - there are several of missing images that are replaced with a red rectangle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;are-there-hosted-tools-that-can-just-do-this-html-to-sketch-export-for-me&quot;&gt;Are there hosted tools that can just do this HTML to Sketch export for me?&lt;/h2&gt;

&lt;p&gt;In &lt;a href=&quot;https://medium.com/seek-blog/sketching-in-the-browser-33a7b7aa0526&quot;&gt;&lt;em&gt;Sketching in the Browser&lt;/em&gt;&lt;/a&gt; from 2018, Mark Dalgleish mentions a number of tools that are trying to bridge the code-to-design gap. At the end of 2021, only one of those tools appears to actually let you export some form of HTML (React components) into your editor: &lt;a href=&quot;https://www.uxpin.com/merge&quot;&gt;UXPin&lt;/a&gt;*.&lt;/p&gt;

&lt;h2 id=&quot;is-there-a-sweet-spot-for-html-sketchapp&quot;&gt;Is there a sweet spot for HTML-Sketchapp?&lt;/h2&gt;

&lt;p&gt;Rather than converting entire HTML documents into Sketch pages, I think the sweet spot for HTML-Sketchapp is turning coded components into Sketch symbols. This eliminates the need for designers to maintain their own design libraries.&lt;/p&gt;

&lt;p&gt;There are two relevant examples of this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/brainly/html-sketchapp-style-guide&quot;&gt;html-sketchapp-style-guide&lt;/a&gt; - Brainly’s tool for converting their styleguide into &lt;code class=&quot;highlighter-rouge&quot;&gt;*.asketch.json&lt;/code&gt; files.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/chrisvxd/story2sketch&quot;&gt;story2sketch&lt;/a&gt; - Convert &lt;a href=&quot;https://storybook.js.org/&quot;&gt;Storybook&lt;/a&gt; stories into Sketch symbols.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;github.com/html-sketchapp/html-sketchapp&quot;&gt;HTML-Sketchapp&lt;/a&gt; is a library that developers can use to help automate the conversion of HTML into the Sketch file format. It’s a great way to remove the tedious manual process designers need to apply today of maintaining their own design library. HTML-Sketchapp works great for converting coded components into Sketch symbols.&lt;/p&gt;

&lt;p class=&quot;small text-muted&quot;&gt;
* - disclaimer: &lt;a href=&quot;https://www.uxpin.com/studio/blog/meet-uxpin-merge/&quot;&gt;I'm working at UXPin to help with Merge&lt;/a&gt;.
&lt;/p&gt;</content><author><name></name></author><summary type="html">Taking the output of html-sketchapp and importing into Sketch.</summary></entry><entry><title type="html">Understanding DesignOps coming from DevOps</title><link href="https://dlite.cc/2020/08/18/design-ops.html" rel="alternate" type="text/html" title="Understanding DesignOps coming from DevOps" /><published>2020-08-18T11:00:00+00:00</published><updated>2020-08-18T11:00:00+00:00</updated><id>https://dlite.cc/2020/08/18/design-ops</id><content type="html" xml:base="https://dlite.cc/2020/08/18/design-ops.html">&lt;p&gt;As a developer who lived through the advent of DevOps and the squabbling over its meaning, I experienced a bit of déjà vu recently when I stumbled across DesignOps. This post is the Cliff Notes™ version of my research into DesignOps.&lt;/p&gt;

&lt;p&gt;Before I begin, here’s the Wikipedia definition of &lt;a href=&quot;https://en.wikipedia.org/wiki/DevOps&quot;&gt;DevOps&lt;/a&gt;. I think it is commonly accepted today:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;DevOps is a set of practices that combines software development (Dev) and IT operations (Ops). It aims to shorten the systems development life cycle and provide continuous delivery with high software quality.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This can be shortened to “get shit done” or “release often”. DevOps encourages small, incremental releases that are easier to rollback and debug than the massive quarterly release cycles of yore.&lt;/p&gt;

&lt;h2 id=&quot;what-are-common-definitions-of-designops&quot;&gt;What are common definitions of DesignOps?&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://www.abstract.com/blog/what-is-designops&quot;&gt;Abstract says&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;DesignOps is a dedicated person or team in an organization that focuses solely on enabling the design team to work as well as it possibly can.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Collin Whitehead, Head of Brand at Dropbox &lt;a href=&quot;https://medium.com/designbetter/getting-started-with-core-models-for-designops-87c5a999acb6&quot;&gt;says&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“The job of the DesignOps team is to protect the time and headspace of everyone within the design organization – the designers, writers, researchers, and so on – which allows everyone to focus on their respective craft”.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Atlassian &lt;a href=&quot;https://www.atlassian.com/blog/inside-atlassian/designops-atlassian-design-studio&quot;&gt;defines&lt;/a&gt; DesignOps as:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Putting the appropriate tools, instrumentation and processes in place so that we get to ‘learn’ as quickly as possible.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Adrian Cleave, Director of DesignOps @ AirBnB &lt;a href=&quot;https://medium.com/airbnb-design/airbnb-designops-2734cf4801b3&quot;&gt;says&lt;/a&gt; this about their DesignOps team:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Our mission is to provide agility to the whole product organization through centralized tools, systems and services that enhance  speed and quality of execution.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Finally, Almitra Inocenci &lt;a href=&quot;https://uxdesign.cc/im-sorry-but-design-ops-is-not-new-6c73a1f00e5a&quot;&gt;says&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;It’s a division of people and tasks pertaining to the planning, management, and execution of responsibilities and design process in order to get shit done, whatever the task-at-hand may be, particularly in a design organization.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;My take:&lt;/strong&gt; &lt;em&gt;DevOps has a definition focused on releasing faster. The definitions of DesignOps tend to be more broad and abstract which makes the term harder to understand. I think part of the reason DevOps is more focused is because dev teams already had a term (&lt;a href=&quot;https://en.wikipedia.org/wiki/Agile_software_development&quot;&gt;agile development&lt;/a&gt;) that covered the creation portion of releasing software. DevOps depends on this style of development. I don’t see the same division for design, so many DesignOps definitions cover everything from the earliest stages (planning) all the way to release.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-is-the-origin-story-of-designops&quot;&gt;What is the origin story of DesignOps?&lt;/h2&gt;

&lt;p&gt;While it was likely practiced without a name for a while, AirBnB is one of the first brands to discuss the term &lt;a href=&quot;https://medium.com/airbnb-design/airbnb-designops-2734cf4801b3&quot;&gt;starting around 2015&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;why-designops-now&quot;&gt;Why DesignOps now?&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/designops/dvi.jpeg&quot; alt=&quot;dvi&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Via the &lt;a href=&quot;https://www.dmi.org/general/custom.asp?page=DesignValue&quot;&gt;Design Value Index Study&lt;/a&gt;, design-centric public companies returns are more than 200% greater than the S&amp;amp;P 500. That’s a damn good reason to prioritize product design. With the increased focus on design, the ratio of developers to designers within an organization has been getting closer. For example, &lt;a href=&quot;https://techcrunch.com/2017/05/31/here-are-some-reasons-behind-techs-design-shortage/&quot;&gt;IBM’s developer-to-designer ratio target&lt;/a&gt; has changed from 72:1 to 8:1. This also doesn’t include the increased number of frontend developers that focus on implementing user experiences. It’s safe to say the ratio of backend devs to those that touch the user experience is closer than ever before.&lt;/p&gt;

&lt;p&gt;More people and faster releases requires more organization and processes, hence the growing importance of DesignOps.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;See Sonja Krogius’ post on &lt;a href=&quot;https://blog.nordkapp.fi/why-designops-why-now-c256500595a7&quot;&gt;Why DesignOps? Why now?&lt;/a&gt; for a detailed look on the growth of DesignOps.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-are-some-of-the-tools-airbnb-has-developed-to-support-designops&quot;&gt;What are some of the tools AirBnB has developed to support DesignOps?&lt;/h2&gt;

&lt;p&gt;It’s helpful to look at the tooling AirBnB has developed to increase the speed of their design process. They are perhaps the earliest DesignOps evangelist and thus have fairly polished tools. The most significant open source tools AirBnB has shared are:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/airbnb/Lona&quot;&gt;Lona&lt;/a&gt;&lt;/strong&gt; - A tool for defining &lt;a href=&quot;https://www.designbetter.co/design-systems-handbook&quot;&gt;design systems&lt;/a&gt; and using them to generate cross-platform UI code, Sketch files, and other artifacts.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/airbnb/react-sketchapp&quot;&gt;react-sketchapp&lt;/a&gt;&lt;/strong&gt; - render React components to Sketch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Both of these tools are focused on the chasm that causes the most friction between designs and their release: translating a visual design to code (and back).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lona&lt;/strong&gt;’s &lt;a href=&quot;https://github.com/airbnb/Lona/blob/master/docs/overview/background.md&quot;&gt;background doc&lt;/a&gt; explains how a Sketch-built design system requires manual translation to code for each platform AirBnB supports (web, iOS, Android, and React Native). This is “time consuming and error prone”. Lona encodes all of the detail needed to accurately translate from design to code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;react-sketchapp&lt;/strong&gt; is different than other tools that cross the design-code chasm. Most tools try to go from design to code while react-sketchapp goes in the opposite direction. By working backwards from the source of truth (the design of the deployed app), design systems are able to stay in sync.&lt;/p&gt;

&lt;h2 id=&quot;designops-should-focus-on-the-design-code-chasm&quot;&gt;DesignOps should focus on the design-code chasm&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;“We’re investing in code as a design tool. Moving closer to working with assets that don’t only include layout and design, but also logic and data. This helps bridge the gap between engineers and designers, thus reducing the need for design specs–or redlines–and the steps between vision and reality”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;-&lt;a href=&quot;https://airbnb.design/painting-with-code/&quot;&gt;Alex Schleifer&lt;/a&gt;, head of design at AirBnB&lt;/p&gt;

&lt;p&gt;Designers and developers have a chasm to cross: translating design to code (and back). Rather than a change in mindset, it became possible to release high-quality software faster (the goal of DevOps) due to dramatic enhancements in version control (Git), code review tools (GitHub), continuous integration products that automatically run tests, automated code quality products, and error monitoring. If what I saw in DevOps holds true for DesignOps, tools that reduce design/code friction likely outweighs the more abstract, softer side of DesignOps definitions.&lt;/p&gt;</content><author><name></name></author><summary type="html">As a developer who lived through the advent of DevOps and the squabbling over its meaning, I experienced a bit of déjà vu recently when I stumbled across DesignOps. This post is the Cliff Notes™ version of my research into DesignOps.</summary></entry><entry><title type="html">Storybook - a beautiful library for your web components</title><link href="https://dlite.cc/2020/08/16/storybook.html" rel="alternate" type="text/html" title="Storybook - a beautiful library for your web components" /><published>2020-08-16T11:00:00+00:00</published><updated>2020-08-16T11:00:00+00:00</updated><id>https://dlite.cc/2020/08/16/storybook</id><content type="html" xml:base="https://dlite.cc/2020/08/16/storybook.html">&lt;p&gt;&lt;img src=&quot;/img/posts/storybook/ui.png&quot; alt=&quot;storybook&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Like a set of Lego bricks, a web app’s UI is composed of &lt;a href=&quot;https://www.componentdriven.org/&quot;&gt;individual components&lt;/a&gt;. Many of these components also have multiple states. For example, a navigation header may have multiple states:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Displaying the avatar of a user if logged in and a “sign in” link if not&lt;/li&gt;
  &lt;li&gt;Adding a banner if a free trial is coming to a close shortly&lt;/li&gt;
  &lt;li&gt;Adding a notice if parts of the service are not working correctly&lt;/li&gt;
  &lt;li&gt;Displaying additional features for admin users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To verify each state looks acceptable, I need to start a local version of the app, load a page in my browser, and override values (ie set &lt;code class=&quot;highlighter-rouge&quot;&gt;admin = true&lt;/code&gt;, adjust the signup date of an account, etc). The reality? I rarely test each state when making a change. It’s painful and awkward (and I’m a bit lazy). Enter &lt;a href=&quot;https://storybook.js.org/&quot;&gt;Storybook&lt;/a&gt;, an open source UI component explorer that lets you develop UI components in isolation.&lt;/p&gt;

&lt;p&gt;Here’s a look at my initial experience using Storybook.&lt;/p&gt;

&lt;h2 id=&quot;who-is-storybook-for&quot;&gt;Who is Storybook for?&lt;/h2&gt;

&lt;p&gt;Storybook is designed for frontend developers that are already using a Javascript framework to create components and CSS to style them. However, there are several secondary users:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Designers - verify the design of UI components and their states are true to your designs.&lt;/li&gt;
  &lt;li&gt;Project Managers - quickly QA UI component changes without loading the entire web app locally.&lt;/li&gt;
  &lt;li&gt;Backend developers &amp;amp; ops - alleviate the need for frontend devs, designers, and project managers to keep a full local dev stack updated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s not much of a learning curve when getting started with Storybook as you continue to develop components in your editor. You just view them within Storybook. I like that Storybook doesn’t try to own the editing experience. We’re all opinionated about our editors.&lt;/p&gt;

&lt;h2 id=&quot;setting-up-storybook&quot;&gt;Setting up Storybook&lt;/h2&gt;

&lt;p&gt;Storybook is installed inside an existing application. In my test of Storybook, I used &lt;a href=&quot;https://create-react-app.dev/&quot;&gt;create-react-app&lt;/a&gt; to setup a simple React app (see a detailed &lt;a href=&quot;https://www.learnstorybook.com/intro-to-storybook/react/en/get-started/&quot;&gt;React + Storybook tutorial&lt;/a&gt; for more info) then installed Storybook via &lt;code class=&quot;highlighter-rouge&quot;&gt;npx -p @storybook/cli sb init&lt;/code&gt;. In addition to React, Storybook supports Vue, Angular, Ember, and more frameworks. However, the official docs and tutorials appear to be more extensive for React than other frameworks.&lt;/p&gt;

&lt;p&gt;Once installed, you start the local Storybook server via &lt;code class=&quot;highlighter-rouge&quot;&gt;yarn storybook&lt;/code&gt;. This command opens a browser tab at &lt;code class=&quot;highlighter-rouge&quot;&gt;http://localhost:6006&lt;/code&gt; and is filled with a number of example React components and their associated stories (more on stories shortly). Checking out these examples is a good place to start. The Storybook app is well-designed with a clean esthetic.&lt;/p&gt;

&lt;p&gt;Storybook has an addon system to extend its functionality. Starting with version 6.0.0, &lt;a href=&quot;https://medium.com/storybookjs/zero-config-storybook-66e7c4798e5d&quot;&gt;essential addons are pre-installed&lt;/a&gt; and I found the app to be immediately usable from the start.&lt;/p&gt;

&lt;p&gt;Storybook doesn’t magically import your existing React components. However, it’s not a huge amount of work to have a component appear in Storybook. This is also a process you can perform incrementally, adding your most important components first.&lt;/p&gt;

&lt;h2 id=&quot;creating-stories&quot;&gt;Creating stories&lt;/h2&gt;

&lt;p&gt;When I first started Storybook, I expected to see my custom components immediately. This didn’t happen and I was disappointed. However, I realized this didn’t make much sense as most components require a default parameter values to appear. For example, my alert component needs some text to render:&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// src/components/Alert.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;React&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;alertType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;className=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`alert ${alertType}`&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To view a component within Storybook you need to create at least one &lt;a href=&quot;https://storybook.js.org/docs/react/get-started/whats-a-story&quot;&gt;story&lt;/a&gt;. A story describes an interesting state of a component. For my alert box, I decided to start with &lt;code class=&quot;highlighter-rouge&quot;&gt;Default&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;Error&lt;/code&gt; stories.&lt;/p&gt;

&lt;h2 id=&quot;importing-a-react-component-into-storybook&quot;&gt;Importing a React component into Storybook&lt;/h2&gt;

&lt;p&gt;Importing a component into Storybook requires just two steps:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Create a &lt;code class=&quot;highlighter-rouge&quot;&gt;src/components/[COMPONENT].stories.js&lt;/code&gt; file with at least one story. Stories follow the &lt;a href=&quot;https://storybook.js.org/docs/react/api/csf&quot;&gt;Component Story Format&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Restart the storybook server. Restarting is only required when adding a component, not updating an existing component.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My Alert stories file:&lt;/p&gt;

&lt;div class=&quot;language-jsx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// src/components/Alert.stories.js&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;React&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;react&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;./Alert&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;component&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Alert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;Alert&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;argTypes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;The text to display within the alert box&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;defaultValue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;This is the alert text.&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;
          &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Template&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Alert&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;/&amp;gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Default&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({});&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Template&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({});&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;alertType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I like the separation of stories from the component. Storybook doesn’t force you to modify your component code. The additional functionality provided by Storybook is isolated to stories files. See Storybook’s &lt;a href=&quot;https://storybook.js.org/docs/react/writing-stories/introduction&quot;&gt;Story docs&lt;/a&gt; for more information on creating stories. Additionally, no Storybook-specific libraries are required when creating a stories file.&lt;/p&gt;

&lt;h2 id=&quot;autogenerating-docs&quot;&gt;Autogenerating docs&lt;/h2&gt;

&lt;p&gt;I love great docs. One feature I love about Storybook is its ability to &lt;a href=&quot;https://storybook.js.org/docs/react/writing-docs/introduction&quot;&gt;autogenerate docs&lt;/a&gt; from source code comments. This creates a single source of truth for docs.&lt;/p&gt;

&lt;p&gt;Here’s the docs for the Alert component:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/storybook/docs.png&quot; alt=&quot;docs&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;wait-theres-more&quot;&gt;Wait there’s more&lt;/h2&gt;

&lt;p&gt;Storybook has a robust &lt;a href=&quot;https://storybook.js.org/addons/&quot;&gt;addon ecosystem&lt;/a&gt; that helps you build out an automated &lt;a href=&quot;//2020/08/18/design-ops.html&quot;&gt;DesignOps&lt;/a&gt; process. From importing &lt;a href=&quot;https://storybook-addon-headless.netlify.app/?path=/story/intro--page&quot;&gt;dynamic data&lt;/a&gt; to &lt;a href=&quot;https://www.chromatic.com/&quot;&gt;visual testing&lt;/a&gt;, there are many ways to integrate Storybook into your existing tools.&lt;/p&gt;

&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://storybook.js.org/&quot;&gt;Storybook&lt;/a&gt; is a polished, well-designed UI component browser that can be extended in many ways. Easy to install and incrementally integrate into your existing apps, Storybook provides a clean separation of Storybook-specific functionality and your existing components.&lt;/p&gt;</content><author><name></name></author><summary type="html"></summary></entry><entry><title type="html">A quick look at Rookout, a real-time debugging &amp;amp; logging product</title><link href="https://dlite.cc/2020/08/05/rookout-review.html" rel="alternate" type="text/html" title="A quick look at Rookout, a real-time debugging &amp; logging product" /><published>2020-08-05T11:00:00+00:00</published><updated>2020-08-05T11:00:00+00:00</updated><id>https://dlite.cc/2020/08/05/rookout-review</id><content type="html" xml:base="https://dlite.cc/2020/08/05/rookout-review.html">&lt;p&gt;&lt;img src=&quot;/img/posts/rookout/rookout_logo.png&quot; style=&quot;float:right;padding:0 0 10px 10px&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The great thing about defining a new category is that there is no competition. The bad part is it can be difficult to explain what problem your product solves and how it fits alongside other tools. &lt;a href=&quot;https://www.rookout.com/&quot;&gt;Rookout&lt;/a&gt;, described as “Rapid Debugging. Frictionless Logging” falls into this bucket. Recently I spent a couple hours digging into Rookout to better understand its place in an engineering team’s tool chest.&lt;/p&gt;

&lt;p&gt;If you ask me to define a debugger, I go straight to an &lt;em&gt;interactive debugger&lt;/em&gt;. For example, searching for &lt;a href=&quot;https://duckduckgo.com/?q=rails+debugger&amp;amp;t=ffab&amp;amp;atb=v184-1&amp;amp;ia=web&quot;&gt;&lt;em&gt;Rails debugger&lt;/em&gt;&lt;/a&gt; brings up &lt;a href=&quot;http://pry.github.io/&quot;&gt;pry&lt;/a&gt; and &lt;a href=&quot;https://github.com/deivid-rodriguez/byebug&quot;&gt;byebug&lt;/a&gt;. These are both &lt;em&gt;interactive&lt;/em&gt; debuggers where the application pauses at breakpoints and allows you to step through the execution (even running arbitrary code). &lt;em&gt;This is not what Rookout does.&lt;/em&gt; Rookout does not pause a live application. I think a better description is that &lt;strong&gt;Rookout lets developers apply temporary logging to a live application without having to deploy a new version.&lt;/strong&gt; Let me explain.&lt;/p&gt;

&lt;p&gt;Let’s say your exception monitoring service (say &lt;a href=&quot;https://sentry.io&quot;&gt;Sentry&lt;/a&gt;) is reporting an elevated error rate and your APM product (say &lt;a href=&quot;https://scoutapm.com&quot;&gt;ScoutAPM&lt;/a&gt;) is showing a rapid increase in response times. You inspect the exception details and transaction traces but do not see an obvious culprit. Next step? Adding some additional log lines.&lt;/p&gt;

&lt;p&gt;If you aren’t using Rookout, you typically do this by creating a new git branch, adding the new log lines, and deploying. This deployment cycle can be lengthy on large, critical applications. Additionally, it’s rare to capture what you need in the first commit+deploy cycle. This means more deploys, more logging, and a slow feedback cycle. Instead, what if could tell your live, production app in real-time to begin logging additional information at a given file and line number? That’s what Rookout can do.&lt;/p&gt;

&lt;p&gt;I took Rookout for a spin using their &lt;a href=&quot;https://github.com/Rookout/deployment-examples/tree/master/python-django&quot;&gt;demo Python Django application&lt;/a&gt;. After following their setup instructions (and with some fast help from their support team) here’s how it works:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Open the “sources” tab. Navigate to the file and line number you wish to add a breakpoint.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Click in the gutter next to the line number.
&lt;img src=&quot;/img/posts/rookout/add_breakpoint.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Wait a couple seconds for the confirmation that the breakpoint is active.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When the breakpoint is triggered, a new message will appear in the Rookout UI:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/rookout/triggered.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Click the message row for more details (like the local variables, stacktrace, and more). You can also define a custom log message and reference variables and their properties. For example, I modified the log message to print the todo description:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/rookout/desc.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It’s likely you are using another destination for your logging output. From Slack to ElasticSearch to DataDog, &lt;a href=&quot;https://docs.rookout.com/docs/integrations/&quot;&gt;Rookout can send messages to many targets&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A significant fuzzy area for me is how likely a breakpoint will persist across git commits and branches. What if you modify a file that contains existing breakpoints - do the breakpoints disappear on the next commit? Rookout support says they make a best-effort to preserve breakpoints by hashing each LOC referenced by a breakpoint, but that it isn’t always possible. Because of that, it seems safer to view Rookout as a temporary logger when actively debugging a problem. As a product, they would be in a better position if breakpoints were guaranteed to persist. Then, you could depend on Rookout to log everything. I can see this being a hard problem to solve.&lt;/p&gt;

&lt;p&gt;In summary, &lt;a href=&quot;https://rookout.com&quot;&gt;Rookout&lt;/a&gt; is an interesting way to increase logging in a production app without requiring new deploys to add calls to a logger. I think it’s most valuable for teams that have larger apps where the deploy cycle is longer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: I have not tested Rookout on a live, significant, production application.&lt;/em&gt;&lt;/p&gt;</content><author><name></name></author><summary type="html"></summary></entry><entry><title type="html">Zwift + Wahoo Kickr Core Review</title><link href="https://dlite.cc/2020/08/03/zwift-wahoo-kickr-review.html" rel="alternate" type="text/html" title="Zwift + Wahoo Kickr Core Review" /><published>2020-08-03T11:00:00+00:00</published><updated>2020-08-03T11:00:00+00:00</updated><id>https://dlite.cc/2020/08/03/zwift-wahoo-kickr-review</id><content type="html" xml:base="https://dlite.cc/2020/08/03/zwift-wahoo-kickr-review.html">&lt;p&gt;As an almost 40 year-old raised playing competitive team sports, I still itch to take the field. That said, lots of things can make scheduling this time difficult: family commitments, work, and an aging body are just a few of the excuses. That’s why I’ve found group road cycling rides here in Fort Collins, Colorado to be an almost-perfect solution for my middle-aged competitive needs.&lt;/p&gt;

&lt;p&gt;In my cycling bubble we have standing hard group rides during lunch on Tuesday and Thursday, Wednesday evening (two options), and Saturday morning. There’s no need to coordinate ad-hoc times with friends and the low-impact nature of cycling makes it a lot easier to do a couple of these hard rides each week.&lt;/p&gt;

&lt;p&gt;However, you know what’s not fun about group rides? Hanging off the back, struggling for air. That was my default mode of operation during the spring and early summer as I usually spent the colder months off the bike. To make these rides consistently more fun, I decided to dive into a smart trainer setup. Here’s an overview of my setup, a review of the key parts, and an answer to the key question: does work on a smart trainer translate to real-world group rides?&lt;/p&gt;

&lt;h2 id=&quot;my-indoor-training-supplies&quot;&gt;My Indoor Training Supplies&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://zwift.com&quot;&gt;Zwift&lt;/a&gt; - multiplayer online cycling training program ($14.99/mo)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.wahoofitness.com/devices/bike-trainers/kickr-core-indoor-smart-trainer&quot;&gt;Wahoo Kickr Core&lt;/a&gt; - smart bike trainer ($899.99)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://strava.com&quot;&gt;Strava&lt;/a&gt; - training activity tracker ($60/yr)&lt;/li&gt;
  &lt;li&gt;MacBook Pro (2014)&lt;/li&gt;
  &lt;li&gt;Fan&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.wahoofitness.com/devices/accessories/usb-ant-kit&quot;&gt;Wahoo USB ANT+ Dongle &amp;amp; Extension Cable Kit&lt;/a&gt; - provides better connectivity than the Mac’s bluetooth ($39.99)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.wahoofitness.com/devices/heart-rate-monitors/tickr/buy&quot;&gt;Wahoo TICKR&lt;/a&gt; - Heart rate monitor chest strap ($49.99)&lt;/li&gt;
  &lt;li&gt;Floor mat - collect the sweat&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.wahoofitness.com/devices/bike-trainers/snap-wheel-block&quot;&gt;Wahoo Kickr Snap Wheel Block&lt;/a&gt; - keep the bike level ($19.99)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some notes on my equipment:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Initially I used an optical heart rate monitor on my arm, the &lt;a href=&quot;https://www.wahoofitness.com/devices/heart-rate-monitors/tickr-fit-optical-heart-rate-monitor&quot;&gt;Wahoo TICKR FIT&lt;/a&gt;. However, the numbers didn’t pass the eye test: my heart rate would drop off a cliff at the end of a workout (when I’d expect it to be a bit a higher than the beginning) and in general the numbers were always lower than I’d expect. The chest strap (&lt;a href=&quot;https://www.wahoofitness.com/devices/heart-rate-monitors/tickr/buy&quot;&gt;Wahoo TICKR&lt;/a&gt;) reports the numbers I’d expect.&lt;/li&gt;
  &lt;li&gt;I started by connecting my devices (the Wahoo Kickr Core and Wahoo TICKR) to my computer via bluetooth. However, I experienced intermittent connectivity issues with the bluetooth connection. Connecting by ANT+ via the &lt;a href=&quot;https://www.wahoofitness.com/devices/accessories/usb-ant-kit&quot;&gt;Wahoo USB ANT+ Dongle&lt;/a&gt; resolved all of my bluetooth dropouts.&lt;/li&gt;
  &lt;li&gt;A good fan is critical. I didn’t use one as the weather turned warmer and I’d drop five pounds of water in an hour workout. My workouts were pretty terrible. Adding a solid fan left me feeling much better.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;why-try-indoor-training-now-versus-ten-years-ago&quot;&gt;Why try indoor training now versus ten years ago?&lt;/h2&gt;

&lt;p&gt;Indoor training for cycling has evolved considerably over the years making the act of riding your bike without moving more rewarding. I’d describe the evolution as three eras:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;The Rocky V Era&lt;/strong&gt; - just throw your bike on a set of &lt;a href=&quot;https://www.amazon.com/ROCK-BROS-Foldable-Resistance-Exercise/dp/B07VX4J2JD/ref=zg_bs_6389527011_2?_encoding=UTF8&amp;amp;psc=1&amp;amp;refRID=V1D148JS5AX2AF7SSBJR&quot;&gt;rollers&lt;/a&gt; or &lt;a href=&quot;https://www.amazon.com/Bike-Trainer-Stand-Sportneer-Reduction/dp/B071HQFKBY/ref=sr_1_3?_encoding=UTF8&amp;amp;c=ts&amp;amp;dchild=1&amp;amp;keywords=Bike+Resistance+Trainers&amp;amp;qid=1596373368&amp;amp;sr=8-3&amp;amp;ts_id=3403551&quot;&gt;basic trainer&lt;/a&gt;, turn on your favorite cycling video, and ride. Perhaps you use a heart rate monitor, but beyond that, you’re riding natural.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The Smart Solo Era&lt;/strong&gt; - Connect your smart trainer to a computer so it can control the resistance and generate tailored workouts. Apps like &lt;a href=&quot;https://www.trainerroad.com/&quot;&gt;Trainer Road&lt;/a&gt; ($19.99/mo) and &lt;a href=&quot;https://thesufferfest.com/&quot;&gt;The Sufferfest&lt;/a&gt; ($14.99/mo) provide this.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The Multi-Player Era&lt;/strong&gt; - &lt;a href=&quot;https://zwift.com&quot;&gt;Zwift&lt;/a&gt; enters the scene, providing virtual worlds where cyclists use their smart trainers to race against each other in realtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tangentially, &lt;a href=&quot;https://www.onepeloton.com/bike&quot;&gt;Peleton&lt;/a&gt; (from $2,245 or $58/mo for 39 mos) emerged within the Multi-Player Era of indoor cycling training as well. Like Zwift, Peleton provides a real-time group environment. Peleton is targeted at in-person cycling class participants like &lt;a href=&quot;https://www.soul-cycle.com/&quot;&gt;Soul Cycle&lt;/a&gt; rather than avid outdoor cyclists.&lt;/p&gt;

&lt;p&gt;Why has indoor training evolved this way? It’s all about motivation. In the &lt;em&gt;The Rocky V Era&lt;/em&gt;, it was up to you define workouts and get your heart rate into the proper zone. That’s easy to let up on. &lt;em&gt;The Smart Solo Era&lt;/em&gt; made it easier to stay motivated by setting power and interval lengths for you and you see your metrics (power, heart rate, cadence, etc) following a workout. It’s motivating to hit the workout’s prescribed numbers. Finally, the &lt;em&gt;The Multi-Player Era&lt;/em&gt; taps into our innate competitive instinct: it’s hard to resist chasing a rider up the road even when they are just a collection of sprites on the screen. It’s easier than ever before to stay motivated on a trainer.&lt;/p&gt;

&lt;h2 id=&quot;setting-up-the-wahoo-kickr-core-and-zwift&quot;&gt;Setting up the Wahoo Kickr Core and Zwift&lt;/h2&gt;

&lt;p&gt;Connecting all of these parts isn’t too bad:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Remove the rear wheel of the bike. Connect the bike to the cassette on the Wahoo Kickr Core. You’ll need to purchase and attach your own cassette to the Kickr. I had the bike shop where I purchased the trainer do this for me.&lt;/li&gt;
  &lt;li&gt;Turn on your computer, power up Zwift.&lt;/li&gt;
  &lt;li&gt;Connect the USB ANT+ doggle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zwift should automatically connect to the trainer and heart rate monitor.&lt;/p&gt;

&lt;h2 id=&quot;zwift-riding-options&quot;&gt;Zwift riding options&lt;/h2&gt;

&lt;p&gt;There are several ways to use Zwift. Here’s the primary ways:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Free Ride&lt;/strong&gt; - Just pick a world and ride. You’ll be in the virtual world with other riders across the globe. Zwift adjusts the trainer resistance as inclines increase on the virtual road and drops it down when you’re descending.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Workouts&lt;/strong&gt; - Pick from many individual workouts, organized by their total time. Workout difficulty is based on power zones determined from your FTP.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Training Programs&lt;/strong&gt; - In addition to individual workouts, Zwift also provides collections of workouts called training programs (ex: 8 week race prep).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Virtual Race&lt;/strong&gt; - Race against others in a virtual world. Riders are grouped in one of 4 categories based on their FTP (you pick the category). There are typically multiple races every hour.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Group Training&lt;/strong&gt; - Similar to a virtual race, you can also do a group training ride. There are two primary types of training rides: hour plus rides that attempt to ride at a constant watts/kg and interval rides.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I use the individual workouts and virtual races. I spend my soul riding time outdoors, so I don’t use the free ride option. I find it hard to stick to a long training program with many varied workouts as it is hard to gauge my progress when the workouts change so frequently. Finally, the group training felt like a bit of a mess. Because riding in a group is motivating - even in a virtual world - many folks end up pushing the pace and it ends up resembling a race anyway.&lt;/p&gt;

&lt;h2 id=&quot;how-does-riding-on-a-trainer-feel-versus-the-road&quot;&gt;How does riding on a trainer feel versus the road?&lt;/h2&gt;

&lt;p&gt;While riding your bike on a stationary trainer still feels a lot like riding the same bike on the road, I’ve noticed some differences:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;Keep Pedaling&lt;/em&gt; - Unlike riding on the road, your pedals are always moving when using a smart trainer. Other riders don’t coast in Zwift and when using &lt;a href=&quot;https://support.trainerroad.com/hc/en-us/articles/201869764-Erg-Mode-Explained&quot;&gt;ERG Mode&lt;/a&gt; in a workout (the default) the trainer sets the resistance for you. If you stop pedaling, it’s a bear to get the pedals moving again. I don’t think this behavior is a negative as it makes my trainer rides more efficient than training on the road. An hour on the trainer feels like two hour ride on real roads.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Harder to move from a seated to standing position&lt;/em&gt; - It feels like considerably more work shifting to a standing position than it does on the road. I believe this is because there are no inclines and you need to keep the pedals moving when in ERG mode.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Less shifting&lt;/em&gt; - I never use my small chain ring on the trainer and only shift between a couple positions on the cassette. Shifting is not as smooth as in the real world.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because of the constant pedaling and less dynamic positioning on the bike, initially I was more sore than on an outdoor session. This got better over time.&lt;/p&gt;

&lt;h2 id=&quot;my-basic-training-program&quot;&gt;My basic training program&lt;/h2&gt;

&lt;p&gt;I believe in consistency more than sticking to a workout program I don’t want to do (I’m not getting paid to ride my bike). I enjoy listening to the &lt;a href=&quot;https://www.velonews.com/tag/fast-talk-podcast/&quot;&gt;Velonews Fast Talk&lt;/a&gt; and they regularly cover training. There were two theories that hit home for me:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Short sessions in the winter&lt;/strong&gt; - On one episode, a coach from Toronto advocated for keeping indoor training sessions short (an hour or less) versus traditional large endurance block in the off season. I live in a cold weather climate and long, cold endurance rides sound very demotivating. So does doing long trainer rides in my garage. Why not push these long rides do the warmer spring months?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Stick to a few basic workouts&lt;/strong&gt; - Zwift has multi-week training programs where each workout is different than the last. On one episode, a coach advocated for keeping just a few workouts in rotation. This makes it far easier to track your progress. For me, I stick with 2x20’ and 5x5’ intervals as they each stress my fitness in different ways and work well on the trainer. I’ve found short intervals (say 30 seconds or less) to be really awkward on a trainer with ERG mode. Rotating just two workouts would get very repetitive, so I mix it up with some sweet spot rides and Zwift races.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Polarized training&lt;/strong&gt; - It’s very hard to ride slow and do a proper recovery ride in the real world. With a smart trainer on ERG mode, this is easy. After an interval day I’ll do a recovery ride on the trainer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical winter week might look like this for me:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;MON - 50 minutes Sweet Spot&lt;/li&gt;
  &lt;li&gt;TUE - Zwift Race&lt;/li&gt;
  &lt;li&gt;WED - Recovery ride/jog &amp;amp; weights&lt;/li&gt;
  &lt;li&gt;THU - 2x20 intervals&lt;/li&gt;
  &lt;li&gt;FRI - Recovery ride/jog &amp;amp; weights&lt;/li&gt;
  &lt;li&gt;SAT - 5x5 intervals&lt;/li&gt;
  &lt;li&gt;SUN - Rest or Recovery ride/jog &amp;amp; weights&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ll also substitute a Zwift race for a 2x20 interval (more on this below). If I feel like a big endurance day I’ll try get out for a backcountry ski versus a cold day on the bike.&lt;/p&gt;

&lt;h2 id=&quot;zwift-racing&quot;&gt;Zwift racing&lt;/h2&gt;

&lt;p&gt;Zwift’s bread-and-butter is the virtual racing environment. Zwift has several virtual worlds (three based on real-world locations and one fictional) that host races. There are typically races every hour of the day with varying distances (most take between 40 minutes and 1 hour 20 minutes) and types of courses (flat, hilling, long climbs). When you sign up for a race, you also pick your category (from A-D with A being an FTP of 4.0 w/kg or greater).&lt;/p&gt;

&lt;p&gt;I was pleasantly surprised to find these races to be very motivating. Just like the real world, you ride in a pack and if you are on the front, you need to do more work. You can soft pedal to drop off the front. There’s race dynamics: you can tell that at the end of the race other riders are trying to stay off the front while keeping the pace going. On inclines it’s all about your w/kg (factors like wind don’t come into play). There are also several rough sections (dirt, cobbles) where pure power wins.&lt;/p&gt;

&lt;p&gt;Zwift races start very, very hard. After a couple of minutes, they settle into a pace. I’d classify races as an hour-long FTP session if you are going hard. Because of this feel, I’ll substitute a race for a 2x20 session if I feel inclined.&lt;/p&gt;

&lt;p&gt;Just like the real world of sports, it’s not a level playing field in races. Since w/kg is vital for any race that includes climbs it’s common to see riders stretching the truth on their actual weight. I’ve seen this by comparing race results with linked Strava profiles - somehow a rider that easily looks 180 lbs loses in a photo drops 40 lbs in Zwift.&lt;/p&gt;

&lt;p&gt;Another factor that Zwift doesn’t account for is that your &lt;a href=&quot;https://joefrielsblog.com/altitude-and-aerobic-performance/&quot;&gt;FTP lowers as altitude increases&lt;/a&gt;. I ride at about 5k feet here in Colorado, which means my FTP is about 5.6% lower than at sea level. If my FTP is 300 watts, my FTP at sea level would be nearly 317 watts. That’s a pretty big difference in a race.&lt;/p&gt;

&lt;p&gt;I think the best mental strategy when doing a Zwift race is to focus less on your place and more on your numbers. There’s always a group to ride with and compete against. It feels the same whether they are truthful about their weight and whatever altitude they reside in.&lt;/p&gt;

&lt;h2 id=&quot;does-it-translate-to-group-rides&quot;&gt;Does it translate to group rides?&lt;/h2&gt;

&lt;p&gt;The reason I considered riding stationary, staring at my dusty garage wall is to enjoy group rides for a greater portion of the year. While these COVID times prematurely aborted the group riding season, I felt better in the handful of group rides I was able to do in the spring of 2020. The one area that is difficult to replicate on a trainer are the short, high-power surges that occur on a group ride. I struggled with these initially but in a few weeks had adjusted for these. I’m OK with that - just don’t be disappointed if you notice the same problem.&lt;/p&gt;

&lt;h2 id=&quot;do-i-use-an-indoor-trainer-in-the-summer&quot;&gt;Do I use an indoor trainer in the summer?&lt;/h2&gt;

&lt;p&gt;Surprising, I’ve found myself continuing to ride the trainer during the warm weather months. I’ve done this for three reasons:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Better for hard training rides&lt;/strong&gt; - An interval ride is just as hard indoors or outside. When I do these indoors, I don’t have to worry about traffic, flats, or other issues.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Better for recovery rides&lt;/strong&gt; - A smart trainer showed me how slow I really need to ride to do a recovery ride. I realized I likely never performed a proper recovery ride in the wild.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Outdoors is for the soul&lt;/strong&gt; - Since my structured time is indoors, this frees my soul to ride outside by feel. Indoors is for structure, outdoors is for freedom.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;

&lt;p&gt;Using the Wahoo Kickr and Zwift helped me build my FTP (or at least keep in reasonable) at the start of the group riding season in the spring without suffering through long cold weather rides. The virtual world of Zwift is almost as motivating as the real world and helped me ride consistently through the winter. I continue to use the indoor trainer during summer for short, high-intensity workouts and dedicate outdoor to riding by feel.&lt;/p&gt;</content><author><name></name></author><summary type="html">As an almost 40 year-old raised playing competitive team sports, I still itch to take the field. That said, lots of things can make scheduling this time difficult: family commitments, work, and an aging body are just a few of the excuses. That’s why I’ve found group road cycling rides here in Fort Collins, Colorado to be an almost-perfect solution for my middle-aged competitive needs.</summary></entry></feed>