<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>MeyerPerin</title>
<link>https://meyerperin.org/</link>
<atom:link href="https://meyerperin.org/index.xml" rel="self" type="application/rss+xml"/>
<description>The MeyerPerin Foundation is a non-profit organization dedicated to the advancement of knowledge in science and technology, with a preferred focus on Artificial Intelligence and Education. We are especially interested in helping neurodivergent people thrive in their scientific and technical pursuits, especially those with Autism and ADHD.</description>
<generator>quarto-1.8.27</generator>
<lastBuildDate>Sat, 28 Feb 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>Croppers and the Death of Leetcoding</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2026-02-28-leetcoding.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>A couple of years ago, I wrote about the <a href="https://www.meyerperin.com/posts/2024-03-06-luddites.html">Luddites and the AI Revolution</a>. In that post, I talked about croppers, the skilled workers who used massive 40-pound shears to finish cloth. Their skill was so specialized and physically demanding that you could identify a cropper by the large callus on their forearm, called a “saddle.” They were well-paid and high-status, and then machines came along and did the same job faster and cheaper.</p>
<p>I’ve been thinking about croppers again lately, because I realize that the tech industry has its own version of them: LeetCode grinders.</p>
<section id="the-croppers-skill" class="level2">
<h2 class="anchored" data-anchor-id="the-croppers-skill">The Cropper’s Skill</h2>
<p>Think about what made croppers valuable. They endured years of painful training to develop a very specific physical skill. The skill itself wasn’t the point. The point was to produce a well-finished piece of cloth. But because the skill was hard to acquire, it became a proxy for the outcome. If you had the saddle, you could finish cloth. If you could finish cloth, you were valuable.</p>
<p>It’s unlikely that anybody actually <em>wanted</em> to develop a giant callus on their forearm. The callus was a side effect of the real work. But over time, the callus became the credential. You could tell a cropper by his saddle.</p>
</section>
<section id="the-leetcoders-skill" class="level2">
<h2 class="anchored" data-anchor-id="the-leetcoders-skill">The LeetCoder’s Skill</h2>
<p>Now think about what makes a LeetCode grinder valuable, or rather, what is <em>supposed</em> to make them valuable. They spend months memorizing algorithms and data structures, practicing problems until they can solve them under time pressure. The skill itself isn’t the point: the point is to identify people who can write good software. But because the skill is hard to acquire and (loosely) related to writing software, it became a proxy for the outcome.</p>
<p>It’s unlikely that anybody actually <em>wants</em> to memorize the algorithm for finding the longest palindromic substring. That knowledge is a side effect of what companies are ostensibly looking for: the ability to solve real engineering problems. But over time, the LeetCode performance became the credential. You can tell a grinder by their leetcode performance.</p>
<p>I’ve <a href="https://www.meyerperin.com/posts/2022-07-22-leetcode.html">written before</a> about how to separate LeetCode grinders from people who can actually code. Even back in 2022, the distinction mattered. Grinders could ace a standard interview while being mediocre at the actual job. The proxy had already diverged from the outcome it was supposed to measure.</p>
</section>
<section id="croppers-were-more-useful" class="level2">
<h2 class="anchored" data-anchor-id="croppers-were-more-useful">Croppers Were More Useful</h2>
<p>Here’s the thing, though. Croppers, for all the absurdity of using a callus as a credential, were actually doing the real work when cropping. The saddle was a byproduct of genuinely finishing cloth. A cropper’s skill and the output were directly connected. A great cropper produced great cloth, and a bad cropper produced bad cloth. The credential was imperfect, but it was at least <em>correlated</em> with the real thing.</p>
<p>LeetCode grinding doesn’t necessarily have that property. The ability to implement a red-black tree on a whiteboard or on a notepad-like interface in a video interview has, at best, a weak correlation with the ability to design a reliable and useful system. The proxy was always more disconnected from the outcome than the cropper’s saddle ever was.</p>
<p>So even before AI came along, LeetCoders were already a worse version of croppers: people who developed a painful, specialized skill that served mainly as a credential, except the credential was less connected to the actual work. Hopefully, when hiring a leetcoder, you were getting a good software engineer, and in many cases you were. But there was a lot of “false negatives”: people who could write great software but didn’t do well under pressure, or did not have a lot of time to memorize algorithms while developing a real system and raising a family. There were also a lot of “false positives”: people who could grind LeetCode but weren’t great engineers.</p>
</section>
<section id="rage-against-the-machine" class="level2">
<h2 class="anchored" data-anchor-id="rage-against-the-machine">Rage Against the Machine</h2>
<p>And now, of course, there’s a machine that does it all.</p>
<p>AI can easily solve LeetCode problems. Not just the easy ones, it also handles hard problems reliably and quickly. The entire ritual of memorizing algorithms, practicing under time pressure, and performing in a 45-minute interview can now be done by a system that costs a few cents per query. The machine doesn’t need months of preparation, doesn’t get nervous, and doesn’t forget the algorithm after the interview is over.</p>
<p>In addition, in the last couple of months, creating software with AI agents have <em>felt</em> different. Agents are tackling more complex problems, learning from the existing codebase, and writing code that is very good. The value of being who memorized the syntax of a particular programming language has declined. And since AI models have memorized all Leetcode problems, the value of being someone who also memorized them has also gone down.</p>
</section>
<section id="whats-left" class="level2">
<h2 class="anchored" data-anchor-id="whats-left">What’s Left</h2>
<p>When Mellor and the croppers saw the machines, they knew their skill was becoming worthless. John Booth, the young outsider, tried to tell them that the machine could be “man’s chief blessing instead of his curse,” if the benefits were fairly shared. But that was cold comfort to the workers, and in the end, even Booth couldn’t hold to his own argument. He joined the Luddites and died in the raid on Rawfolds Mill, at nineteen.</p>
<p>The croppers’ trade didn’t adapt. It simply disappeared. But the cloth industry didn’t disappear with it. It grew, and the people who thrived in it were the ones who understood cloth, not the ones who had the biggest calluses. The skill that mattered was never the shearing itself. It was creating cloth products that users wanted and needed.</p>
<p>The same will be true for software engineers. LeetCode performance is the saddle: a painful credential that was always an imperfect proxy for the real thing. What will matter is what always mattered: judgment, taste, and knowing what good software looks like. Now you won’t need to code everything yourself and memorize syntax. You can simply direct AI agents that write the code for you.</p>
<p>The saddle was never the point. The cloth was.</p>
</section></article>
<hr>
<p>This post appeared first on <a href="https://www.meyerperin.org/posts/2026-02-28-leetcoding.html">www.meyerperin.org</a>.</p>




 ]]></description>
  <guid>https://meyerperin.org/posts/2026-02-28-leetcoding.html</guid>
  <pubDate>Sat, 28 Feb 2026 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/croppers-shears-h500.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>My new laptop for AI</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2025-12-24-my-new-ai-laptop.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>I found a laptop that I can use for my AI work even though I travel a lot.</p>
<section id="the-problem" class="level2">
<h2 class="anchored" data-anchor-id="the-problem">The problem</h2>
<p>I love to travel, and as an applied scientist, I get to travel a lot for work, visiting partners in the field, and presenting the work of the [AI for Good Lab]. In addition, I also love to travel for leisure.</p>
<p>Working with AI (training models or even doing local inference) meant having a “mobile workstation,” a very heavy laptop that can train models locally. For many years I used the Lenovo P-series, with my latest being a Lenovo P15 Gen2 that served me well since 2021.</p>
<p>The problem: it weighs 6 lbs (without counting the gigantic power brick) and it goes through its battery in about a couple of hours. Charging it is tricky, too - it does not charge from USB, and its power brick draws so much power that it prevents charging in most planes.</p>
<p>So I started looking for a lighter computer. Given my long-time loyalty to Lenovo, I started there, but I was quickly underwhelmed by the selection. The smaller and lighter Lenovo P14s felt underpowered, with Ada GPUs with only 4 Gb of VRAM, or Blackwells with 6 Gb of VRAM, which restrict the size of models I can use locally.</p>
<p>So, after many years of being a Lenovo-only household, I looked outside, and I found two laptops that I liked.</p>
</section>
<section id="my-requirements" class="level2">
<h2 class="anchored" data-anchor-id="my-requirements">My requirements</h2>
<ul>
<li><p>Large GPU VRAM for local AI work. 12-16 GB is the difference between “this is a fun demo” and “I can actually do something useful on the road.”</p></li>
<li><p>A truly portable chassis. I need a decent battery, and also a laptop that fits in the tray of an economy seat.</p></li>
<li><p>Practical charging. I wanted something that could top up over USB-C for “normal work” days (email, notebooks, writing, light coding), even if full GPU performance still needs the big brick. Also, it’s helpful to have some way to charge while I rest during a flight.</p></li>
</ul>
<p>Both the <a href="https://amzn.to/45s4gjh">ASUS ROG Zephyrus G14</a> and the <a href="https://amzn.to/4qlkGSx">Razer Blade 14</a> fit all those requirements. (These are affiliate links.)</p>
<ul>
<li><a href="https://rog.asus.com/laptops/rog-zephyrus/rog-zephyrus-g14-2025/">ASUS Zephyrugs G14 product page</a></li>
<li><a href="https://www.razer.com/gaming-laptops/razer-blade-14">Razer Blade 14 product page</a></li>
</ul>
<p>Review comparisons consistently show that the Zephyrus G14 delivers excellent real‑world performance for its price, great battery life, and an OLED display that delivers deep contrast and rich colors, which benefits both content creation and media consumption.</p>
<p>In contrast, the Razer Blade 14 leans more heavily into premium design, which gives it a slightly better thermal envelope for the GPU (increasing the performance a bit), and slightly higher refresh‑rate displays. However, this is currently limited to a RTX 5070 with 12 Gb of VRAM for $2,300. The same configuration for a Zephyrus G14 is $1,999. I was able to find a Zephyrus G14 with a RTX 5080 16 Gb VRAM for $2,550, $250 more than the Blade with a 12 Gb 5070, and I got it.</p>
</section>
<section id="early-real-world-notes" class="level2">
<h2 class="anchored" data-anchor-id="early-real-world-notes">Early “real world” notes</h2>
<p>A few things became clear almost immediately after switching away from the old Lenovo.</p>
<ul>
<li><p>USB-C charging is an incredible quality-of-life upgrade. These days, everything charges off USB-C, so even if the computer is charging a bit slower than the full power brick (200W for the power brick vs 100W for the USB-C), it still charges properly overnight. Better yet, I can charge it from my [power bank]</p></li>
<li><p>It fits an economy class tray with space for a drink (the laptop has to hang a little out)</p></li>
<li><p>It showed 4h30 hours of battery in a flight without having to adjust power configurations or dial down the OLED brightness (no GPU use). My flight was shorter, so I only used 2h.</p></li>
<li><p>It’s sufficiently powerful when connected to the power brick: the RTX 5080 seems to be equivalent to a V100, enough to run a lot of real-world workloads.</p></li>
</ul>
</section>
<section id="closing-thought" class="level2">
<h2 class="anchored" data-anchor-id="closing-thought">Closing thought</h2>
<p>I honestly expected Lenovo to have a better offer, and that would have saved me given the number of power bricks and docking stations that my family and I accumulated over the years. But 4 Gb VRAM is insuficcient these days for a small form-factor, and the larger form-factors are impractical. I’m glad Razer and ASUS have ultraportable powerful computers available.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2025-12-24-my-new-ai-laptop.html</guid>
  <pubDate>Wed, 24 Dec 2025 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/rog.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Warming up to information</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2025-02-08-warming-up-to-information copy.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>Information is powerful—especially when you have it before everyone else and know how to use it. In any market, whether it’s sports betting, stocks, or product pricing, being the first to know something can give you a huge edge.</p>
<section id="warming-up-to-information" class="level2">
<h2 class="anchored" data-anchor-id="warming-up-to-information">Warming up to information</h2>
<p>In February 2023, the Missouri Tigers basketball team (then 17–5) were heading to play the Mississippi State Bulldogs. On paper, the Tigers were the favorites, and the betting markets reflected that. However, the flight taking the Tigers’ players got delayed due to mechanical issues, which meant that they would not arrive in time to perform their warm-ups.</p>
<p>You likely don’t know this, but teams that don’t warm up usually lose their games. Of particular importance, at the time of this game, teams that didn’t warm up were 0-13 against covering their spreads. A spread is the margin by which the favored team is expected to win. Covering the spread means meeting or exceeding that margin if you’re the favorite, or staying within that margin (or winning) if you’re the underdog. For example, if a team is favored by 5 points and wins by 7, they have covered the spread. If they’re expected to lose by 5 points and lose by only 2 (or win), they have covered the spread.</p>
<p>Quickly after the flight delay announcement, large bets started pouring in for the underdog Mississippi State. Sportsbooks took notice and opened an investigation. The investigating company contacted the Missouri athletic director and found out that they were running late. The time between the announcement, the surge of bets, the investigation, and the Missouri athletic director receiving the call from the investigators? <strong>Six minutes.</strong></p>
<p>Mississippi State went on to win 63–52, covering both the first-half and full-game point spreads and confirming once again the importance of warm-ups. This travel hiccup turned the odds on their head—and turned out to be a winning bet for people who had the information early.</p>
<section id="information-in-markets" class="level3">
<h3 class="anchored" data-anchor-id="information-in-markets">Information in markets</h3>
<p>The same principle applies to many other markets—such as the stock market, elections, or even when it comes to convincing people to buy products. Whenever new information pops up, it can cause swift and significant changes. A company’s earnings, a major political event, or a sudden shift in supply can all move markets, public opinion, or consumer behavior almost instantly. Being the first to get reliable information (or interpret it correctly) often gives you a valuable advantage.</p>
<p>Large institutions have an advantage in the amount of data and tools (data science and AI) to comb through enormous amounts of information, spot unexpected changes, and react before the odds move too much. Because of this, smaller investors, consumers, and gamblers often find themselves at a disadvantage: they don’t have the same resources to identify and act on opportunities before the moment has passed. It’s about quickly processing real-time data—everything from flight schedules and injury updates to social media chatter. Professional market participants can do that. Regular people with a hunch are easy prey.</p>
<p>Can the underdog market participant ever win? The Tigers vs.&nbsp;Bulldogs case shows that it’s <em>possible</em>: the bets were allowed, and the fact that an investigation was opened indicates that the sports betting platforms didn’t know what was happening or how to take advantage of it.</p>
<p>For a whole six minutes.</p>
</section></section></article>


<section id="comments" class="level2 unlisted">
<h2 class="unlisted anchored" data-anchor-id="comments">Comments</h2>
<bluesky-comments-section post="https://bsky.app/profile/lucas.meyerperin.org/post/3lhqweomqsk2y"></bluesky-comments-section>


</section>

 ]]></description>
  <guid>https://meyerperin.org/posts/2025-02-08-warming-up-to-information copy.html</guid>
  <pubDate>Sat, 08 Feb 2025 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/warmup.webp" medium="image" type="image/webp"/>
</item>
<item>
  <title>Using Astral’s uv Pyhon packager in Azure Web Apps GitHub Actions</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2025-02-05-all-cool-kids-using-uv.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>I will eventually have to write a more detailed post about <a href="https://docs.astral.sh/uv/">Astral’s <code>uv</code></a>, a tool that all the cool kids are using.</p>
<p>In summary, it’s a single tool that replaces a lot of different tools that we use to develop and distribute Python software. It can replace <code>pip</code>, <code>pip-tools</code>, <code>pipx</code>, <code>poetry</code>, <code>pyenv</code>, <code>twine</code>, and <code>virtualenv</code>. It’s also very fast.</p>
<p>For now, I just want to show how to integrate <code>uv</code> in a GitHub Actions workflow that deploys to Azure Web Apps.</p>
<section id="a-typical-azure-web-apps-workflow" class="level2">
<h2 class="anchored" data-anchor-id="a-typical-azure-web-apps-workflow">A typical Azure Web Apps workflow</h2>
<p>Azure Web Apps know how to work with Python repositories that contain a <code>requirements.txt</code> file. If you’re building a Python app with Django, Flask or FastAPI and deploying it to an Azure Web App, you probably have a GitHub Actions workflow YAML that has a <strong>build</strong> step that looks like this, under the <code>.github/workflows</code> directory of your repository:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb1-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">build</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb1-2"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">runs-on</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> ubuntu-latest</span></span>
<span id="cb1-3"></span>
<span id="cb1-4"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">steps</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb1-5"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">uses</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> actions/checkout@v4</span></span>
<span id="cb1-6"></span>
<span id="cb1-7"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">name</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Set up Python version</span></span>
<span id="cb1-8"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">        </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">uses</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> actions/setup-python@v5</span></span>
<span id="cb1-9"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">        </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">with</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span></span>
<span id="cb1-10"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">          </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">python-version</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'3.12'</span></span>
<span id="cb1-11"></span>
<span id="cb1-12"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">name</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Create and start virtual environment</span></span>
<span id="cb1-13"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">        run</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">: </span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">|</span></span>
<span id="cb1-14">          python -m venv venv</span>
<span id="cb1-15">          source venv/bin/activate</span>
<span id="cb1-16"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span></span>
<span id="cb1-17"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">name</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Install dependencies</span></span>
<span id="cb1-18"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">        </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">run</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> pip install -r requirements.txt</span></span>
<span id="cb1-19"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">        </span></span>
<span id="cb1-20"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">name</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Zip artifact for deployment</span></span>
<span id="cb1-21"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">        </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">run</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> zip release.zip ./* -r</span></span></code></pre></div></div>
<p>As you can see, this workflow checks out the repository, sets up Python 3.12, creates a virtual environment, installs the dependencies as listed in <code>requirements.txt</code>, zips the artifact and uploads it to the GitHub Actions runner.</p>
<p>If you have been using <code>uv</code>, you know that you don’t need to manually create a virtual environment, and you don’t need to list your dependencies in a <code>requirements.txt</code> file. You <strong><em>can</em></strong> do this locally with <code>uv</code> by exporting the dependencies with <code>uv pip compile pyproject.toml -o requirements.txt</code>, but that would add an extra step before checking in your code. Instead, we’re going to do this directly in the GitHub Actions runner.</p>
</section>
<section id="replacing-the-virtual-environment-and-requirements.txt-steps-with-uv" class="level2">
<h2 class="anchored" data-anchor-id="replacing-the-virtual-environment-and-requirements.txt-steps-with-uv">Replacing the virtual environment and <code>requirements.txt</code> steps with <code>uv</code></h2>
<p>In order to use <code>uv</code> during the Azure build, you need to install <code>uv</code> in your GitHub Actions runner. You can delete the <code>Create and start virtual environment</code> and add the following code to install <code>uv</code>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb2-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">name</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Install uv</span></span>
<span id="cb2-2"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">        </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">uses</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> astral-sh/setup-uv@v5</span></span></code></pre></div></div>
<p>This will install <code>uv</code> in the GitHub Actions runner. The next step is to generate the requirements file. You can do this with the following command:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb3-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">      </span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">-</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">name</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> Generate requirements.txt</span></span>
<span id="cb3-2"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">        </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">run</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> uv pip compile pyproject.toml -o requirements.txt</span></span></code></pre></div></div>
<p>Interestingly, deleting the <code>Install dependencies</code> step seems to have no effect at this point. If you need the packages during the build step, for example to perform tests, you shouldn’t delete them, but apparently the <code>deploy</code> step of the GitHub Actions workflow will install the dependencies from the <code>requirements.txt</code> file even if you don’t install them explicitly in the <code>build</code> step.</p>
</section>
<section id="just-do-it" class="level2">
<h2 class="anchored" data-anchor-id="just-do-it">Just do it!</h2>
<p>I have been using <code>uv</code> for the past few months, and I can tell that my producitivity improved. It’s easier to manage packages, to maintain installations, to install <code>pytorch</code>, and a lot easier to perform tests using multiple versions of Python using <code>uv</code>.</p>
<p>The only thing that was holding me back on using <code>uv</code> more was the lack of native integration with Azure Web Apps, but now that I have a workaround, I can’t see myself going back to the old way of doing things.</p>
</section></article>

<section id="comments" class="level2 unlisted">
<h2 class="unlisted anchored" data-anchor-id="comments">Comments</h2>
<bluesky-comments-section post="https://bsky.app/profile/lucas.meyerperin.org/post/3lhhh2vddyc2f"></bluesky-comments-section>


</section>

 ]]></description>
  <guid>https://meyerperin.org/posts/2025-02-05-all-cool-kids-using-uv.html</guid>
  <pubDate>Wed, 05 Feb 2025 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/Astral.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Trip Report - Houston to New Orleans in a Kia EV9</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2025-01-04-ev9-trip-report.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>In December 2024, on Christmas day, I drove my Kia EV9 from Fulshear, a suburb west of Houston, to New Orleans, a 380-mile trip.</p>
<section id="range" class="level2">
<h2 class="anchored" data-anchor-id="range">Range</h2>
<p>My Kia EV9 is the Land long-range AWD trim, with a 99.8 kWh battery. The listed range is about 290 miles, but that assumes a mix of city and highway driving and some idealized conditions. This listed value is somewhat misleading in the real world for many reasons, one of which is that you don’t want to get to your destination with 0% battery: keeping a reserve charge is important to avoid being stranded due to unexpected delays, detours, or non-functional chargers. Therefore, you should count that the best you can consume on a trip is about 90%. Even if you were to take a 10% discount and use a range of 261 miles, you may find that a lot of the mental shortcuts that you could use for internal combustion engine (ICE) vehicles (gas-powered cars) don’t work well for electric vehicles (EVs).</p>
<p>One mental shortcut is that for long highway trips, ICE vehicles tend to go farther than their listed range. The listed range assumes some proportion of highway driving and some proportion of city driving. In ICE vehicles, the highway consumption is lower, so an ICE vehicle with a listed range of 261 miles will probably go farther than that. The opposite is true for EVs: at high speeds, consumption is higher than average, so the actual highway travel range of the vehicle is likely lower.</p>
<p>From my own measurements, my EV9 can do about 3.5 miles per kWh in the city (which would mean a 90% range of 315 miles) or about 2.7 miles driving at around 65 mph in a Houston highway (which would mean a 90% range of 253 miles). My planned interstate trip would have me driving at higher speeds, and therefore, my range would be even lower. I figured I’d need to charge twice unless I wanted to arrive almost empty.</p>
</section>
<section id="chargers-and-planners" class="level2">
<h2 class="anchored" data-anchor-id="chargers-and-planners">Chargers and Planners</h2>
<p>These days, most hotels in the South of the United States don’t have EV chargers (either slow or fast), and the city of New Orleans also doesn’t have many public chargers. New Orleans is very walkable, but we wanted to do some things that required driving. Luckily, one of the places we wanted to drive to (Audubon Zoo) had a free slow charger that charges at 6 kWh per hour, so we could leave the car there for a few hours and get a little bit of charge. I planned for a 50% charge on arrival and assumed I’d be able to charge 30% at the Audubon Zoo on the day we visited it.</p>
<p>Although I planned the whole route at home, I still had two concerns: one is that I hadn’t yet driven the Kia EV9 on a long trip, so I didn’t know what mileage I should use. The higher speeds I expected at the trip might lower my mileage substantially, but I didn’t know exactly by how much. The other is that I didn’t know which chargers would be working (a common problem these days), and whether they would be busy or not. With fast chargers spread far apart, a non-working charger could mean a long detour for which I might not even have enough charge to get to the next one.</p>
<p>For some vehicles, Apple Maps can help: the vehicle provides real time information to Apple Maps using CarPlay. However, the Kia EV9 does not support CarPlay yet, so I first tried using its built-in route planner, but it was frequently planning a route that used chargers that I suspected wouldn’t be working. To confirm my suspicions, I used the PlugShare app and found out that several chargers suggested by Kia were closed at night, had restrictions like “Rivian owners only,” or were out of service.</p>
<p>Searching online, I got the recommendation to use a range calculator called <a href="https://abetterrouteplanner.com/">A Better Route Planner</a>, usually abbreviated to ABRP. I installed it, and it asked me about my car model and trim and whether I had a NACS (Tesla) to CCS (legacy fast charger) adapter. I have an adapter that I use in my other EV (a Ford F-150), so said yes, and it told me that I would need two stops to get to New Orleans with 50% charge. It planned a route with an Electrify America (CCS) charger in Sulphur, LA, and a Tesla (NACS) charger in Baton Rouge, LA. The total distance was 400 miles, and the total time was about 7 hours, including charging stops. I subscribed to the premium version of the app, which allows you to see real-time data about your car and adjust for climate and traffic, which would help me adjust the route as the app gathered real-time information about actual mileage, traffic and temperature.</p>
</section>
<section id="a-worse-route-planner" class="level2">
<h2 class="anchored" data-anchor-id="a-worse-route-planner">A Worse Route Planner</h2>
<p>As we set out, I started measuring the real-time consumption. At about 75 mph, the actual consumption was about 2.4 miles per kWh, but at 80 mph, it was about 2.1 miles per kWh. At higher speeds, a lot of the advantages that make EVs energy efficient disappear. Even driving fast, we made it to Sulphur and quickly charged at Electrify America, reaching charging speeds of 200kW, which got us to 80% in about 20 minutes. Most fast chargers start charging slower when the battery state-of-charge (SoC) reaches 80%, so charging from 20% to 80% may take 20 minutes and charging from 80% to 90% may take an additional 20 minutes. Since the next planned stop in Baton Rouge was also a fast charger, to save trip time, I didn’t “top off” the car to 90% or 100%.</p>
<p>I got to Baton Rouge to a bad surprise. I did not know then, but even though the app said I could charge at the Tesla supercharger, it turns out that I can’t, even with the adapter. The reason is that Kia vehicles from charging in the Tesla superchargers until Jan 15th, 2025, even if you bring your own adapter. They can only charge in Tesla “magic chargers”, a Tesla charger that includes a Tesla-owned attached adapter, but these chargers are not available in all locations.</p>
<p>I then had two options: a detour to Electrify America in Hammond, LA, which meant a few extra miles, some extra time and reaching the charger with the battery at about 5% or keep going to a new group of Shell Recharge fast chargers at Tangers, LA, which was a lot closer to New Orleans. The problem: if that didn’t work, we would be far away from other chargers. I decided to take the risk.</p>
</section>
<section id="shell-doesnt-recharge" class="level2">
<h2 class="anchored" data-anchor-id="shell-doesnt-recharge">Shell (doesn’t) Recharge</h2>
<p>As it’s sometimes the case, the risk didn’t pay off. Upon arriving at the Shell Recharge station, I was required to install the Shell Recharge app to activate the charger. The process was cumbersome, involving a $75 hold on my credit card for each attempt. Despite multiple tries and remote support assistance, most chargers failed to work, and the one that did could only charge at the unusably slow speed of 1 kW, meaning that it would take about 75 hours to charge the car. In total, I got a hold of $750 on my card (released after a few days) and lost about one hour.</p>
<p>At that point, both Hammond and New Orleans were out of reach, and the teenagers in my car were hungry and cranky, so instead of having a Cajun dinner in New Orleans, we ended up going to the always reliable Waffle House, and I let my car charge for a couple hours in a slow ChargePoint charger, which gave me enough range to get somewhere. Bellies full, we decided that we would go to Hammond, use the reliable Electrify America and arrive with a charged car in New Orleans.</p>
</section>
<section id="new-orleans-chargers-and-the-trip-back" class="level2">
<h2 class="anchored" data-anchor-id="new-orleans-chargers-and-the-trip-back">New Orleans chargers and the trip back</h2>
<p>Although New Orleans downtown doesn’t have a lot of fast public chargers, the slow chargers I found were free, so I parked on them and recharged the car as we walked around at Audubon Zoo, or downtown while consuming beignets.</p>
<p>Lessons learned, I deleted ABRP, manually planned a route back using Electrify America chargers, and decided that my next trip with the Kia EV9 would be only after Jan 15th, 2025, when my Kia will have access to a much larger charging network.</p>
</section>
<section id="final-thoughts" class="level2">
<h2 class="anchored" data-anchor-id="final-thoughts">Final thoughts</h2>
<p>EVs are new, and we are having the usual growth pains for something that requires that much infrastructure. I was enormously happy that I could do my whole trip using my EV. At home, my EV is charged with solar power, so this means that the carbon signature of my trip was a lot lower than if I had driven a gas-powered car.</p>
<p>Even though the adapter mishap and the problems with broken chargers were stressful, I am really happy that I chose to exclusively use EVs. The significantly lower carbon emissions compared to gas-powered cars and the cost and emissions savings from charging with solar power at home reaffirmed my decision. These benefits far outweighed the challenges I faced during this trip. They worked well enough for what I needed, and I know how to make the next trip better and perhaps even stress-free: I’ll just wait a couple of weeks until both my EVs can use all fast-charging networks.</p>
</section></article>

<section id="comments" class="level2 unlisted">
<h2 class="unlisted anchored" data-anchor-id="comments">Comments</h2>
<bluesky-comments-section post="https://bsky.app/profile/lucas.meyerperin.org/post/3lf4662ovx22r"></bluesky-comments-section>


</section>

 ]]></description>
  <guid>https://meyerperin.org/posts/2025-01-04-ev9-trip-report.html</guid>
  <pubDate>Sat, 04 Jan 2025 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/kia-ev9.webp" medium="image" type="image/webp"/>
</item>
<item>
  <title>Understanding how LLMs work</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-12-09-understanding-llms.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>Large Language Models (LLMs) like ChatGPT sure look like magic. And, like magic, it may be super interesting to pull back the curtain and understand how the magic trick works. Stephen Wolfram wrote an introductory article on <a href="https://writings.stephenwolfram.com/2023/02/what-is-chatgpt-doing-and-why-does-it-work/">What Is ChatGPT Doing … and Why Does It Work?</a> in February 2023, just a few months after the release of ChatGPT. The article is also available as a <a href="https://amzn.to/4f3WcY4">book</a>. Even though there was a lot of evolution in the field of LLMs since then, the article is still a great introduction to the topic.</p>
<p>I love that article, and frequently point beginners to it. And they <strong>hate</strong> it, because it gets very technical very quickly. Therefore, I’ll try to summarize below the main ideas of that article in a way that is more accessible.</p>
<section id="its-just-adding-one-word-at-a-time" class="level2">
<h2 class="anchored" data-anchor-id="its-just-adding-one-word-at-a-time">It’s Just Adding One Word at a Time</h2>
<p>The main idea behind LLMs is that they generate text one word at a time. The model is trained to predict the next word in a sequence of words, given the previous words. To understand how a model can choose the words, you can think that it uses a list of words and the probabilities that they will come next. For example, for a phrase like “The best thing about AI is its ability to”, the list of probabilities for the next word might look like this:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Word</th>
<th>Probability</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>learn</td>
<td>4.5%</td>
</tr>
<tr class="even">
<td>predict</td>
<td>3.5%</td>
</tr>
<tr class="odd">
<td>make</td>
<td>3.2%</td>
</tr>
<tr class="even">
<td>understand</td>
<td>3.1%</td>
</tr>
<tr class="odd">
<td>do</td>
<td>2.9%</td>
</tr>
</tbody>
</table>
<p>These probabilities come from the corpus of text that the model was trained on. It is natural to think that the model should always choose the word with the highest probability, but if we always do that, we typically get a very boring output. Therefore, we allow the model to choose a word with a lower probability, which makes the output more interesting. The <strong>temperature</strong> parameter controls how “creative” the model is. The higher the temperature (a real number that goes from 0 to 1), the more likely the model is to choose lower-ranked words.</p>
<p>The model repeats this process for each word in the sequence, generating text one word at a time. For example, if the model generates the word “learn” as the next word, it then uses the phrase “The best thing about AI is its ability to learn” as the context for predicting the next word. This process continues until the model generates the desired amount of text.</p>
<p>Here are three completions I ran in ChatGPT for “The best thing about AI is its ability to”:</p>
<ul>
<li>learn, adapt, and solve complex problems, enhancing human potential and transforming industries with innovative solutions.</li>
<li>analyze vast amounts of data, uncover patterns, and provide insights that would otherwise be impossible for humans to achieve on their own.</li>
<li>continuously evolve and tackle diverse challenges, enabling more efficient, personalized, and creative solutions across countless domains.</li>
</ul>
</section>
<section id="where-the-probabilities-come-from" class="level2">
<h2 class="anchored" data-anchor-id="where-the-probabilities-come-from">Where the probabilities come from</h2>
<p>Although the probabilities do come from the corpus of text that the model was trained on, there’s no giant table with all possible combinations of words and their probabilities.</p>
<p>The reason is that the table would be too big, with a number of entries larger than all the atoms in the universe. Instead of having a table of probabilities, LLMs create a <strong>model</strong> based on a deep neural network. The model has a fixed size, called the number of <strong>parameters</strong>. Parameters are numbers that the model learns as it trains. Neural networks work with numbers, so the words are converted to numbers using a technique called embeddings. Embeddings are numerical representations of words done in such a way that similar words have similar embeddings, placing them close to each other. The image below shows embeddings for some words in a 2D space.</p>
<p><img src="https://meyerperin.org/images/embeddings.png" class="img-fluid"></p>
<p>The model shown a sequence of words with a word obscured. It then tries to predict the obscured word’s embedding. The input is converted to numbers, and the numbers are modified (through multiplications and sums) by the parameters of the model. The training program knows the right answer, and if the model gets close to the correct answer, the parameters are reinforced. If the model is far from the correct answer, the parameters are adjusted. This process is repeated trillions of times, with the model getting better and better at predicting the next embedding in a sequence. Once the model is good enough, you can stop training it and start using it to generate text.</p>
</section>
<section id="it-cant-be-that-simple" class="level2">
<h2 class="anchored" data-anchor-id="it-cant-be-that-simple">It can’t be that simple</h2>
<p>At this point, you may be thinking — it can’t be that simple. And you are right. There are a lot of other details that I intentionally skipped to make this post as simple as possible for a beginner. The descriptions above apply to earlier versions of large language models such as GPT-2, back when you could run such a model on your own computer. These earlier models created interesting human-like outputs but they didn’t seem like magic. Today, models are trained on a lot more data, and require a lot more computational power to train. The original GPT-2 had 1.5 billion parameters. The recently released <a href="https://www.llama.com/docs/model-cards-and-prompt-formats/llama3_3/">Llama 3.3</a> has 70 billion parameters.</p>
<p>Another change from these original large language models was the introduction of Reinforcement Learning from Human Feedback (RLHF) training, a way to train a model to generate text that is more likely to be useful to humans. This was done by showing humans the text generated by an LLM and asking them to rate it. The generated text and the ratings are then used to retrain the model to generate better text. This can be done in an external model that interacts with the LLM or you can simply allow the model to update itself. This idea is generally known as <strong>fine-tuning</strong> and an early version that performed extremely well was implemented in <a href="https://openai.com/index/instruction-following/">InstructGPT</a>.</p>
</section>
<section id="where-to-go-from-here" class="level2">
<h2 class="anchored" data-anchor-id="where-to-go-from-here">Where to go from here</h2>
<p>I hope this post helps you understand a little better how LLMs work. If you want to go deeper, I recommend reading the <a href="https://writings.stephenwolfram.com/2023/02/what-is-chatgpt-doing-and-why-does-it-work/">original article by Stephen Wolfram</a>.</p>
</section></article>

<section id="comments" class="level2 unlisted">
<h2 class="unlisted anchored" data-anchor-id="comments">Comments</h2>
<bluesky-comments-section post="https://bsky.app/profile/lucas.meyerperin.org/post/3lcvhb4hc2s2v"></bluesky-comments-section>


</section>

 ]]></description>
  <guid>https://meyerperin.org/posts/2024-12-09-understanding-llms.html</guid>
  <pubDate>Mon, 09 Dec 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/wolfram_llms.png" medium="image" type="image/png" height="88" width="144"/>
</item>
<item>
  <title>Using Bluesky to add comments to my blog</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-12-02-blog-comments.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>Does anybody still blog? I am of the opinion that if you are in a a position of technical leadership, <a href="../posts/2019-02-13-why-you-should-blog-if-you-are-a-data-scientist.html">you should have a blog</a>, and the main reason is <strong>not</strong> because it helps you promote your work, but because you will forget all the stuff you did. Egotistically, if you write about it, you can remember <strong>what</strong> you did, and most importantly, <strong>how</strong>.</p>
<p>Writing things as explanations to other people also helps you understand them better. And besides helping yourself, you can help a lot of other people, if someone besides you ever reads your blog.</p>
<section id="quarto-and-bluesky-comments" class="level2">
<h2 class="anchored" data-anchor-id="quarto-and-bluesky-comments">Quarto and Bluesky comments</h2>
<p>I use <a href="https://quarto.org">Quarto</a> to write my blog. It’s free, allows me to write in Markdown and write customizations in Python, and I can host my blog for free on GitHub or Netlify (I chose Netlify). One perpetual annoyance for bloggers is to enable comments. People who use the popular WordPress platform can use a plugin, but there’s a lot of spam.</p>
<p>Recently, <a href="https://bsky.app/profile/grrrck.xyz">Garrick Aden-Buie</a> created a plugin that allows you do combine two actions: promoting your blog post on Bluesky and using Bluesky to manage comments.</p>
<p>His full solution is on <a href="https://pkg.garrickadenbuie.com/quarto-bluesky-comments/">his website</a>, and it’s very simple to use, so I’ll just describe it here.</p>
</section>
<section id="enabling-comments-and-a-chicken-and-egg-problem" class="level2">
<h2 class="anchored" data-anchor-id="enabling-comments-and-a-chicken-and-egg-problem">Enabling comments and a chicken and egg problem</h2>
<p>The first step is to install the Quarto extension. To do so, assuming you have Quarto installed, you can simply run the following command:</p>
<p><code>quarto add gadenbuie/quarto-bluesky-comments</code></p>
<p>You can publish your posts normally. To add the Bluesky comment functionality, you first need to share the post to Bluesky, to get the post (Bluesky posts are called skeets) ID. For example, I wrote my <a href="../posts/2024-11-27-bird-buddy.html">post about Birdbuddy</a> and shared it to Bluesky in the following skeet: <a href="https://bsky.app/profile/lucasmeyer.bsky.social/post/3lbxfiyp7jc2a">https://bsky.app/profile/lucasmeyer.bsky.social/post/3lbxfiyp7jc2a</a>.</p>
<p>Once you have the post/skeet URL, you have to go back to your blog post, ad add the following code at the end of the post <strong>and republish it</strong> (this is the chicken and egg problem):</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" data-shortcodes="false" style="background: #f1f3f5;"><pre class="sourceCode markdown code-with-copy"><code class="sourceCode markdown"><span id="cb1-1">{{&lt; bluesky-comments skeet-url &gt;}}</span></code></pre></div></div>
<p>To make my blog post look pretty, I added a heading, so the final code for the Bluesky post would look like this:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" data-shortcodes="false" style="background: #f1f3f5;"><pre class="sourceCode markdown code-with-copy"><code class="sourceCode markdown"><span id="cb2-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">## Comments {.unlisted}</span></span>
<span id="cb2-2"></span>
<span id="cb2-3">{{&lt; bluesky-comments https://bsky.app/profile/lucasmeyer.bsky.social/post/3lbxfiyp7jc2a &gt;}}</span></code></pre></div></div>
<p>Now comments to the Bluesky post appear as comments on the blog.</p>
</section></article>
<p>I think this is great, because I would have promoted the post to Bluesky anyway, and since I always have to fix at list one error in the blog post, I would also end up republishing it, so I’m kinda getting the comments for free.</p>

<section id="comments" class="level2 unlisted">
<h2 class="unlisted anchored" data-anchor-id="comments">Comments</h2>
<bluesky-comments-section post="https://bsky.app/profile/lucas.meyerperin.org/post/3lce4em3ors2b"></bluesky-comments-section>


</section>

 ]]></description>
  <guid>https://meyerperin.org/posts/2024-12-02-blog-comments.html</guid>
  <pubDate>Mon, 02 Dec 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/blog_comments.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Automatic posting from my Birdbuddy to Threads and Bluesky</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-11-27-bird-buddy.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>I <strong>love</strong> my Birdbuddy. The Birdbuddy is a <strong>camera trap</strong> for birds. It’s a bird feeder that senses when something is in front of it, records a movie and takes several pictures. It then uses AI to identify the animal (usually a bird, but sometimes a squirrel) and sends me a notification on my phone, containing a “postcard”, a gallery with the video and some of the pictures it took.</p>
<p>Whenever I realize that the Birdbuddy captured a great picture, I post it to social media, usually to Threads and Bluesky, but there’s a lot of friction in the process. I must download the pictures from the Birdbuddy app, then upload them to the social media app, write a caption, etc. So I decided to automate the process using AI and the Threads and Bluesky APIs.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>The code below is a simplified version of what was actually deployed. The real code is at <a href="https://github.com/MeyerPerin-Foundation/thread-manager">this GitHub repository</a>, and uses more sophisticated prompting and error handling.</p>
</div>
</div>
<section id="gathering-the-postcards" class="level2">
<h2 class="anchored" data-anchor-id="gathering-the-postcards">Gathering the postcards</h2>
<p>The first step is to gather the postcards. Birdbuddy doesn’t really have a public API, but it uses GraphQL, and people have reverse-engineered the interface and created a Python library called <code>pybirdbuddy</code> that can download the postcards from the Bird Buddy app.</p>
<p>The code below shows how to get the pictures from the postcards that were collected in the last 24 hours:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1">    bb <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> BirdBuddy(BIRD_BUDDY_USER, BIRD_BUDDY_PASSWORD)</span>
<span id="cb1-2"></span>
<span id="cb1-3">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># get the last day</span></span>
<span id="cb1-4">    since <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> datetime.datetime.now() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> datetime.timedelta(hours<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">24</span>)    </span>
<span id="cb1-5">    postcards <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> bb.refresh_feed(since<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>since)</span>
<span id="cb1-6"></span>
<span id="cb1-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> card <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> postcards:</span>
<span id="cb1-8">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> card.get(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'__typename'</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"FeedItemNewPostcard"</span>:</span>
<span id="cb1-9">            sighting <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> bb.sighting_from_postcard(card.get(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'id'</span>)) </span>
<span id="cb1-10">            species <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> sighting.report.sightings[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>].species.name</span>
<span id="cb1-11"></span>
<span id="cb1-12">            media_items <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [{<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'id'</span>: item[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'id'</span>], </span>
<span id="cb1-13">                            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'date_created'</span>: item[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'createdAt'</span>], </span>
<span id="cb1-14">                            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'species'</span>: species,</span>
<span id="cb1-15">                            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'media_type'</span>: item[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'__typename'</span>], </span>
<span id="cb1-16">                            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'image_url'</span>: item[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'contentUrl'</span>]} </span>
<span id="cb1-17">                            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> item <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> sigthing.medias </span>
<span id="cb1-18">                            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> item[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'__typename'</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'MediaImage'</span>]</span></code></pre></div></div>
<p>The downside is that this will download all the pictures, and many of the pictures are not very good. I want to filter them out automatically.</p>
</section>
<section id="is-this-picture-any-good" class="level2">
<h2 class="anchored" data-anchor-id="is-this-picture-any-good">Is this picture any good?</h2>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://meyerperin.org/images/bird2.jpg" class="img-fluid figure-img" width="500"></p>
<figcaption>Cardinal</figcaption>
</figure>
</div>
<p>To filter the pictures, I use the GPT-4o model with a simple prompt. Here’s the Python code that I use:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"></span>
<span id="cb2-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> good_bird(image_url):</span>
<span id="cb2-3"></span>
<span id="cb2-4">    openai_client <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> OpenAI(api_key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>app_config.OPENAI_API_KEY)</span>
<span id="cb2-5">    prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"I want to post cute and interesting images of birds to social media.</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb2-6">    prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Is this image such a picture? Reply 'Yes' if it is good, otherwise 'No'."</span></span>
<span id="cb2-7"></span>
<span id="cb2-8">    response <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> openai_client.chat.completions.create(</span>
<span id="cb2-9">        model<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gpt-4o"</span>, </span>
<span id="cb2-10">        messages<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[</span>
<span id="cb2-11">            {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"role"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"system"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"content"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"You are a photography critic and social media content creator"</span>},</span>
<span id="cb2-12">            {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"role"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"user"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"content"</span>: [{<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"type"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"text"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"text"</span>: prompt}, </span>
<span id="cb2-13">            {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"type"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"image_url"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"image_url"</span>: {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"url"</span>: image_url}}]}],)</span>
<span id="cb2-14"></span>
<span id="cb2-15">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'yes'</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> response.choices[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>].message.content.lower()</span></code></pre></div></div>
<p>Running the list of <code>media_items</code> through the <code>good_bird</code> function, I can filter out the pictures that are not good enough to post.</p>
<p>I also use GPT-4o to generate a caption:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> caption(species, image_url):</span>
<span id="cb3-2"></span>
<span id="cb3-3">    openai_client <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> OpenAI(api_key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>app_config.OPENAI_API_KEY)</span>
<span id="cb3-4">    </span>
<span id="cb3-5">    prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Generate a caption for this </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>species<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> that was captured on a bird feeder camera.</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb3-6">    prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Do not assume the bird's gender.</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb3-7">    prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"The caption will be used in a social media post and should be less than 200 characters.</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb3-8">    prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"The caption should be suitable for a professional brand, although it can be funny.</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb3-9">    prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Do not use emojis or hashtags.  Do not ask for engagement. Do not ask questions.</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb3-10"></span>
<span id="cb3-11">    response <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> openai_client.chat.completions.create(</span>
<span id="cb3-12">        model<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gpt-4o"</span>, </span>
<span id="cb3-13">        messages<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[</span>
<span id="cb3-14">            {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"role"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"system"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"content"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"You are a photographer and social media content creator"</span>},</span>
<span id="cb3-15">            {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"role"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"user"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"content"</span>: [{<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"type"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"text"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"text"</span>: prompt}, </span>
<span id="cb3-16">            {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"type"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"image_url"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"image_url"</span>: {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"url"</span>: image_url}}]}],)</span>
<span id="cb3-17"></span>
<span id="cb3-18">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> response.choices[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>].message.content</span></code></pre></div></div>
<p>Now each image has a caption and a flag indicating if it’s good enough to post. In my app, I’m saving all the pictures to a database and posting a few of them every day, but you could post it directly from the code above.</p>
</section>
<section id="posting-to-threads-and-bluesky" class="level2">
<h2 class="anchored" data-anchor-id="posting-to-threads-and-bluesky">Posting to Threads and Bluesky</h2>
<p>To post the pictures, I use the Threads and Bluesky APIs.</p>
<p>The code below shows how to post a picture to Threads. Posting to Threads requires two api calls - one to an endpoint called <code>threads</code> to create the post and upload the assets another to an endpoint called <code>publish</code> to actually publish the post. You need a token and a user id to post to Threads, and you can obtain both from the Meta Developers website. Please note that the code below is a simplified version of the actual code, and you should handle errors and exceptions properly.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"></span>
<span id="cb4-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> post_to_threads(message, image):</span>
<span id="cb4-3">    payload <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {</span>
<span id="cb4-4">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"access_token"</span>: THREADS_TOKEN,</span>
<span id="cb4-5">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"text"</span>: message,</span>
<span id="cb4-6">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"media_type"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"IMAGE"</span>,</span>
<span id="cb4-7">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"image_url"</span>: image</span>
<span id="cb4-8">    }</span>
<span id="cb4-9"></span>
<span id="cb4-10">    post_url <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"https://graph.threads.net/v1.0/</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>THREADS_USER_ID<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">/threads/"</span></span>
<span id="cb4-11"></span>
<span id="cb4-12">    response <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> requests.post(post_url, json<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>payload)</span>
<span id="cb4-13"></span>
<span id="cb4-14">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> response.status_code <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">200</span>:</span>
<span id="cb4-15">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># get response id</span></span>
<span id="cb4-16">        response_json <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> response.json()</span>
<span id="cb4-17">        creation_id <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> response_json[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"id"</span>]</span>
<span id="cb4-18">    </span>
<span id="cb4-19">        publish_payload <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {</span>
<span id="cb4-20">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"access_token"</span>: THREADS_TOKEN,</span>
<span id="cb4-21">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"creation_id"</span>: creation_id</span>
<span id="cb4-22">        }</span>
<span id="cb4-23"></span>
<span id="cb4-24">        publish_url <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"https://graph.threads.net/v1.0/</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>app_config<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>THREADS_USER_ID<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">/threads_publish/"</span></span>
<span id="cb4-25"></span>
<span id="cb4-26">    response <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> requests.post(publish_url, json<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>publish_payload)</span></code></pre></div></div>
<p>Posting to Bluesky is similar, but a lot easier. You must have created an app password, your account password can’t be used with the API. The code below shows how to post to Bluesky. In order to post to Bluesky, you should install the package <code>atproto</code>, which allows you to import a Bluesky <code>Client</code> class.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1"></span>
<span id="cb5-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> post_to_bluesky(message, image, image_alt_text):</span>
<span id="cb5-3">    client <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Client()</span>
<span id="cb5-4">    client.login(BSKY_USER, BSKY_APP_PWD)</span>
<span id="cb5-5"></span>
<span id="cb5-6">    image_data <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> requests.get(image).content</span>
<span id="cb5-7">    client.send_image(message, image<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>image_data, image_alt<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>image_alt_text)</span></code></pre></div></div>
</section>
<section id="want-to-see-the-posts" class="level2">
<h2 class="anchored" data-anchor-id="want-to-see-the-posts">Want to see the posts?</h2>
<p>You can see my Birdbuddy posts on <a href="https://bsky.app/profile/lucas.meyerperin.org">Bluesky</a>. They are all tagged with <code>#birds</code>. I hope you enjoy them!</p>
</section>
<section id="buying-your-own-birdbuddy" class="level2">
<h2 class="anchored" data-anchor-id="buying-your-own-birdbuddy">Buying your own Birdbuddy</h2>
<p>Right now, I own two Birdbuddies, the original version in my backyard and a Pro version in my front yard, and I’m likely to buy the hummingbird one soon.</p>
<p>If you want to buy your own Birdbuddy, you can find it on <a href="https://amzn.to/4iv2cfB">Amazon</a>.</p>
</section></article>

<section id="comments" class="level2 unlisted">
<h2 class="unlisted anchored" data-anchor-id="comments">Comments</h2>
<bluesky-comments-section post="https://bsky.app/profile/lucas.meyerperin.org/post/3lbxfiyp7jc2a"></bluesky-comments-section>


</section>

 ]]></description>
  <guid>https://meyerperin.org/posts/2024-11-27-bird-buddy.html</guid>
  <pubDate>Wed, 27 Nov 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/bird.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>The Yes-Damn Effect and the No-Yay Effect</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-03-20-yes-damn-no-yay.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>You know the feeling. You said yes to something, and now it’s time to do it. But instead of feeling excited, you feel dread. This actually has a name: it’s called the “Yes-Damn Effect” and it’s the subject of lots of memes, usually titled “When you agreed to go out and the time to go out is getting closer.”</p>
<center>
<img src="https://meyerperin.org/images/yesdamn.jpg" class="img-fluid" style="width:50.0%">
</center>
<p>The Yes-Damn Effect is usually caused by dynamic inconsistency: the “you” that needs to do the thing is not the same as the “you” that agreed to do the thing.</p>
<p>You probably know this well: it’s what happens when you go to the supermarket when you’re hungry and buy a bunch of junk food, and then when you’re home and not hungry, you regret it.</p>
<p>As every software developer knows, we tend to overestimate how much time our future selves will have, and underestimate how long the things we currently have on our plates will take, and how many random things will pop up between now and then.</p>
<p>So even if we were able to estimate correctly the benefit of the thing we agreed to do, we tend to underestimate the cost.</p>
<p>Damn, indeed.</p>
<p>It can be even worse: sometimes we overestimate the benefits, too. But let’s not even dwell on that, because there’s a cool technique that I want to tell you about. It’s called the “No-Yay Effect.”</p>
<center>
<img src="https://meyerperin.org/images/agree-to-go-out.jpg" class="img-fluid" style="width:50.0%">
</center>
<p>As you become more experienced, you learn that you need to say “no” sometimes. You can’t do everything, and you can’t do everything well. But saying “no” is hard. You immediately see the disappointment of the person you’re saying “no” to, but you may not see the benefit to you.</p>
<p>So here’s a cool trick.</p>
<p>When you say “no” to something, put an empty reminder on your calendar for the time when you would have done the thing. A very simple way is to put a 30-minute meeting with yourself on the day that the request was due. I usually mark that time as “free”, so it doesn’t actually block my calendar, but I make sure to turn on the reminder.</p>
<p>This will start to give you some positive reinforcement for saying “no” as you’ll start to see the benefit of the time you’ve saved. It’s not uncommon that one such reminder will pop up in my calendar when I’m really busy and then I think “Yay, I’m glad I said no to that thing.”</p>
<p>Yay, indeed.</p>
<p>Over time, this positive reinforcement may make it easier for you to say “no” to things. And you may be happier for it.</p>
</article>



 ]]></description>
  <guid>https://meyerperin.org/posts/2024-03-20-yes-damn-no-yay.html</guid>
  <pubDate>Wed, 20 Mar 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/noyay.png" medium="image" type="image/png" height="144" width="144"/>
</item>
<item>
  <title>Luddites and the AI Revolution</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-03-06-luddites.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>These days, calling someone a “Luddite” is an insult, a little bit like “boomer”. But the original Luddites were not exactly afraid of technology — they were afraid of <em>losing their jobs</em> to technology. They were skilled workers who saw their livelihoods threatened by machines that could do their jobs faster and cheaper. Sounds familiar?</p>
<section id="croppers" class="level2">
<h2 class="anchored" data-anchor-id="croppers">Croppers</h2>
<p>A central figure of the Luddite movement was the “cropper”, a skilled worker who used large shears to finish cloth pieces. The shears were heavy (40 lbs) and unwieldly, and using them well was tiring and painful, requiring a lot of training. Over time, croppers developed a large callus on their forearm (called a saddle), and it was said at the time that “you could tell a cropper by his saddle”.</p>
<center>
<img src="https://meyerperin.org/images/croppers-shears-h500.jpg" class="img-fluid">
</center>
<p>&nbsp;</p>
<p>A good finish could mean the difference between a piece of cloth that would sell for a high price and one that would need to be thrown away. Given their importance, the training requirements, and the physical toll of the job, croppers were well-paid, high-status workers. But then, in the early 1800s, some machines were invented that could do the job of the croppers as well as the croppers themselves, but also faster and cheaper.</p>
<p>The conversation below was reported by the nineteenth century historian Frank Peel, who recorded oral stories handed down over generations in his book <a href="https://amzn.to/3uZdx3r">The Rising of the Luddites</a>.</p>
</section>
<section id="the-conversation" class="level2">
<h2 class="anchored" data-anchor-id="the-conversation">The Conversation</h2>
<p>In a cropping shop, a group of men are talking after work. Most of the men are croppers, and recently started forming gangs to smash the new machines. One of men is John Booth, a nineteen-year-old apprentice saddle maker, the son of a clergyman with the Church of England. Since he is not a cropper, he’s not directly affected by the new machines. Not yet, anyway.</p>
<p>“Would it not be better,” asks Booth, “to reason with them rather than infuriate them by destroying their machines?”</p>
<p>“Reason with them!?,” says one of the croppers, “We might as well reason with a stone!”</p>
<p>The cropper is named George Mellor, and he’s the local leader of a masked marauding gang that destroys the machines that are replacing workers. Mellor is still working for now, but he knows that he can’t compete with the machines. He and his fellow croppers can see that the skill they’ve spent years perfecting is soon to be rendered worthless.</p>
<center>
<img src="https://meyerperin.org/images/cropper-example.jpg" class="img-fluid">
</center>
<p>&nbsp;</p>
<p>Booth clearly thought this out, and says that “being a cropper is hard work, and painful too, until you develop that hard, calloused skin on the wrist. But have you seen those machines in action? You just have to set up the cloth and keep an eye on them. They take away all of that hard, painful work. Seen like that, the machinery is a thing of beauty.”</p>
<p>Booth continues, “I quite agree with you about the problem, and respect the harm your livelihood suffers from the machinery. But such a machine might be man’s chief <strong>blessing</strong> instead of his curse. You can’t say the machine itself is evil, that’s absurd! No, the problem is that the <em>mill owner</em> gets all the benefits. If society was differently constituted, if those benefits were fairly shared out…”</p>
<p>Mellor interrupts him. “If, if, if…” he says. “What’s the use of such sermons as thine to starving men?”</p>
<p>England is at war with Napoleon’s France, and the war has disrupted trade. Nobody’s hiring, and the price of food has shot up. Another cropper joins in the conversation. He tells how he went to see a former workmate, a cropper who lost his job. He can’t find work. The man’s been struggling to afford to eat, and now his wife has died. “There she lay on the bed, poor thing, skin and bone.”</p>
<p>It’s hard for John Booth to argue with that. He is sure that the machines would be a blessing if society were differently constituted. On the other hand, he also knows that his cropper friends are right. There’s no chance of reorganizing society at present, and there’s no comfort in imagining a different societal organization when you are starving to death.</p>
<p>“What can I say?” asks Booth.</p>
<p>“Say you’ll join us,” says Mellor.</p>
</section>
<section id="the-luddites-and-general-ludd" class="level2">
<h2 class="anchored" data-anchor-id="the-luddites-and-general-ludd">The Luddites and General Ludd</h2>
<p>The gangs were not a single, unified group. They were a loose collection of people who were angry at the new machines and the mill owners who were using them.</p>
<center>
<img src="https://meyerperin.org/images/Luddite.jpg" class="img-fluid" style="width:70.0%">
</center>
<p>&nbsp;</p>
<p>There was a local story that a young weaving apprentice named Ned Ludd smashed two machines in a fit of rage. When gangs started smashing machines, the story goes, instead of snitching on them, the other workers would say that “Ned Ludd did it”. Over time, the gangs incorporated that legend, and said that they were acting on behalf of General Ludd. Incidentally, General Ludd was supposed to live in Sherwood Forest, same as Robin Hood. Before striking, the gangs would usually send letters to the mill owners, ordering them to stop using the machines otherwise they would be smashed. The letters were signed “General Ludd”. The followers started to be called Luddites.</p>
<p>For a while, the Luddites were successful. Mill owners were concerned, and some stopped using machines. However, a few mill owners held out. One of the holdouts was William Cartwright, the owner of Rawfolds Mill. On April 11th, 1812, more than than one hundred Luddites gathered at the Dumb Steeple, still a landmark near Mirfield, in Yorkshire. At the dead of night, they marched on Rawfolds Mill. But Cartwright was ready for them. The mill was fortified and soldiers were stationed there. The Luddites were beaten off and two of them were killed.</p>
<p>One the dead was the nineteen-year old John Booth, the young apprentice saddle maker who had been trying to reason with the croppers. Legend says that before Booth died, a reverend tried to extract from him the name of the local leader of the Luddites under the seal of the confessional.</p>
<p>Booth asked the reverend, “Can you keep a secret?” The reverend said that he could. “So can I,” said Booth, and died.</p>
</section>
<section id="the-mill-owners-problem" class="level2">
<h2 class="anchored" data-anchor-id="the-mill-owners-problem">The Mill Owner’s Problem</h2>
<p>It’s easy to see the mill owners as the villains of the story. They were the ones who were buying and using the machines that were putting the croppers out of work. They were the ones that put soldiers to fight against the common man. But the mill owners had a problem too. Machines were making the production of cloth much cheaper. If they didn’t use the machines, they would be outcompeted by other mill owners who did.</p>
<p>Even if the Luddites were successful across the whole kingdom of England, a different country could organize itself to start using the machines. If that happened, the mill owners would still go out of business, and all of their workers would be out of work, including the croppers.</p>
</section>
<section id="the-luddites-and-the-ai-revolution" class="level2">
<h2 class="anchored" data-anchor-id="the-luddites-and-the-ai-revolution">The Luddites and the AI Revolution</h2>
<p>John Booth’s argument was on point. Centuries later, the economists Daron Acemoglu and Simon Johnson wrote the book <a href="https://amzn.to/3T2hGf3">Power and Progress</a>, in which the central argument is not very different from the one in John Booth’s conversation. “Progress depends on the choices we make about technology. New ways of organizing production and communication can either serve the narrow interests of an elite or become the foundation for widespread prosperity”.</p>
<p>In the book, they provide several other examples spanning over a thousand years. The examples include resource allocations that did not work, like the agricultural innovations in the middle ages that led to larger and more opulent cathedrals. They also include allocations that did work, like the assembly automation in the 20th century that led to the rise of the middle class and the increased prosperity of the United States post World War II.</p>
<p>Now, with AI, we again have a technology that can do the job of many workers faster and cheaper. Will we, as a society, do better this time?</p>
</section>
<section id="what-the-future-holds" class="level2">
<h2 class="anchored" data-anchor-id="what-the-future-holds">What the future holds</h2>
<p>It seems inevitable that AI will be incorporated more and more in the workplace and in day-to-day life, to the extent that it may be pointless to try to stop it. Several countries already have defined strategies to incorporate AI in their economies, so even if a fight against AI is successful in one country, that victory may translate into a much bigger loss. Avoiding AI is unlikely to be a long-term winning strategy.</p>
<p>When people ask me about AI, I frequently tell them to start using it in their day-to-day, to understand its limitations and its potential. I suggest that the get a suscription like OpenAI’s ChatGPT or Bing Copilot and attempt to use it extensively, for every thing they can think of. In most cases, it will not work, or it will not work well, but the point is to try and learn.</p>
<p>I especially do this with my young kids, as I imagine that they will go into a workforce that will probably use AI <strong>a lot</strong>. I certainly don’t know what careers will exist in the future and how they are going to be affected by AI, but I want them to be very familiar with the usefulness and limitations of AI so that they are well-equipped to make the best decisions for themselves.</p>
<p>As a society, it’s beyond time to start thinking about the allocations of the gains and losses. One thing is clear: if nothing changes, the losses will be allocated to workers that have their job automated. Even at the time of the Luddites, there were already proposals to allocate resources to retrain workers that were losing their jobs to machines. These proposals ended up not being implemented.</p>
<p>But since the time of the Luddites, we have accumulated a lot of knowledge, and we have also improved our communication infrastructure. We can share stories like this one with thousands of people. People can find training online. People have access to cheap AI, even free. We are much better equipped this time around.</p>
<p>Hopefully, this time, we can do better.</p>
</section>
<section id="references" class="level2">
<h2 class="anchored" data-anchor-id="references">References</h2>
<p>The main source for this article was the excellent Cautionary Tales podcast episode by Tim Harford, <a href="https://www.pushkin.fm/podcasts/cautionary-tales/general-ludds-rage-against-the-machines">General Ludd’s Rage Against the Machines</a>. I’ve also used the following sources:</p>
<ul>
<li><a href="https://amzn.to/3uZdx3r">The Rising of the Luddites</a></li>
<li><a href="https://freakonomics.com/podcast/daron-acemoglu-on-economics-politics-and-power/">Daron Acemoglu interview in “People I Mostly Admire</a></li>
<li><a href="https://amzn.to/3T2hGf3">Power and Progress</a></li>
<li><a href="https://www.ludditelink.org.uk/documents.php">The Luddite Link</a></li>
</ul>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2024-03-06-luddites.html</guid>
  <pubDate>Wed, 06 Mar 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/Luddite.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>The Social Costs of Killing Vultures</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-02-13-vultures.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>In 1993, the patent of a painkiller called diclofenac expired and it started to be used in livestock. At about the same time, the population of vultures in India started to decline. Recently, researchers from UChicago linked these two events and showed that this resulted in approximately 100k additional HUMAN deaths per year. How?</p>
<p>It was not known then, but diclofenac is extremely toxic to vultures. Animals treated with diclofenac pose no risk to humans, but even trace amounts in an animal carcass cause fatal kidney failure in vultures. Therefore, the population of vultures in India fell by over 95%.</p>
<p>Vultures are extremely efficient cleaners - a group can consume a carcass of a large animal in less than one hour. They usually compete with dogs and rats, who are not efficient cleaners. Without vultures, carcasses are more likely to rot in the open, potentially increasing water pollution and supporting more dogs that can carry rabies.</p>
<p>Because of the quick demise and large impact of vultures in sanitation, the researchers were able to use a Differences-in-Differences quasi-experimental design. One thing that helped a lot was that vultures were present in some regions but not others, naturally creating a treatment group (going from lots of vultures to no vultures) and a control group (no change).</p>
<p>After the vultures’ demise, the death rates in these regions started to decline more slowly than regions that never had vultures.</p>
<p>This is a very interesting paper that can help you learn a lot about experiment design and how sustainability and economics are sometimes closely related.</p>
<p>You can find the full paper by Eyal Frank and Anant Sudarshan in the link below.</p>
<p><a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4318579" class="uri">https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4318579</a></p>
</article>



 ]]></description>
  <guid>https://meyerperin.org/posts/2024-02-13-vultures.html</guid>
  <pubDate>Tue, 13 Feb 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/dr-vulture.png" medium="image" type="image/png" height="144" width="144"/>
</item>
<item>
  <title>ADHD Treatment — 4-week update</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-02-10-adhd-treatment-4-week.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>In the beginning of 2024, I have been <a href="../posts/2024-01-04-adhd.html">diagnosed with ADHD</a>. Since then, I started <a href="../posts/2024-01-19-adhd-treatment.html">treatment with medicine and therapy</a>.</p>
<p>Over time, I <a href="../posts/2024-02-02-adhd-coping-technologies.html">acquired a few technologies</a> and developed a <a href="../posts/2024-02-03-adhd-coping-strategies.html">few strategies</a> to cope with ADHD.</p>
<p>This is my 4-week update on my treatment.</p>
<section id="week-checkpoint" class="level2">
<h2 class="anchored" data-anchor-id="week-checkpoint">4-week checkpoint</h2>
<p>The doctor and I agreed to run the treatment for about four weeks, after which I should check back for any adjustments. I agreed to document how things were going to make the adjustments easier.</p>
<p>For the first four weeks, I diligently took the prescribed Ritalin dose, usually at 8 AM. As you would expect with someone with ADHD, I forgot to take the medicine a few times. When that happened, I usually remembered before 11 AM. During the 28 days, I completely forgot one time, took it by 11 AM five times, and took it at 1 PM one time.</p>
<p>I noticed a few things when taking the medicine. The first is that I flush a little. When I looked at online forums like Reddit, I found that this seems to be a common side effect that disappears over time. The second is that I usually feel <em>wiped out</em> at the end of the day, even more than usual. Interestingly, this helped me sleep better.</p>
<p>The other side-effect we talked about was insomnia. Taking the medicine at 11 AM didn’t seem to make any difference in my tiredness or productivity, but on the day I took the medicine at 1 PM, I was awake until 1 AM, which is unusual for me (I’m usually up early at around 6 AM). Insomnia is a known side-effect of stimulants, and definitely something to monitor. After that experience, I decided to completely skip the medicine for days I forget to take it before 11 AM.</p>
<p>Because I keep track of almost every one of my tasks in a <a href="../posts/2024-02-02-adhd-coping-technologies.html#microsoft-to-do">to-do list</a>, I was able to notice that there was some small improvement of my symptoms, but it was minimal. The main improvement was that I was able to tackle some tasks that were “annoying but easy”, like scheduling someone to clean the dryer vents of my house. Incidentally, <a href="https://www.thumbtack.com">Thumbtack</a> (no affiliation) has been pretty good for scheduling those “annoying but easy” home maintenance tasks. As for the other symptoms, my overall levels of forgetfulness and fidgetry didn’t seem to change.</p>
<p>I met with my doctor again, and she suggested increasing the dose to 20 mg of long-acting. which is the default dose for most people. I will start with this new dose on Monday. The long acting version is supposed to work for 8-12 hours.</p>
</section>
<section id="therapy" class="level2">
<h2 class="anchored" data-anchor-id="therapy">Therapy</h2>
<p>The therapist I’m working with can only meet every other week. I think that now, in the beginning, it would be beneficial for me to meet more frequently, especially as try to recast a lot of the things that have been problematic for me over my life under the new lens provided by a diagnosis. For example, sometimes I would think that I was not interested in some things, but it was just a period of my life where a lot was going on and I was easily distracted. We are still trying to find out a schedule that works well for both of us.</p>
</section>
<section id="verdict" class="level2">
<h2 class="anchored" data-anchor-id="verdict">Verdict</h2>
<p>I think the initial doses of therapy and medicine were not sufficient for improving the symptoms substantially, but at least the side-effects haven’t been bad. Now that the medicine dose has increased, I plan to increase the frequency of my therapy and document my progress after three months.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2024-02-10-adhd-treatment-4-week.html</guid>
  <pubDate>Sat, 10 Feb 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/adhd-4-week.png" medium="image" type="image/png" height="144" width="144"/>
</item>
<item>
  <title>Strategies I use to cope with ADHD</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-02-03-adhd-coping-strategies.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>In the beginning of 2024, I have been <a href="../posts/2024-01-04-adhd.html">diagnosed with ADHD</a>. Since then, I started <a href="../posts/2024-01-19-adhd-treatment.html">treatment with medicine and therapy</a>. I have also written a <a href="../posts/2024-02-10-adhd-treatment-4-week.html">4-week update on my progress</a> and I plan to continue documenting it.</p>
<p>Over time, I <a href="../posts/2024-02-02-adhd-coping-technologies.html">acquired a few technologies</a> and developed a few strategies to cope with ADHD.</p>
<p>They are listed below, and I plan to keep updating them as I find new ways to deal with ADHD, mostly through therapy.</p>
<section id="multiple-open-projects" class="level2">
<h2 class="anchored" data-anchor-id="multiple-open-projects">Multiple open projects</h2>
<p>As counterintuitive as it may seem, I usually have multiple projects going on at the same time. The way this works is that when I’m working on a project and get distracted, the distraction usually comes from another project (like an email or a meeting), so I switch to that project. This way, I’m still being productive.</p>
</section>
<section id="log-activities-as-i-start-them" class="level2">
<h2 class="anchored" data-anchor-id="log-activities-as-i-start-them">Log activities as I start them</h2>
<p>I got the habit (or routine) of logging most activities as I start executing them. This helps me when I eventually get distracted, because I can go back to the log and see what I was doing before I got distracted. I use Siri and Microsoft To-Do to log activities, as described in the Coping Technologies section.</p>
</section>
<section id="plan-activities-i-dont-want-to-start" class="level2">
<h2 class="anchored" data-anchor-id="plan-activities-i-dont-want-to-start">Plan activities I don’t want to start</h2>
<p>I learned this technique recently. When I realize that I haven’t been productive and my day is a “bad day”, instead of trying to concentrate and do more, I go into planning mode. I essentially give up trying to do things, I start just planning and preparing things. If I’m trying to download 100 images and keep getting distracted, I would instead just list the websites that I want to download the images from, create the the Azure storage to receive it, etc. I may call this <strong>productive avoidance</strong>.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2024-02-03-adhd-coping-strategies.html</guid>
  <pubDate>Sat, 03 Feb 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/adhd-strategies.png" medium="image" type="image/png" height="141" width="144"/>
</item>
<item>
  <title>Technnologies I use to cope with ADHD</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-02-02-adhd-coping-technologies.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>In the beginning of 2024, I have been <a href="../posts/2024-01-04-adhd.html">diagnosed with ADHD</a>. Since then, I started <a href="../posts/2024-01-19-adhd-treatment.html">treatment with medicine and therapy</a>. I also wrote a <a href="../posts/2024-02-10-adhd-treatment-4-week.html">4-week update</a> about my treatment.</p>
<p>Over time, I developed a <a href="../posts/2024-02-03-adhd-coping-strategies.html">few strategies</a> to cope with ADHD.</p>
<p>Since I work with technology, I have also acquired several products and services that help me cope. They are listed below.</p>
<section id="microsoft-to-do" class="level2">
<h2 class="anchored" data-anchor-id="microsoft-to-do">Microsoft To-Do</h2>
<p>I use <a href="https://l.meyerperin.com/todo">Microsoft To-Do</a> to keep track of tasks. I like it because it’s <strong>very</strong> simple, it’s multiplatform, so I can see and check tasks from my phone, my computer and my watch. Since it integrates well with Siri, I can add tasks simply by saying ‘Hey Siri, remind me to X’, which helps me keep track of things. I specifically add tasks to my to-do list as I’m starting them, because that will help me remember to finish if I get distracted.</p>
</section>
<section id="apple-airtags" class="level2">
<h2 class="anchored" data-anchor-id="apple-airtags">Apple AirTags</h2>
<p>I have <a href="https://l.meyerperin.com/airtags">Apple AirTags</a> on everything since I lose something almost daily. The new AirTags have a feature called Precision Finding that allows me to find things a lot faster, and I use it a lot.</p>
</section>
<section id="wallet-with-key-ring" class="level2">
<h2 class="anchored" data-anchor-id="wallet-with-key-ring">Wallet with Key Ring</h2>
<p>I bought a <a href="https://l.meyerperin.com/keyring_wallet">wallet with an attached keyring</a> that allows me to carry my badge, car and house keys, wallet, and an AirTag all together, making it hard for me to lose things.</p>
</section>
<section id="s-biner-carabiners" class="level2">
<h2 class="anchored" data-anchor-id="s-biner-carabiners">S-biner carabiners</h2>
<p>I have a lot of <a href="https://l.meyerperin.com/sbiner">S-biner carabiners</a>. I use them to attach things to my backpack and to each other, so I have only one bundle of items to keep track of.</p>
</section>
<section id="ray-ban-meta-sunglasses" class="level2">
<h2 class="anchored" data-anchor-id="ray-ban-meta-sunglasses">Ray-Ban Meta Sunglasses</h2>
<p>The <a href="https://l.meyerperin.com/rayban_meta">Ray-Ban Meta</a> works as sunglasses, a headset and as reading glasses. It also has a camera, which I use quite a lot, and an AI that I don’t use as much yet.</p>
</section>
<section id="kindle-scribe" class="level2">
<h2 class="anchored" data-anchor-id="kindle-scribe">Kindle Scribe</h2>
<p>I always carry a <a href="../posts/2024-01-12-kindle-scribe.html">Kindle Scribe</a>. I had (and lost) many notebooks and loose sheets of paper. The Kindle Scribe is an e-ink notebook that feels like a real notebook, but has a lot of capacity. I can also carry academic papers on it. I used to have a reMarkable 2 but I changed to the Kindle Scribe because it solves a few more of my use cases.</p>
</section>
<section id="pillbox" class="level2">
<h2 class="anchored" data-anchor-id="pillbox">Pillbox</h2>
<p>I use a <a href="https://l.meyerperin.com/pillbox">pillbox</a> to organize my vitamins and medications. I usually forget whether I took the medication or not a few hours after the time I was supposed to take it, so I use the pillbox to keep track of whether I took it or not.</p>
</section>
<section id="coping-strategies" class="level2">
<h2 class="anchored" data-anchor-id="coping-strategies">Coping Strategies</h2>
<p>I also have a few coping strategies that I use to manage my symptoms. I will list them below:</p>
<section id="multiple-open-projects" class="level3">
<h3 class="anchored" data-anchor-id="multiple-open-projects">Multiple open projects</h3>
<p>As counterintuitive as it may seem, I usually have multiple projects going on at the same time. The way this works is that when I’m working on a project and get distracted, the distraction usually comes from another project (like an email or a meeting), so I switch to that project. This way, I’m still being productive.</p>
</section>
<section id="log-activities-as-i-start-them" class="level3">
<h3 class="anchored" data-anchor-id="log-activities-as-i-start-them">Log activities as I start them</h3>
<p>I got the habit (or routine) of logging most activities as I start executing them. This helps me when I eventually get distracted, because I can go back to the log and see what I was doing before I got distracted. I use Siri and Microsoft To-Do to log activities, as described in the Coping Technologies section.</p>
</section>
<section id="plan-activities-i-dont-want-to-start" class="level3">
<h3 class="anchored" data-anchor-id="plan-activities-i-dont-want-to-start">Plan activities I don’t want to start</h3>
<p>I learned this technique recently. When I realize that I haven’t been productive and my day is a “bad day”, instead of trying to concentrate and do more, I go into planning mode. I essentially give up trying to do things, I start just planning and preparing things. If I’m trying to download 100 images and keep getting distracted, I would instead just list the websites that I want to download the images from, create the the Azure storage to receive it, etc. I may call this <strong>productive avoidance</strong>.</p>
</section></section></article>





 ]]></description>
  <guid>https://meyerperin.org/posts/2024-02-02-adhd-coping-technologies.html</guid>
  <pubDate>Fri, 02 Feb 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/adhd-tech.png" medium="image" type="image/png" height="144" width="144"/>
</item>
<item>
  <title>Running multiple OpenAI requests concurrently with Python’s asyncio</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-02-01-openai-concurrency.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>It’s possible that you have multiple OpenAI requests that you want to run concurrently. For example, you want to process several documents at the same time, so that you can finish processing them faster. You can do so by using Python’s <code>asyncio</code> library.</p>
<section id="prerequisites" class="level2">
<h2 class="anchored" data-anchor-id="prerequisites">Prerequisites</h2>
<p>To run the code in this article, you need to have Python <strong>3.11</strong> installed. This will enable you to use the <code>TaskGroup</code> functionality. You also need to have the <code>openai</code> Python package installed, version 1.0 or higher.</p>
<p>In my case, I have two Azure OpenAI subscriptions, one in the US East region and another in the US West region. Therefore, I have two API keys, one for each region. To make things more interesting, I’ll assume that each region has different models, <code>gpt-4</code> in US East and <code>gpt-4-turbo</code> in US West.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>This code also works if you have only one subscription, and it’s also faster when run concurrently. One potential issue with that approach is that the OpenAI API has <a href="https://platform.openai.com/docs/guides/rate-limits">rate limits</a> per account, so if you run multiple requests concurrently, you may hit the rate limit faster.</p>
</div>
</div>
<p>To test concurrency, I will ask GPT to perform a task that takes about 30 seconds to complete, such as generating a five-paragraph story about a theme. I have four themes, “dog”, “cat”, “chicken”, and “tiger”. I will ask GPT to generate stories about “dog” and “cat” concurrently, and then “chicken” and “tiger” will be generated concurrently. I will time how long it took to generate the stories concurrently, and I will also measure the time to generate the stories sequentially, so that I can compare the results.</p>
</section>
<section id="the-code" class="level2">
<h2 class="anchored" data-anchor-id="the-code">The code</h2>
<p>First, let’s import the required libraries.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> os</span>
<span id="cb1-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> openai <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> AsyncAzureOpenAI</span>
<span id="cb1-3"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> dotenv <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> load_dotenv</span>
<span id="cb1-4"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> asyncio</span>
<span id="cb1-5"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> timeit <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> default_timer <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> timer</span></code></pre></div></div>
<p>Note that I’m importing the <code>AsyncAzureOpenAI</code> class from the <code>openai</code> package. This class is used to call the OpenAI API asynchronously. I’m also importing the <code>load_dotenv</code> function from the <code>dotenv</code> package, which is used to load environment variables from a <code>.env</code> file, where my subscription keys and endpoints are stored.</p>
<p>The concurrency is managed by the <code>asyncio</code> library. I’m also importing the <code>timer</code> function from the <code>timeit</code> package to measure the time it takes to generate the stories.</p>
<p>Now, let’s define the function that will call the OpenAI API to generate the stories.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">async</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> call_openai(client, <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">id</span>, model, theme, answers):</span>
<span id="cb2-2">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span> (<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Generating a story about </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>theme<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> using </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">id</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">."</span>)</span>
<span id="cb2-3">    response <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> client.chat.completions.create(</span>
<span id="cb2-4">        model<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>model,</span>
<span id="cb2-5">        messages<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[</span>
<span id="cb2-6">            {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"role"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"system"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"content"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"You are a helpful assistant."</span>},</span>
<span id="cb2-7">            {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"role"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"user"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"content"</span>: <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Generate a five-paragraph story about a </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>theme<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">."</span>},</span>
<span id="cb2-8">        ]</span>
<span id="cb2-9">    )</span>
<span id="cb2-10">    answers.append(response.choices[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>].message.content)</span>
<span id="cb2-11">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span> (<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Generated a story about </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>theme<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> using </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">id</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">."</span>)</span></code></pre></div></div>
<p>The function is a straightforward call to the <code>chat.completions.create</code> method of the <code>AsyncAzureOpenAI</code> class. Note that I’m requesting a five-paragraph story about a theme. I chose that prompt because it takes about 30 seconds to complete, so even if GPT randomly generates a shorter story, we should still be able to see the differences. The response is appended to the <code>answers</code> list (not shown).</p>
<p>Let’s now see the main body of the function.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">async</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> main():</span>
<span id="cb3-2">    load_dotenv()</span>
<span id="cb3-3">    client1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> AsyncAzureOpenAI(</span>
<span id="cb3-4">        api_key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>os.getenv(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"USEAST_KEY"</span>),  </span>
<span id="cb3-5">        api_version<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"2023-12-01-preview"</span>,</span>
<span id="cb3-6">        azure_endpoint <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> os.getenv(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"USEAST_ENDPOINT"</span>)</span>
<span id="cb3-7">    )</span>
<span id="cb3-8"></span>
<span id="cb3-9">    client2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> AsyncAzureOpenAI(</span>
<span id="cb3-10">        api_key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>os.getenv(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"USWEST_KEY"</span>),  </span>
<span id="cb3-11">        api_version<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"2023-12-01-preview"</span>,</span>
<span id="cb3-12">        azure_endpoint <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> os.getenv(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"USWEST_ENDPOINT"</span>)</span>
<span id="cb3-13">    )</span>
<span id="cb3-14"></span>
<span id="cb3-15">    themes <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dog"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"cat"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"chicken"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"tiger"</span>]</span>
<span id="cb3-16">    i <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb3-17">    model1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gpt-4"</span></span>
<span id="cb3-18">    model2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gpt-4-turbo"</span></span>
<span id="cb3-19">    id1 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"client-1"</span></span>
<span id="cb3-20">    id2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"client-2"</span></span></code></pre></div></div>
<p>In the code above, I creaate two separate OpenAI clients, one for each region. I also define the themes for which I want to generate stories, and the models that I want to use. I also define the IDs for each client, so that I can identify which client generated each story.</p>
</section>
<section id="calling-the-functions-sequentially" class="level2">
<h2 class="anchored" data-anchor-id="calling-the-functions-sequentially">Calling the functions sequentially</h2>
<p>Now let’s call the functions sequentially, so that I can establish the baseline.</p>
<p>The code below calls the <code>call_openai</code> function (that we defined above) sequentially, and measures the time it takes to generate the stories. The code is still inside the <code>main</code> function, which is called at the end of the script.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1">    answers_s <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb4-2">    start <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> timer()</span>
<span id="cb4-3">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Run sequentially</span></span>
<span id="cb4-4">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(themes), <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>):</span>
<span id="cb4-5">        start_step <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> timer()</span>
<span id="cb4-6">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> call_openai(client1, id1, model1, themes[i], answers_s)</span>
<span id="cb4-7">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> call_openai(client2, id2, model2, themes[i<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>], answers_s)</span>
<span id="cb4-8">        end_step <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> timer()</span>
<span id="cb4-9">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Finished generating stories about a </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>themes[i]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> and a </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>themes[i<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> sequentially in </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>end_step <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> start_step<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.2f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> seconds."</span>)</span>
<span id="cb4-10">    end <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> timer()</span>
<span id="cb4-11">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Generated stories sequentially in </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>end <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> start<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.2f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> seconds."</span>)</span></code></pre></div></div>
<section id="sequential-results" class="level3">
<h3 class="anchored" data-anchor-id="sequential-results">Sequential results</h3>
<p>The output of the code above is as follows.</p>
<pre><code>Generating a story about dog using client-1.
Generated a story about dog using client-1.
Generating a story about cat using client-2.
Generated a story about cat using client-2.
Finished generating stories about a dog and a cat sequentially in 85.34 seconds.
Generating a story about chicken using client-1.
Generated a story about chicken using client-1.
Generating a story about tiger using client-2.
Generated a story about tiger using client-2.
Finished generating stories about a chicken and a tiger sequentially in 57.66 seconds.
Generated stories sequentially in 143.00 seconds.</code></pre>
<p>It took 143 seconds to generate the stories sequentially. If you run this code, you will see that it starts the “dog” story, then it finishes, then it starts the “cat” story, then it finishes, and so on sequentially, as we would expect.</p>
<p>Now let’s see what happens when we run these functions concurrently.</p>
</section>
</section>
<section id="calling-the-functions-concurrently" class="level2">
<h2 class="anchored" data-anchor-id="calling-the-functions-concurrently">Calling the functions concurrently</h2>
<p>Here, I will use the <code>asyncio.TaskGroup</code> class to run the <code>call_openai</code> function concurrently. This is still inside the <code>main</code> function.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb6-1">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Run concurrently</span></span>
<span id="cb6-2">    answers_c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb6-3">    start <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> timer()</span>
<span id="cb6-4">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(themes), <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>):</span>
<span id="cb6-5">        start_step <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> timer()</span>
<span id="cb6-6">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">async</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">with</span> asyncio.TaskGroup() <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> tg:</span>
<span id="cb6-7">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Started generating stories about </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>themes[i]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> and </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>themes[i<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> concurrently."</span>)</span>
<span id="cb6-8">            tg.create_task(call_openai(client1, id1, model1, themes[i], answers_c))</span>
<span id="cb6-9">            tg.create_task(call_openai(client2, id2, model2, themes[i<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>], answers_c))</span>
<span id="cb6-10">        end_step <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> timer()</span>
<span id="cb6-11">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Finished generating stories about a </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>themes[i]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> and a </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>themes[i<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> concurrently in </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>end_step <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> start_step<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.2f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> seconds."</span>)</span>
<span id="cb6-12">    end <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> timer()</span>
<span id="cb6-13">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"Generated stories concurrently in </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>end <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> start<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.2f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> seconds.</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<p>The <code>TaskGroup</code> class is a context manager that allows you to run multiple tasks concurrently. You can start asynchronous tasks with the <code>create_task</code> method, and the context manager will wait for all tasks to complete before exiting. In my case, since I have two clients, I will start two tasks in each iteration of the loop.</p>
<p>Let’s see the results.</p>
<section id="concurrent-results" class="level3">
<h3 class="anchored" data-anchor-id="concurrent-results">Concurrent results</h3>
<p>The output of the code above is as follows.</p>
<pre><code>Generating a story about dog using client-1.
Generating a story about cat using client-2.
Generated a story about cat using client-2.
Generated a story about dog using client-1.
Finished generating stories about a dog and a cat concurrently in 34.89 seconds.
Started generating stories about chicken and tiger concurrently.
Generating a story about chicken using client-1.
Generating a story about tiger using client-2.
Generated a story about chicken using client-1.
Generated a story about tiger using client-2.
Finished generating stories about a chicken and a tiger concurrently in 30.25 seconds.
Generated stories concurrently in 65.14 seconds.</code></pre>
<p>You can now see that it took 65 seconds to generate the stories concurrently. This is about half the time it took to generate the stories sequentially. You can also see that the “dog” and “cat” stories started at the same time, and the “chicken” and “tiger” stories also started at the same time, as we would expect.</p>
<p>The code below is required just to start the whole process.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb8-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">__name__</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"__main__"</span>:</span>
<span id="cb8-2">    asyncio.run(main())</span></code></pre></div></div>
</section>
</section>
<section id="what-if-you-have-just-one-subscription" class="level2">
<h2 class="anchored" data-anchor-id="what-if-you-have-just-one-subscription">What if you have just one subscription?</h2>
<p>The code above also works if you only have one subscription. For example, if you were to replace the <code>client1</code> with <code>client2</code> everywhere in the code above, you would still be able to run the tasks concurrently. The only difference is that you would be using the same subscription for both tasks, so you would be consuming your rate limits faster. However, you would still be able to run the tasks concurrently, and you would still see a significant improvement in the time it takes to generate the results.</p>
</section>
<section id="conclusion" class="level2">
<h2 class="anchored" data-anchor-id="conclusion">Conclusion</h2>
<p>Running AI models concurrently can significantly reduce the time it takes to generate the results. This is especially important when you have to generate a large number of results, or when the results take a long time to generate. In this case, we saw that running the models concurrently with double the resources, as expected, took about half the time. This is a significant improvement, and it can make a big difference in practice.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2024-02-01-openai-concurrency.html</guid>
  <pubDate>Thu, 01 Feb 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/code-concurrency.webp" medium="image" type="image/webp"/>
</item>
<item>
  <title>Books to Help Teenagers Fall in Love with Science</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-01-25-science-books.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>Here are some books that talk about science and technology in fun ways, and have been very successful with my teenagers.</p>
<p>The books are ordered roughly in the order of preference of my teens. The one they liked the most (and re-read all the time) is Humble Pi, by Matt Parker. This is the very top of their list. Even the book on the bottom of this list, The Martian, is still a great book that they loved. I, too, read and enjoyed all of them.</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Title</th>
<th>Author</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><a href="https://amzn.to/48NQxTw">Humble Pi</a></td>
<td>Matt Parker</td>
<td>A funny book about math mistakes. Their favorite.</td>
</tr>
<tr class="even">
<td><a href="https://amzn.to/42bacdR">What If?</a></td>
<td>Randall Munroe</td>
<td>Precise scientific answers to absurd questions.</td>
</tr>
<tr class="odd">
<td><a href="https://amzn.to/3Sa52Kx">What If? 2</a></td>
<td>Randall Munroe</td>
<td>Precise scientific answers to even more absurd questions.</td>
</tr>
<tr class="even">
<td><a href="https://amzn.to/4bdCxUG">Soonish</a></td>
<td>Kelly and Zach Weinersmith</td>
<td>A funny book about the future of technology.</td>
</tr>
<tr class="odd">
<td><a href="https://amzn.to/4b90GMi">Fuzz</a></td>
<td>Mary Roach</td>
<td>A funny book about the science of animal-human interactions.</td>
</tr>
<tr class="even">
<td><a href="https://amzn.to/3vSbLRS">How to Fake a Moon Landing</a></td>
<td>Darryl Cunningham</td>
<td>A graphic novel about science denial.</td>
</tr>
<tr class="odd">
<td><a href="https://amzn.to/4931MYJ">How To</a></td>
<td>Randall Munroe</td>
<td>A funny book about how to do things in extremely difficult ways.</td>
</tr>
<tr class="even">
<td><a href="https://amzn.to/47Kxp7T">Hollywood Wants to Kill You</a></td>
<td>Dr.&nbsp;Michael Brooks and Rick Edwards</td>
<td>A funny and informative way of learning science through movies about extinguishing the human race.</td>
</tr>
<tr class="odd">
<td><a href="https://amzn.to/47QhULD">The Martian</a></td>
<td>Andy Weir</td>
<td>Every problem has a solution, but every solution creates a new problem. Will he survive at the end?</td>
</tr>
</tbody>
</table>
<p>I hope this helps you find great reads.</p>
</article>



 ]]></description>
  <guid>https://meyerperin.org/posts/2024-01-25-science-books.html</guid>
  <pubDate>Thu, 25 Jan 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/teen-books.png" medium="image" type="image/png" height="144" width="144"/>
</item>
<item>
  <title>Starting ADHD treatment</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-01-19-adhd-treatment.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>On January 19th, 2024, I started treatment for ADHD. If you want to read more about how I got my diagnosis, you can read my previous post about <a href="../posts/2024-01-04-adhd.html">getting Diagnosed with ADHD</a>.</p>
<p>Over time, I <a href="../posts/2024-02-02-adhd-coping-technologies.html">acquired a few technologies</a> and developed a <a href="../posts/2024-02-03-adhd-coping-strategies.html">few strategies</a> to cope with ADHD.</p>
<section id="the-first-week-jan-12---jan-19" class="level2">
<h2 class="anchored" data-anchor-id="the-first-week-jan-12---jan-19">The first week (Jan 12 - Jan 19)</h2>
<p>Switching from a diagnosis to a treatment was harder than I expected. After I finished the diagnosis, my results were immediately sent the results to my primary care provider (PCP). A few days went by and nothing happened. I emailed my PCP’s care coordinator, and she asked me to send a copy of the diagnosis to her. I did, and again nothing else happened for a few days. Then I found an opening in my primary care provider’s calendar and booked it for next day. My PCP had not yet received the results from her own coordinator, so I printed a copy and brought to her.</p>
<section id="medicine" class="level3">
<h3 class="anchored" data-anchor-id="medicine">Medicine</h3>
<p>The PCP and I had a 20-minute meeting about the results and the treatment options. She suggested a stimulant from three options: Adderall (amphetamine), Ritalin (methylphenidate), and Strattera (Atomoxetine). For no specific reason, I was worried about habit-forming, which I had read that could be a problem with Ritalin and Adderall. I asked about Strattera first, but she told me that it only work for about 50% of the people, whereas Ritalin and Adderall tend to work for 70-80%, and that the risk of addiction is very low at the low dosages used in ADHD treatment. She also told me that the most common side effects are loss of appetite and insomnia, but that they usually go away after a few weeks.</p>
<p>She suggested I try Ritalin first, and prescribed me 10mg of the extended release version. She told me to take them in the morning, becase it is a stimulant and it could cause me to lose sleep if I took it too late in the day.</p>
<p>I got my prescription filled that same day and took my first dose at about 11 AM. The day seemed to progress as usual, with me getting distracted at about the usual amount and using all of my coping strategies to manage. According to my primary care provider, the initial dosage of 10mg is probably not even therapeutic and used more to see if I will have any side effects, but we’ll meet again in 2-3 weeks to review.</p>
</section>
<section id="therapy" class="level3">
<h3 class="anchored" data-anchor-id="therapy">Therapy</h3>
<p>In addition to Ritalin, I also started therapy with a provider that specializes in ADHD. I was actually supposed to start it a week before the medicine, but the therapist had to reschedule because she was sick. I had my first session with her today, and it was mostly an introduction to the process of therapy.</p>
<p>She asked me if I had therapy before, and I told her that I was switching therapists, as the therapist I used until November last year retired. She asked about my family history (dad left my life early, mom probably neurodivergent), whether I had a difficult childhood, and about my social life. Apparently, social media doesn’t count as much for a social life, but it’s better than nothing. She also screened me for anxiety and depression (negative for both). Then, she asked about my goals.</p>
<p>I told her that my main goal is to get better tools to manage being overwhelmed, which happens occasionally. The feeling of being overwhelmed happens when I have too many things to do and I don’t know where to start, so I end up doing nothing. This usually results in some things being late, which leads to me being more overwhelmed. Another common problem is being distracted by something and forgetting to do something important. I told her that I have a lot of coping strategies, but I’m not sure if they are the best ones, and I would like to learn more.</p>
<p>We decided on a bi-weekly rythm, and I will have my next session in two weeks.</p>
</section>
</section>
<section id="how-are-things-going-so-far" class="level2">
<h2 class="anchored" data-anchor-id="how-are-things-going-so-far">How are things going so far?</h2>
<p>Getting diagnosed is helpful, because now I understand many of the things that happen, even though I still have trouble controlling them. As of now, the dosage of the medicine is probably too low to make a difference, but I noticed that I am getting more tired than usual at night and sleeping better. My level of distraction and being overwhelmed don’t seem to have improved substantially.</p>
<p>The post above is about my first week of treatment. I also documented the <a href="../posts/2024-02-10-adhd-treatment-4-week.html">first four weeks</a> of treatment.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2024-01-19-adhd-treatment.html</guid>
  <pubDate>Fri, 19 Jan 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/adhd_treatment.png" medium="image" type="image/png" height="144" width="144"/>
</item>
<item>
  <title>Is a PhD worth it?</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-01-17-phd-for-job.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>“Is a PhD worth it?” is a question that I get quite a lot on LinkedIn.</p>
<p>If you do an online search or ask ChatGPT about it, you will get a lot of answers, but most people find those answers unsatisfactory. A lot of the problem is with the question, as it’s a little bit like asking whether “is it worth it to travel to Seattle?” It depends on you, on the timing, and on your objectives. Most of the text below is about the experience from a PhD in the US, but some of it is applicable to other countries.</p>
<section id="phd-fields-are-vastly-different" class="level2">
<h2 class="anchored" data-anchor-id="phd-fields-are-vastly-different">PhD fields are vastly different</h2>
<p>You may have talked to someone or heard their experience with a PhD, and you may have heard that it was a great experience, or that it was a terrible experience. Before you incorporate that information into your decision, you need to know what the person’s field was.</p>
<p>Although there are some similarities between PhD fields, in practice the careers in different fields are vastly different. The differences are not only between fields, but also between countries, universities, departments, and even specific topics. Someone’s experience with a PhD in English is going to be vastly different from someone’s experience with a PhD in Finance or Accounting. Even within the same field there are differences. The experience of someone in Finance — Investments is going to be different from the experience from someone in Finance — Real Estate. The supply and demand for professionals in different fields varies a lot. Therefore, when you are looking for opinions, it’s important to talk to people that at least have done a PhD in a similar subject as you are considering, and as close as possible to what you are actually considering. At a high level, different fields can be thought as completely different careers. Asking an English PhD about a PhD in Finance is like asking a poet about a career in accounting.</p>
<p>Some fields have a robust market in the industry, but for some others, the market is mostly limited to academia. For example, PhDs in Finance, Accounting, Economics, Statistics, Computer Science, and Engineering have a robust market in the industry. On the other hand, the industry market for PhDs in English, History, and Philosophy is very limited. This is not to say that there are no jobs, or that the PhD won’t be helpful to get a specific job, but that the PhD is usually not a requirement for industry jobs in these subjects and it’s unlikely to help in the average case. In addition, some careers are more careful about admitting PhD students, and only admit a small number that will be employable later (that tends to be the case with Finance and Accounting). Some other fields admit as many students as possible, which can cause the market to become oversupplied.</p>
</section>
<section id="what-is-your-objective" class="level2">
<h2 class="anchored" data-anchor-id="what-is-your-objective">What is your objective?</h2>
<p>If you worked with me on anything, you know that this is a question you hear a lot from me. You need to know what you’re trying to accomplish before you can be helped.</p>
<p>If you want to work as a researcher in a research university, you need a PhD, and usually a PhD from a well-regarded university. For most other cases, a PhD is not a hard requirement. There are some positions in large companies that require it, usually research positions, and there are some positions for which it is strongly desired. In some cases, requirements make little sense: I’ve seen someone with a Ph.D.&nbsp;in Philosophy from a mid-tier university get an interview for a software development position that required a PhD, but another person with a Master’s in Computer Science from a top university not get an interview for the same position because they didn’t have a PhD, even though the person without the PhD had a lot of experience in the subject and in the area. These cases are uncommon. One example of an industry position that often requires PhDs is the Applied Scientist position at Amazon, although if I recall correctly, it’s not a hard requirement for all teams.</p>
<p>If you don’t want to work in a position that requires a PhD, your question is probably about the overall value of the PhD, as in “is it worth the investment?” Most PhDs are funded with a stipend that is enough to live on, but it’s not a lot of money. This makes getting a PhD a substantial investment — depending on your career, you will be forfeiting 4-6 years of salary, and you will be spending a lot of time and effort. The starting salary of a Computer Science graduate in the West Coast is easily over $100,000, so you are looking at a $400,000 to $600,000 investment in forfeited salary alone, and there’s no guarantee that having a Ph.D.&nbsp;will substantially improve your salary later. For science careers with a solid industry market, the PhD is more likely to make a difference in your salary after the initial investment than in other fields. For a long career in these fields, it’s likely to pay off, but not guaranteed. For fields with a weaker market, a PhD is unlikely to pay off financially. Also, doing a PhD outside of your career is unlikely to help you get a job. For example, if you are planning to work as a software engineer and want to do a PhD in English before you start your career, you are probably going to be better off financially if you don’t do the PhD. It may still be worth it for you for the learning experience.</p>
<p>If want to do it because you really love to learn… well, maybe that’s a good reason. PhDs in top universities are very rigorous, and you will learn a lot. I have to say that after doing it, you will feel that you know <em>less</em> than before you started it, but at least you are going to be pretty sure about what you know well and what you don’t. You get lots of skills: you will learn to research rigorously, you will improve your presentation skills, you will learn how to learn, you will get project management skills, but those are skills that you can get elsewhere while being paid.</p>
</section>
<section id="can-it-help-me-get-a-job" class="level2">
<h2 class="anchored" data-anchor-id="can-it-help-me-get-a-job">Can it help me get a job?</h2>
<p>As it’s common with most answers… it depends. Let’s do a multiverse analysis. Assume that there are two versions of you. Both just finished a Computer Science degree at a prestigious university. One of them will go to work in a well-recognized tech company for five years, the other version will go for a PhD in Computer Science at a prestigious university for five years. Five years later, both apply for a non-research software engineering position. Besides having more money, the engineering version of you has more experience and it’s more likely to get the job. On the other hand, if five years later both of you apply for a research position in a tech company, the PhD version of you is more likely to get the job. Only the research version of you can apply for a tenure-track Assistant Professor position. Ten years after finishing school, the software engineer version is likely to have more wealth.</p>
</section>
<section id="what-about-the-experience" class="level2">
<h2 class="anchored" data-anchor-id="what-about-the-experience">What about the experience?</h2>
<p>For most people in research institutions, the experience is very hard. It’s a lot of work, and it’s very stressful. It’s not uncommon to have to work 60-80 hours a week. There’s a lot to learn and a lot of work to do in a short amount of time, and your work will be evaluated at the highest level. You need to improve the overall human body of knowledge, and a lot of dedicated people have been working on it for a while before you started, so it’s supposed to be hard. You need to find something that you enjoy doing hours on end and researching very deeply. You need to be able to self-motivate. For some careers, a lot of the experience is about recovering from failure, as a successful researcher has many papers rejected. Most people who start a PhD are good students that are used to success, and it’s a very different experience to have to deal with failure.</p>
<p>Therefore, it’s not something that most people would recommend for the experience alone.</p>
<p>For people in non-research institutions that get the title without having to publish new research at top tier journals, the experience tends to be a lot better, but the applicability of the degree also tends to be a lot lower.</p>
</section>
<section id="so-should-i-do-a-phd" class="level2">
<h2 class="anchored" data-anchor-id="so-should-i-do-a-phd">So, should <strong><em>I</em></strong> do a PhD?</h2>
<p>If you want to be a researcher in a top university, it’s kind of the only way. If you are thinking about doing a PhD to make someone else happy, or to get a lot of easy money, my suggestion is that you shouldn’t do it — it’s an uncertain way to make parents happy, or to make a lot of money. For all other cases, the answer is an unsatisfactory <em>“it depends a lot on your particular situation and unlikely that you’ll find the answer in an online article”</em>. You need to look hard into your objectives and talk to some people in both your subject area and the career you plan to pursue afterwards. A PhD is probably one of the major purchases you will do in your whole life, usually as big as a house, so you should invest a lot of time in researching it and making sure it’s the right decision for you. It’s definitely worth your time and money to get information from people that have been through a similar path in order to answer your questions.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2024-01-17-phd-for-job.html</guid>
  <pubDate>Wed, 17 Jan 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/phd-holding-book.jpeg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Replacing my reMarkable 2 with a Kindle Scribe</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-01-12-kindle-scribe.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>I recently replaced my <a href="../posts/2023-12-31-remarkable2.html">reMarkable 2</a> e-tablet by a <a href="https://amzn.to/4aOKq33">Kindle Scribe</a>. The Scribe solves some of the problems I had with the reMarkable 2, and it is a better fit for my needs. In this post, I will describe what I use the Kindle Scribe for, and how it compares to the reMarkable 2.</p>
<p>The Kindle Scribe is a tablet with an e-ink display, similar to the Kindle. It is designed to be used with a stylus, and it is marketed as a paper replacement, similar to the reMarkable 2.</p>
<p>Although the reMarkable 2 worked well enough for me, one of the problems I found in my <a href="../posts/2023-12-31-remarkable2.html">review</a> was that it was not great for reading PDFs of academic articles, even ones that are text-only. The main problem for me was the lack of a backlight, which made it hard to read in low light conditions, and even in somewhat normal light conditions — the contrast is not very good and not adjustable. The Kindle Scribe solves this problem.</p>
<p>In addition, the Kindle Scribe gives you access to the Kindle store, which means you can read most books on it.</p>
<section id="what-problems-i-want-to-solve" class="level1">
<h1>What problems I want to solve?</h1>
<p>These days, I can take handwritten notes in most of my devices. My main device is a Surface Laptop Studio 2, which folds into a tablet and has a stylus. I also have an iPad Pro 13” with a stylus, but my Surface mostly replaced it. However, I cannot do everything with my Surface Laptop Studio 2. Here are a few of the issues preventing it from being my only device:</p>
<ul>
<li>It is a little on the heavy side. It is heavier than the iPad Pro 13”, that many people already find too heavy for handwritten notes.</li>
<li>Battery life is not great. I can get about 2-3 hours of battery life out of it, which is not enough for a full day of meetings.</li>
<li>It is easy to get distracted by notifications and other apps on the device.</li>
</ul>
</section>
<section id="how-does-the-kindle-scribe-solve-these-problems" class="level1">
<h1>How does the Kindle Scribe solve these problems?</h1>
<p>The Kindle Scribe solves these problems in the following ways:</p>
<ul>
<li>The Kindle Scribe is light. It is 433 grams (28g heavier than the reMarkable 2) and 5.8 mm (0.22inches) thick, 1.1 mm (0.02 inches) thicker than the reMarkable 2. For comparison, my iPad Pro 13” is 682 grams (1.5 pounds) and 5.9 mm (0.23 inches) thick. I can confortable use the Scribe to read in in bed, which I do often.</li>
<li>It has a very long battery life. I charge it about once a week.</li>
<li>The Kindle Scribe doesn’t bother me with notifications, and its browser is hidden and very limited, so I don’t get distracted by it. One advantage of having a browser is that you can use captive networks, such as the ones in hotels, which was a problem I had with the reMarkable 2. Frequently I had to connect to my phone’s hotspot to get my notes out of the reMarkable 2. I have not traveled with the Kindle Scribe yet, but I expect it to be easier to join captive networks.</li>
</ul>
</section>
<section id="what-is-the-kindle-scribe-useful-for" class="level1">
<h1>What is the Kindle Scribe useful for?</h1>
<p>I have been using the Kindle Scribe for the following use cases:</p>
<ul>
<li><strong>Academic paper reading and annotation</strong>: This was my main goal, and where the Kindle Scribe succeeded and the reMarkable 2 fell a little short because of the lack of contrast and backlight. There is still one issue with the Kindle scribe: similar to the reMarkable, it has a 10.3” screen, smaller than A4 paper, so I need to read papers in landscape mode and scroll. I can write notes directly on the PDF, and highlight parts of the PDF. The highlights are saved to a file called “My Clippings”, in text format.</li>
<li><strong>Reading books</strong>: A big advantage that the Kindle Scribe has over the reMarkable 2 is that the Scribe has my whole Kindle library. Although some books I read require the iPad, the vast majority are perfectly fine in the Scribe, and this has helped me read more.</li>
<li><strong>Journaling</strong>: I have been using the Kindle Scribe for journaling almost every day. The writing experience is good. If I never had a reMarkable, I think I’d find it great, but the reMarkable definitely feels more like paper. The Kindle Scribe sometimes feels like it slides a little bit too easily, but it’s a lot better than my experience writing on the iPad or on the Surface Laptop Studio. One thing that helped was to change my default pen to “marker” or “fountain pen”.</li>
<li><strong>Note-taking in meetings</strong>: I have taken the Kindle Scribe to meetings, and this allows me to focus a lot better. I have had no problem importing notes later - you can scan them and send them by email to yourself if you need to use your notes in an email, for example.</li>
<li><strong>Writing down ideas</strong>: I have been using the Kindle Scribe to write ideas, similar to what I did with the reMarkable. I find it easier to use the Kindle Scribe, because I can do it in low light conditions (e.g., when my kids are watching a movie in the living room).</li>
</ul>
</section>
<section id="what-are-its-downsides" class="level1">
<h1>What are its downsides?</h1>
<p>Here are some of the downsides:</p>
<ul>
<li><strong>Templates</strong>: the Kindle Scribe doesn’t have many templates. My preferred template on the reMarkable 2 was the “small dotted”, a template that had barely visible dots to guide lines The Scribe has a dotted template but the dots are quite big, I end up mostly using the blank template. Also, the reMarkable 2 has tons of interesting templates (e.g.&nbsp;“Cornell Notes”) for download. I couldn’t find those for the Kindle yet.</li>
<li><strong>Size</strong>: Its 10” screen is a little small for reading PDFs. I have been reading a lot of academic papers on it, and it’s easier to do it on landscape mode, where it gets a little bigger, but requires scrolling. This is similar to my experience with the reMarkable 2.</li>
<li><strong>Writing experience</strong>: This is very subjective, but the writing experience is not as good as the reMarkable 2. The reMarkable 2 feels more like paper, and the Kindle Scribe feels a little more slippery. However, it’s still a lot better than writing on the iPad or on the Surface Laptop Studio.</li>
<li><strong>Drawing mode</strong>: In the reMarkable, if you hold the pen for a few seconds, it switches to drawing mode, where you can draw shapes. I used this a lot to draw boxes and arrows. The Kindle Scribe doesn’t have this feature, so I have to draw boxes and arrows by hand with my low drawing skills.</li>
<li><strong>No page turn buttons</strong>: some Kindles have page turn buttons, but the Kindle Scribe doesn’t. I don’t mind this, but some people do.</li>
</ul>
</section>
<section id="conclusion-do-i-recommend-it" class="level1">
<h1>Conclusion: do I recommend it?</h1>
<p>I feel much better recommending the Kindle Scribe than the reMarkable 2 to most people. Besides solving more problems, the Scribe is cheaper than the reMarkable 2. I bought my Kindle Scribe with a premium pen for $299, while the reMarkable can easily get to $429. The materials used in the Kindle Scribe feel cheaper than the reMarkable 2, but it has more storage, a backlight, and access to the Kindle store. Being pragmatic, I think the Kindle Scribe is a better device for most people.</p>
<p>If your regular Kindle is due for an update, it may be worth it to get the Kindle Scribe instead. I think it is a good device for people who want to read academic papers and books, and take notes on them. It is also a good device for people who want to replace their Moleskines and loose paper sheets.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>Links to Amazon.com are affiliate links. These help the site stay online.</p>
</div>
</div>
<p>You can buy the Kindle Scribe at <a href="https://amzn.to/4aOKq33">Amazon</a>.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2024-01-12-kindle-scribe.html</guid>
  <pubDate>Fri, 12 Jan 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/kindle-scribe.png" medium="image" type="image/png" height="158" width="144"/>
</item>
<item>
  <title>Getting an ADHD diagnosis</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-01-04-adhd.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>In October 2023, lots of people with ADHD started posting about their experiences on <a href="https://www.threads.net">Threads</a>. Have you ever commuted to work and forgot to bring your laptop? Check. Do you frequently procrastinate a simple two-minute task for several days? Check.</p>
<p>As I started seeing a lot of myself in their experiences, and realized that I developed <a href="../posts/2024-02-03-adhd-coping-strategies.html">several coping mechanisms</a> and acquired several <a href="../posts/2024-02-02-adhd-coping-technologies.html">coping technologies</a> over time, I decided to go for a full diagnosis.</p>
<div class="callout callout-style-default callout-important callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Important
</div>
</div>
<div class="callout-body-container callout-body">
<p><strong>I am not a doctor. I am a computer scientist and economist. This is not medical advice. If you suspect you have ADHD, you should talk to a medical professional, like I did.</strong></p>
</div>
</div>
<section id="symptoms" class="level2">
<h2 class="anchored" data-anchor-id="symptoms">Symptoms</h2>
<p>Whether I have ADHD or not, I do have some symptoms that are commonly associated with ADHD:</p>
<ul>
<li>Difficulty getting started with tasks, even simple ones. I have gone to appointments I didn’t need to because I didn’t want to call to cancel the appointment.</li>
<li>Difficulty putting the finishing touches on tasks. It takes me 90% of the time to do 90% of the work, and 90% of the time to do the remaining 10% of the work.</li>
<li>I lose an incredible amount of things. When I was a kid, my mom was once told that about 10% of the items of the lost and found in a 300-student school were mine.</li>
<li>I have a hard time focusing on things that are not interesting to me.</li>
<li>I oscillate between hyperfocus and not being able to focus at all.</li>
</ul>
<p>Despite the difficulties, I manage to function well, because I developed a lot of coping mechanisms and <a href="../posts/2024-02-02-adhd-coping-technologies.html">technologies</a> over the years.</p>
</section>
<section id="why-i-decided-to-get-tested" class="level2">
<h2 class="anchored" data-anchor-id="why-i-decided-to-get-tested">Why I Decided to Get Tested</h2>
<p>The flurry of posts in Threads due to the ADHD awareness month in October 2023 got me started. Several posts described experiences that were very similar to mine. A particular story that resonated a lot with me was the story of someone who was treated for anxiety for years when they in fact had ADHD, and only got diagnosed in their 40s.</p>
<p>I found out that this is a common problem. I have been undergoing treatment for anxiety, and though there was some improvement, progress has been slow. Several posts described a situation similar to mine: people that made little progress while being treated for anxiety, but once diagnosed with and treated for ADHD as adults, their situation improved significantly.</p>
<p>This gave me hope, prompting me to research the diagnostic process. Starting the process ended up being more confusing than helpful. The information I found online described several different processes and tests, some of which even seemed to promise a diagnostic outcome. The best advice came from Threads and it was clear: begin by consulting a medical professional and expressing your concerns, and go from there.</p>
</section>
<section id="the-referral" class="level2">
<h2 class="anchored" data-anchor-id="the-referral">The Referral</h2>
<p>The process, at least where I live, seems to be designed to make it hard for people with ADHD to get diagnosed. I had to go through several steps, and I’m still not done. I live in the state of Washington in the United States. The experience may be different in other states or countries.</p>
<p>As mentioned above, the first step is to talk to a medical professional. I had a previously scheduled appointment for my annual physical just a few weeks after I decided to get tested, so I waited for that appointment in mid-October. Trying to schedule a separate appointment would have taken a lot longer.</p>
<p>In the appointment, my PCP told me they would call me with a referral in a couple of days, but instead of calling me, they called my emergency contact, who forgot to tell me about it. Luckily, people in Threads kept reminding me to follow up, and I called my PCP office a few days later. I found the referral and called the psychologist office.</p>
</section>
<section id="the-initial-evaluation" class="level2">
<h2 class="anchored" data-anchor-id="the-initial-evaluation">The Initial Evaluation</h2>
<p>I called the psychologist’s office in early November. The first thing I needed to do was to set up an account in their system, which took a few days. Then, when the account was completed, they asked me to fill several standard medical forms, plus a two-page biography describing why I think I may have ADHD, and three questionnaires designed to assess whether I have ADHD and potentially autism, which tends to happen hand-in-hand. The questionnaires are below:</p>
<ol type="1">
<li><a href="https://l.meyerperin.com/asrs">Adult ADHD Self-Report Scale (ASRS)</a></li>
<li><a href="https://l.meyerperin.com/badds">Brown ADD Scales</a></li>
<li><a href="https://l.meyerperin.com/aq">Autism Spectrum Quotient (AQ)</a></li>
</ol>
<p>The first questionnaire is a screening tool for ADHD. The second is a more detailed questionnaire about ADHD symptoms. The third is a screening tool for Autism Spectrum Disorder (ASD), a common comorbidity. The first six questions serve as a screening tool: if you score in the grey areas for four out of the six questions, you should definitely talk to a medical professional.</p>
<p>I completed the forms December 8th. After all the forms are filled, there are three more appointments to go through, and the first can only be scheduled after the forms are completed. I received a message on the day after that I should schedule the first appointment. I only managed to call back on December 22nd, and scheduled the first evaluation for January 3rd, 2024.</p>
</section>
<section id="the-first-meeting" class="level2">
<h2 class="anchored" data-anchor-id="the-first-meeting">The First Meeting</h2>
<p>On Jan 3rd, 2024, I had the first meeting with the psychologist. We introduced ourselves and he asked me about the symptoms that resulted in me seeking a diagnosis. I described the symptoms I listed above, and he asked me to elaborate on each one of them. He also asked me about my childhood, and I described how I was a very good student, but I had a hard time focusing on things that were not interesting to me. I also described how I lost a lot of things, and how I had a hard time getting started with tasks.</p>
<p>He asked about my family history. My mom is definitely not neurotypical, but she has never sought a diagnosis. I did not have enough contact with my dad to know if he was neurotypical or not. He also asked about my children, and I told him that their school has guided me to request accomodation arrangements for them (these are called 504s). I told him that I plan to get them through an ADHD and autism evaluation as well and that the school counselor had already suggested that earlier in the year.</p>
<p>The whole meeting lasted about 40 minutes. We scheduled the appointment for the second meeting on January 8th, 2024.</p>
<p>He then gave me three more tests to fill out on my own time, which I did later at night on that same day.</p>
<ul>
<li><strong>A personality test with 344 questions:</strong> the test asked a lot about whether I can control other people’s minds, whether I think they control my mind, and how frequently I think about suicide. It also repeated several questions about the <a href="https://www.cdc.gov/ncbddd/adhd/diagnosis.html">usual ADHD symptoms</a>. It would ask “Do you always feel distracted?” and offer four options for me to choose (False, Slightly True, Mostly True, True). Later, it would ask “Do you frequently feel distracted?”, and show the four options again. This one took a while to get through.</li>
<li><strong><a href="https://en.wikipedia.org/wiki/Big_Five_personality_traits">The Big Five personality test</a>:</strong> a standard personality test with about 50 questions, which was somewhat quick to get through.</li>
<li><strong>An attention test called <a href="https://www.braintrain.com/ivaae2/">IVA-AE2</a>:</strong> this test repeatedly shows an image of a number and speaks another number. For example, it can show 2 and speak “six”. You are assigned two different target numbers, one to click when you see it and another to click when you hear it. It then speaks and shows random numbers for 20 minutes. For example, if you are assigned the number “3 when seen” and “5 when heard”, you should click if it shows 3 and speaks 6, you should also click if it shows 2 and speaks 5, but you should not click when it shows 5 and speaks 3. I think it’s pretty normal to make the mistake of clicking when you hear the number you’re supposed to click only when you see and vice-versa, and I’m sure I made plenty of mistakes, but I think most people would. This test was very tiring.</li>
</ul>
</section>
<section id="the-second-meeting" class="level2">
<h2 class="anchored" data-anchor-id="the-second-meeting">The Second Meeting</h2>
<p>On January 8th, 2024, I had the second meeting with the psychologist. This meeting was markedly shorter. He sent me an email with a link to the <a href="https://moxo.neurotech-solutions.com/patients/">MOXO d-CPT ADHD Assessment</a>, a test that has visual and auditory distractions, for me to do during the meeting, while he scored my previous tests.</p>
<p>The test is similar to the IVA-AE2 test I did for the first meeting. It repeatedly shows images of several playing cards, but I have to identify a specific one (the Ace of Hearts) and press the space bar when I see it, while a lot of distractions happen simultaneously. The test takes 18 minutes to complete, and is also somewhat stressful and tiring.</p>
<p>I downloaded a demo from their website and edited the video to show a 15-second snippet of the most difficult part below.</p>
<div class="quarto-video ratio ratio-16x9"><iframe data-external="1" src="https://www.youtube.com/embed/a3h038jjoIg" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe></div>
<p>After I completed the test, he asked me a few more questions:</p>
<ul>
<li>Do I play videogames a lot? (I used to, but I don’t anymore)</li>
<li>How did I feel about the IVA-AE2 test? (I felt it was very tiring)</li>
<li>Did I have any childhood trauma? (I wouldn’t call it trauma but my father left my life quite soon, and my mom is not neurotypical, and we have some money difficulties, which led me to work really early in my life)</li>
</ul>
<p>I imagine the first question was related to my reaction time, but I’m probably only going to learn about that in the third meeting.</p>
<p>We ended the second meeting somewhat early, after just 30 minutes, and scheduled the feedback meeting for Wednesday January 10th, 2024.</p>
</section>
<section id="the-third-meeting-the-diagnosis" class="level2">
<h2 class="anchored" data-anchor-id="the-third-meeting-the-diagnosis">The Third Meeting: The Diagnosis</h2>
<p>The doctor I used, Dr.&nbsp;Richard Wemhoff, Ph.D., from <a href="https://www.emmauscounseling.net/">Emmaus Counseling Center</a>, is a specialist in <strong>adult</strong> ADHD diagnosis. If you read the whole process above, you will see that it was very comprehensive. He goes through ten steps, seven of which are questionnaires. I’ll describe the process and the final diagnosis below.</p>
<ol type="1">
<li><strong>Do your symptoms get in the way of your personal or work life?</strong> Although this is a simple question, it’s an important one. For me, the answer was yes.</li>
<li><strong>Did this happen when you were young?</strong> Again, this was true in my case. ADHD that appears in adulthood is unusual, and it’s important to rule out other causes.</li>
<li><strong><a href="https://l.meyerperin.com/badds">Brown ADD Scales</a>:</strong> this test goes from a scale of 0 to 120. 0-40 means ADHD is unlikely. 41 to 55 means is possible, and 56+ means it’s likely. I scored 67, and it was significant in all the five components: attention, activation, emotion, memory, and effort. One interesting thing is that if some components are too significant, this may indicate a different condition. For example, a high score on emotion (meaning that the person is feeling very bad about the symptoms) may indicate depression. That was not my case in any of the components.</li>
<li><strong>DSM-5</strong>: a set of 18 questions, 9 to screen for inattention and 9 to screen for hyperactivity. I scored 7/9 on inattention and 8/9 on hyperactivity, again, indicative of ADHD. There are three types of ADHD: innatentive, hyperactive, and combined. I scored high on both, so, according to this test, I’m likely to have combined ADHD.</li>
<li><strong>Can the symptoms be due to another condition?</strong> This is where the long tests done after the first meeting come into play. He scored the results and I don’t seem to have any other condition. The only thing that was a little above average was “social detachment”, but still within normal range.</li>
<li><strong>Family history:</strong> ADHD tends to run in families. My mom has some condition, as of yet undiagnosed. My kids have ADHD symptoms and given a screening, they should be tested, so there’s definitely a family history.</li>
<li><strong><a href="https://l.meyerperin.com/asrs">Adult ADHD Self-Report Scale (ASRS)</a>:</strong> an 18-question test, similar but not exactly the same as DSM-5. This also resulted in a high score, and it’s a good indicator of ADHD.</li>
<li><strong><a href="https://moxo.neurotech-solutions.com/patients/">MOXO d-CPT ADHD Assessment</a>:</strong> this is the test I did in the second meeting, and is considered to be very precise. I scored “ADHD present”. The test has four components. I only had a normal score on “impulsiveness”, meaning I’m not overly impulsive. I scored below average on “attentiveness”, and far below average on “timeliness” and “hyper reactivity”. This last one means that I overreact to distractions, and I felt it in my soul.</li>
<li><strong><a href="https://www.braintrain.com/ivaae2/">IVA-AE2</a>:</strong> to everyone’s surprise, I passed on this attention test. The doctor said that this is not <em>that</em> unusual, people with ADHD can hyperfocus.</li>
<li><strong><a href="https://l.meyerperin.com/aq">Autism Spectrum Quotient (AQ)</a></strong>: this is a test to screen for autism, not to diagnose it. Autism is present in 25% of the ADHD cases, so it’s important to rule it out. I scored way above the cut-off range, which means that I also need to be evaluated for autism.</li>
</ol>
<p>In summary, the result is that I definitely have ADHD, and that information was sent to my PCP to discuss treatment options, which may include medicine. I also need to be evaluated for autism, which is a different process that I will document in a different article. The doctor also said that I should be proud of myself for having accomplished the things I did in life given my condition, and that things should improve a lot when I start treatment.</p>
<p>I am describing my process with the treatment at <a href="../posts/2024-01-19-adhd-treatment.html">this article</a>.</p>
</section>
<section id="do-i-recommend-this-process" class="level2">
<h2 class="anchored" data-anchor-id="do-i-recommend-this-process">Do I recommend this process?</h2>
<p>During this whole process, I felt conflicted. End-to-end, it took months, and for a while I was so distracted with other things that I ended up delaying my diagnosis. I heard stories of people that get their diagnosis very quickly, in a single appointment. My process had several steps, and I had to do a lot of tests, and I was not sure if it was going to be worth it.</p>
<p>There is, however, an immense advantage in going through a process that was so comprehensive. I feel very confident about the diagnosis. I know that I have ADHD, and I know that I don’t have any other condition that masks the symptoms. And now I know what to do about it.</p>
</section>
<section id="reactions" class="level2">
<h2 class="anchored" data-anchor-id="reactions">Reactions</h2>
<p>I posted about my experience in Threads, and I got a lot of reactions. Most people are supportive and describe their own experiences being diagnosed. Some people, however, are skeptical. They say that ADHD is overdiagnosed, and that it’s a fad, or a quick excuse to be lazy and forgetful. While I disagree, I have seen some websites that promise a “quick and easy” diagnosis and to get you medications fast. It feels similar to advertisements for Viagra and off-label Ozempic.</p>
<p>Overall, having documented this process through posts has been a good experience, and I really hope it helps other people.</p>
</section>
<section id="treatment" class="level2">
<h2 class="anchored" data-anchor-id="treatment">Treatment</h2>
<p>I have been documenting the progress with my treatment. I have written a <a href="../posts/2024-01-19-adhd-treatment.html">1-week update</a> and a <a href="../posts/2024-02-10-adhd-treatment-4-week.html">4-week update</a>.</p>
</section>
<section id="what-about-you" class="level2">
<h2 class="anchored" data-anchor-id="what-about-you">What about you?</h2>
<p>If you suspect you may have ADHD, I recommend that you talk to a medical professional. Your PCP is a good place to start. Online communities, such as the ones in Threads, can also be a good place to start.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2024-01-04-adhd.html</guid>
  <pubDate>Wed, 10 Jan 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/adhd_heading.png" medium="image" type="image/png" height="144" width="144"/>
</item>
<item>
  <title>Creating an Azure Web App with Authentication</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-01-07-app-with-authentication.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>This is the first post in a series of posts about creating a web app to manage my social media. The main page for this series is <a href="../posts/2024-01-05-thread_manager.html">here</a>.</p>
<p>We begin by <a href="https://learn.microsoft.com/en-us/azure/app-service/quickstart-python?tabs=django%2Cwindows%2Cazure-portal%2Cvscode-deploy%2Cdeploy-instructions-azportal%2Cterminal-bash%2Cdeploy-instructions-zip-azcli#2---create-a-web-app-in-azure">creating a new App Service in Azure using the Azure Portal</a>.</p>
<div class="callout callout-style-default callout-important callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Important
</div>
</div>
<div class="callout-body-container callout-body">
<p>I did not use the sample code in section 1 from the link above, because I intended to use the code that uses the Microsoft Authentication Library (MSAL) for Python. <strong>I just created an empty app.</strong></p>
</div>
</div>
<p>From the defaults, I changed the App plan to a cheaper plan. The free plan (F1) also works. I also configured the app to use the Python 3.11 runtime, which is the version I’m using to develop.</p>
<p>I then <a href="https://learn.microsoft.com/en-us/azure/app-service/deploy-continuous-deployment?tabs=github">configured the app to sync with GitHub for continuous deployment</a>, so that the app automatically updates in production when I push changes to GitHub. I simply followed the instructions on the link, which are very straightfoward: I selected GitHub as the source, authorized Azure to access my GitHub account, selected the repository and branch, and then selected the option to deploy the code automatically when I push changes to GitHub.</p>
<section id="configuring-microsoft-entra-id-and-linking-the-app" class="level2">
<h2 class="anchored" data-anchor-id="configuring-microsoft-entra-id-and-linking-the-app">Configuring Microsoft Entra ID and Linking the App</h2>
<p>Ultimately, I want to ensure that users are only allowed to see the app if they are authorized. To do this, I will use Microsoft Entra ID to authenticate users. To get to that, we first need to <strong><em>authenticate</em></strong> the users, and then <strong><em>authorize</em></strong> them to access the app. This post is about the authentication part.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>Authentication is the process of verifying a user’s identity, typically using credentials like usernames and passwords. Authorization, on the other hand, determines if an authenticated user has permissions to access certain resources or perform specific actions. While authentication confirms who the user is, authorization decides what an authenticated user is allowed to do. Authentication is the first step, necessary before authorization can be applied to manage access and permissions.</p>
</div>
</div>
<p>To create an app that does authentication, I followed the instructions at <a href="https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-web-app-python-sign-in?tabs=windows">Quickstart: Sign in users and call the Microsoft Graph API from a Python web app</a>. The instructions will tell you where to find the <code>CLIENT_ID</code> and how to generate a <code>CLIENT_SECRET</code> for the app. The <code>AUTHORITY</code> needs to be set to <code>AUTHORITY="https://login.microsoftonline.com/&lt;tenant_id&gt;"</code>, which I obtained from the Azure Portal, in the Overview page of Microsoft Entra ID.</p>
<p>I downloaded the full code for the app, and the only change I had to make was to create an <code>.env</code> file with the variables described above. This is not required for the app to run on the web, but it helps when running locally.</p>
<p>The <code>.env</code> file should look like this:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">CLIENT_ID</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">[</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">client_id</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">]</span></span>
<span id="cb1-2"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">CLIENT_SECRET</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">[</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">client_secret</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">]</span></span>
<span id="cb1-3"><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">AUTHORITY</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>https://login.microsoftonline.com/<span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">[</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">tenant_id</span><span class="pp" style="color: #AD0000;
background-color: null;
font-style: inherit;">]</span></span></code></pre></div></div>
<p>The instructions don’t mention this, but after finishing the instructions, you also have to link the Entra ID configuration to the Web App you created in the first step. To do this, go to the Azure Portal, open the App Service, go to the “Authentication” tab, and click on “Microsoft” under “Identity providers”. Then, select the “Existing App” tab and select the app you created in the previous step.</p>
<p>The final connection step was to set up the environment variables <code>CLIENT_ID</code>, <code>CLIENT_SECRET</code> and <code>AUTHORITY</code> in the App Service. I followed the instructions on <a href="https://docs.microsoft.com/en-us/azure/app-service/configure-common#configure-app-settings">this link</a> to set the environment variables in the Web App to the same values present in the .env file.</p>
<p>Once I did all this, I pushed my code to GitHub and it was automatically deployed to the App Service.</p>
</section>
<section id="code-walkthrough" class="level2">
<h2 class="anchored" data-anchor-id="code-walkthrough">Code walkthrough</h2>
<p>The code is for a <a href="https://flask.palletsprojects.com/en/3.0.x/">Flask</a> Web App. Flask is a Python framework for building web apps. I decided to use it instead of other alternatives like Angular or NextJS because it allows me to use Python, which is the language I’m most familiar with. The code is in the <a href="https://github.com/lucas-a-meyer/thread-manager/blob/65a6fe606281ab89b55b9e6add9fd476c3b0d024/app.py"><code>app.py</code></a> file, and I will walk through it below. I also created an <code>app_config.py</code> file to store the configuration variables, which you can see in GitHub <a href="https://github.com/lucas-a-meyer/thread-manager/blob/main/app_config.py">here</a>. I will not walk through the <code>app_config.py</code> file, because it is just a file to store the configuration variables.</p>
<section id="the-app.py-file" class="level3">
<h3 class="anchored" data-anchor-id="the-app.py-file">The app.py file</h3>
<p>The first thing we do is import the libraries we will use. The <code>identity.web</code> library is the one that does the authentication. The <code>requests</code> library is used to make HTTP requests. The <code>Flask</code> library is the framework we are using to build the web app. The <code>redirect</code>, <code>render_template</code>, <code>request</code>, <code>session</code>, and <code>url_for</code> libraries are all from Flask. The <code>Session</code> library is used to store the session information. The <code>app_config</code> library is the one we created to store the configuration variables.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> identity.web</span>
<span id="cb2-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> requests</span>
<span id="cb2-3"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> flask <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> Flask, redirect, render_template, request, session, url_for</span>
<span id="cb2-4"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> flask_session <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> Session</span>
<span id="cb2-5"></span>
<span id="cb2-6"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> app_config</span>
<span id="cb2-7"></span>
<span id="cb2-8">__version__ <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"0.7.0"</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># The version of this sample, for troubleshooting purpose</span></span>
<span id="cb2-9"></span>
<span id="cb2-10">app <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Flask(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">__name__</span>)</span>
<span id="cb2-11">app.config.from_object(app_config)</span>
<span id="cb2-12"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">assert</span> app.config[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"REDIRECT_PATH"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"/"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"REDIRECT_PATH must not be /"</span></span></code></pre></div></div>
<p>The next thing we do is create a session. This is used to store the session information. The <code>Session</code> library is used to store the session information. The <code>app.jinja_env.globals.update(Auth=identity.web.Auth)</code> is not used in my code.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1">Session(app)</span>
<span id="cb3-2"></span>
<span id="cb3-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># This section is needed for url_for("foo", _external=True) to automatically</span></span>
<span id="cb3-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># generate http scheme when this sample is running on localhost,</span></span>
<span id="cb3-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># and to generate https scheme when it is deployed behind reversed proxy.</span></span>
<span id="cb3-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># See also https://flask.palletsprojects.com/en/2.2.x/deploying/proxy_fix/</span></span>
<span id="cb3-7"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> werkzeug.middleware.proxy_fix <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> ProxyFix</span>
<span id="cb3-8">app.wsgi_app <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> ProxyFix(app.wsgi_app, x_proto<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, x_host<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)</span>
<span id="cb3-9"></span>
<span id="cb3-10">app.jinja_env.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">globals</span>.update(Auth<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>identity.web.Auth)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Useful in template for B2C</span></span>
<span id="cb3-11">auth <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> identity.web.Auth(</span>
<span id="cb3-12">    session<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>session,</span>
<span id="cb3-13">    authority<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>app.config[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"AUTHORITY"</span>],</span>
<span id="cb3-14">    client_id<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>app.config[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"CLIENT_ID"</span>],</span>
<span id="cb3-15">    client_credential<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>app.config[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"CLIENT_SECRET"</span>],</span>
<span id="cb3-16">)</span></code></pre></div></div>
<p>The next thing we do is create a route for the login page. The <code>@app.route("/login")</code> is a decorator that tells Flask that the function below it is a route for the login page. The <code>render_template</code> function renders the <code>login.html</code> template, which is the login page.</p>
<p>The important line in the login template is <a href="https://github.com/lucas-a-meyer/thread-manager/blob/65a6fe606281ab89b55b9e6add9fd476c3b0d024/templates/login.html#L19">line 19</a>, which initiates the login process by directing the user to the <code>auth_response</code>route, which is the same as <code>app_config.REDIRECT_PATH</code>.</p>
<p>The <code>auth.log_in</code> function comes from the <code>identity.web</code> library, and returns a dictionary with the information needed to log in. The <code>scopes</code> parameter is used to tell the user what the app will be able to do on their behalf (in my case, just read their basic information). The <code>redirect_uri</code> parameter is optional, must match the redirect URI registered in the Azure Portal. The <code>prompt</code> parameter is also optional, and it can take different values, which are defined in <a href="https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest">this link</a>.</p>
<p>If the authentication is successful, the <code>auth_response</code> route will be called. If the authentication is not successful, the <code>auth_error.html</code> template will be rendered.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@app.route</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"/login"</span>)</span>
<span id="cb4-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> login():</span>
<span id="cb4-3">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> render_template(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"login.html"</span>, version<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>__version__, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span>auth.log_in(</span>
<span id="cb4-4">        scopes<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>app_config.SCOPE, <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Have user consent to scopes during log-in</span></span>
<span id="cb4-5">        redirect_uri<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>url_for(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"auth_response"</span>, _external<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>), </span>
<span id="cb4-6">        prompt<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"select_account"</span>,  </span>
<span id="cb4-7">        ))</span></code></pre></div></div>
<p>The next thing we do is create a route for the <code>auth_response</code> page. The <code>auth.complete_log_in(request.args)</code> function is from the <code>identity.web</code> library, and it returns a dictionary with the result of the log in process, which can be successful or an error, in which case it will contain an error message.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@app.route</span>(app_config.REDIRECT_PATH)</span>
<span id="cb5-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> auth_response():</span>
<span id="cb5-3">    result <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> auth.complete_log_in(request.args)</span>
<span id="cb5-4">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"error"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> result:</span>
<span id="cb5-5">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> render_template(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"auth_error.html"</span>, result<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>result)</span>
<span id="cb5-6">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> redirect(url_for(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"index"</span>))</span></code></pre></div></div>
<p>If the login was successful, we show the <a href="https://github.com/lucas-a-meyer/thread-manager/blob/65a6fe606281ab89b55b9e6add9fd476c3b0d024/templates/index.html">index page</a>, which lists the information we collected about the user from the authentication provider.</p>
<p>I am only using Microsoft Accounts (formerly Live, currently used in Xbox and Outlook.com) as the authentication provider for now. I may add Google Accounts later, but I don’t intend to manage my own authentication, even though the code supports it.</p>
<p>I call the function <code>auth.get_user()</code> from the <code>identity.web</code> library, and it returns a dictionary with the information about the user. The <code>render_template</code> function renders the <code>index.html</code> template, which is the index page. The <code>user</code> parameter is used to pass the user information to the template. The index page will display the user name and the subscription ID.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb6-1"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@app.route</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"/"</span>)</span>
<span id="cb6-2"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> index():</span>
<span id="cb6-3">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> auth.get_user():</span>
<span id="cb6-4">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> redirect(url_for(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"login"</span>))</span>
<span id="cb6-5">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> render_template(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'index.html'</span>, user<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>auth.get_user(), version<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>__version__)</span>
<span id="cb6-6"></span>
<span id="cb6-7"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">__name__</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"__main__"</span>:</span>
<span id="cb6-8">    app.run()</span></code></pre></div></div>
</section>
</section>
<section id="testing" class="level2">
<h2 class="anchored" data-anchor-id="testing">Testing</h2>
<p>I navigated to the app URL and was able to sign in with my Microsoft account. I was then redirected to the app, where I could see the information about my login.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://meyerperin.org/images/thread_manager/step1.png" class="img-fluid figure-img"></p>
<figcaption>Successful log in</figcaption>
</figure>
</div>
<p>Now that I have an app that can authenticate users, I need to work on authorization, so that only authorized users can access the app.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2024-01-07-app-with-authentication.html</guid>
  <pubDate>Sun, 07 Jan 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/azure-web-app.png" medium="image" type="image/png" height="144" width="144"/>
</item>
<item>
  <title>Automatically adding meta tags to your blog posts with OpenAI</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-01-06-automatically-adding-descriptions.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>As I was updating my blog, I realized that I had forgotten to add the <code>description</code> meta tag to many of my blog posts. This is the text that appears in search results and when you share a link on social media. I didn’t want to go through all my posts and add a description manually, so I decided to use OpenAI GPT to generate descriptions for me.</p>
<p>This code is very simple, but it may be useful to you, so I’m sharing it here. It’s written in Python and uses the new <a href="https://github.com/openai/openai-python/discussions/742">OpenAI Python client syntax</a>.</p>
<section id="the-code" class="level2">
<h2 class="anchored" data-anchor-id="the-code">The code</h2>
<p>My blog is written in Quarto, which is a Markdown-based document format. I use Quarto Markdown (<code>.qmd</code>) files to write my blog posts. A <code>.qmd</code> file is a Markdown file with YAML front matter. The YAML front matter contains metadata about the document, such as the title, author, and date. I wanted to add the <code>description</code> meta tag to the YAML front matter of each file.</p>
<p>The first thing to do is to extract the YAML front matter from the file. I used a regular expression to do this. If the file doesn’t contain YAML front matter, I will skip it.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> yaml</span>
<span id="cb1-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> re</span>
<span id="cb1-3"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> os</span>
<span id="cb1-4"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> openai <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> OpenAI</span>
<span id="cb1-5"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> dotenv <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> load_dotenv</span>
<span id="cb1-6"></span>
<span id="cb1-7"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> parse_qmd(content):</span>
<span id="cb1-8">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Extract the YAML front matter</span></span>
<span id="cb1-9">    matches <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> re.findall(<span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">r'</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">^</span><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">---</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">(</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">.</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*?</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">---'</span>, content, re.DOTALL)</span>
<span id="cb1-10">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> matches:</span>
<span id="cb1-11">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span></span>
<span id="cb1-12">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> matches[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>], content</span></code></pre></div></div>
<p>I wrote a function to recompose the file content after modifying the YAML front matter. This function takes the original YAML front matter, the modified YAML data, and the full file content as input. It replaces the original YAML front matter with the modified YAML data and returns the updated file content.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> recompose_qmd(yaml_data, original_yaml, content):</span>
<span id="cb2-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Convert the modified YAML data to string</span></span>
<span id="cb2-3">    new_yaml_str <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> yaml.dump(yaml_data, default_flow_style<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>)</span>
<span id="cb2-4"></span>
<span id="cb2-5">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Replace the original YAML content with the new one</span></span>
<span id="cb2-6">    updated_content <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> content.replace(original_yaml, new_yaml_str)</span>
<span id="cb2-7">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> updated_content</span></code></pre></div></div>
<p>The workhorse of this code is the part that generates a summary of the Markdown content. I used the OpenAI chat API to do this. I created a chat prompt that asks the user to describe what the reader will learn after reading the article. I then used the <code>gpt-4</code> model to generate a response to this prompt. The response is the summary of the article.</p>
<p>In my first attempts, GPT was starting the summary with “After reading the markdown article, the reader will learn”. I didn’t want this, so I added some text and an example to the prompt to remove this behavior.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> summarize_markdown(markdown):</span>
<span id="cb3-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Load your OpenAI API key</span></span>
<span id="cb3-3"></span>
<span id="cb3-4">    client <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> OpenAI(</span>
<span id="cb3-5">        api_key <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> os.getenv(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'OPENAI_API_KEY'</span>)</span>
<span id="cb3-6">    )</span>
<span id="cb3-7"></span>
<span id="cb3-8">    chat_completion <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.chat.completions.create(</span>
<span id="cb3-9">    messages<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[</span>
<span id="cb3-10">        {</span>
<span id="cb3-11">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"role"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"user"</span>,</span>
<span id="cb3-12">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"content"</span>: <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"""Without repeating 'After reading the markdown article, the reader will learn', </span></span>
<span id="cb3-13"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">            describe in one sentence what the reader will learn about after reading the markdown article. </span></span>
<span id="cb3-14"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">            Also don't say 'the reader will learn about', just say what they'll learn. For example, </span></span>
<span id="cb3-15"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">            instead of saying 'the reader will learn about strategies for programming', </span></span>
<span id="cb3-16"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">            just say 'strategies for programming' </span></span>
<span id="cb3-17"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">            Markdown:</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span></span>
<span id="cb3-18"><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">            </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>markdown<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span>,</span>
<span id="cb3-19">        }</span>
<span id="cb3-20">    ],</span>
<span id="cb3-21">    model<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gpt-4"</span>)</span>
<span id="cb3-22"></span>
<span id="cb3-23">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> chat_completion.choices[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>].message.content</span></code></pre></div></div>
<p>Finally, I wrote a function that processes all the <code>.qmd</code> files in a directory. It loops through all the files in the directory and calls the functions I wrote above to extract the YAML front matter, generate a summary of the Markdown content, and add the summary to the YAML front matter.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> process_qmd_files(directory):</span>
<span id="cb4-2">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Loop through all files in the directory</span></span>
<span id="cb4-3">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> filename <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> os.listdir(directory):</span>
<span id="cb4-4">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> filename.endswith(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'.qmd'</span>):</span>
<span id="cb4-5">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'Processing </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>filename<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">...'</span>)</span>
<span id="cb4-6"></span>
<span id="cb4-7">            file_path <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> os.path.join(directory, filename)</span>
<span id="cb4-8"></span>
<span id="cb4-9">            <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Read the file</span></span>
<span id="cb4-10">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">with</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">open</span>(file_path, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'r'</span>, encoding<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'utf-8'</span>) <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">file</span>:</span>
<span id="cb4-11">                content <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">file</span>.read()</span>
<span id="cb4-12"></span>
<span id="cb4-13">            original_yaml, full_content <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> parse_qmd(content)</span>
<span id="cb4-14">            markdown <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> full_content.replace(original_yaml, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">''</span>)</span>
<span id="cb4-15"></span>
<span id="cb4-16">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> original_yaml <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">is</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>:</span>
<span id="cb4-17">                <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'No YAML front matter found in </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>filename<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">, skipping...'</span>)</span>
<span id="cb4-18">                <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Skip files without YAML front matter</span></span>
<span id="cb4-19"></span>
<span id="cb4-20">            yaml_data <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> yaml.safe_load(original_yaml)</span>
<span id="cb4-21"></span>
<span id="cb4-22">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'description'</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> yaml_data:</span>
<span id="cb4-23">                <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f'YAML front matter already contains a description, skipping...'</span>)</span>
<span id="cb4-24">                <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span></span>
<span id="cb4-25"></span>
<span id="cb4-26">            <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Generate a summary of the Markdown content</span></span>
<span id="cb4-27">            summary <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> summarize_markdown(markdown)</span>
<span id="cb4-28"></span>
<span id="cb4-29">            <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Add the summary to the YAML front matter</span></span>
<span id="cb4-30">            yaml_data[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'description'</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> summary</span>
<span id="cb4-31">            </span>
<span id="cb4-32">            <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Recompose the file content</span></span>
<span id="cb4-33">            updated_content <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> recompose_qmd(yaml_data, original_yaml, full_content)</span>
<span id="cb4-34"></span>
<span id="cb4-35">            <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Write the updated content back to the file</span></span>
<span id="cb4-36">            <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">with</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">open</span>(file_path, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'w'</span>, encoding<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'utf-8'</span>) <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">file</span>:</span>
<span id="cb4-37">                <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">file</span>.write(updated_content)</span>
<span id="cb4-38"></span>
<span id="cb4-39">load_dotenv()</span>
<span id="cb4-40"></span>
<span id="cb4-41"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Specify the directory containing your .qmd files</span></span>
<span id="cb4-42">directory <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>YOUR_DIRECTORY<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span></span>
<span id="cb4-43"></span>
<span id="cb4-44"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Process all .qmd files in the directory</span></span>
<span id="cb4-45">process_qmd_files(directory)</span></code></pre></div></div>
<p>My blog has over 100 documents, and it took less than 2 minutes to add descriptions to all the fields. Using GPT-4 cost me $2.41, but if I was going to do this manually it will have taken me hours. I think it was worth it.</p>
<p>I used my ChatGPT Plus to generate the main block of code that has the function <code>process_qmd_files</code>, so the whole process, end-to-end, took less than 10 minutes.</p>
The complete code is on <a href="https://github.com/lucas-a-meyer/lucas-a-meyer.github.io/blob/main/utils/add_description.py">GitHub</a>.
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2024-01-06-automatically-adding-descriptions.html</guid>
  <pubDate>Sat, 06 Jan 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/openai-logo.png" medium="image" type="image/png" height="146" width="144"/>
</item>
<item>
  <title>Tech reviewers wanted for my Semantic Kernel book</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-01-05-review-semantic-kernel.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>I am wrting a book with <a href="https://www.packtpub.com/">Packt Publishing</a> about the <a href="https://github.com/microsoft/semantic-kernel">Microsoft Semantic Kernel</a>. If you want to help by becoming a technical reviewer, please read on.</p>
<p>The Microsoft Semantic Kernel is an SDK that integrates Large Language Models (LLMs) like <a href="https://platform.openai.com/docs/introduction">OpenAI</a>, <a href="https://azure.microsoft.com/en-us/products/ai-services/openai-service">Azure OpenAI</a>, and <a href="https://huggingface.co/">Hugging Face</a> with conventional programming languages like C# and Python. It allows you to define plugins that can be chained together in just a few lines of code.</p>
<p>The book is called “Essential Guide for the Microsoft Semantic Kernel”, and its intent is to provide a practical, hands-on introduction to the Semantic Kernel.</p>
<section id="reviewers-wanted" class="level2">
<h2 class="anchored" data-anchor-id="reviewers-wanted">Reviewers wanted</h2>
<p>Packt Publishing is looking for technical reviewers to help with the book.</p>
<p>Reviewers need to have some programming experience and familiarity with AI. I don’t know exactly how large is the workload and what are the deadlines, but the book will have between 200 and 250 pages.</p>
<p>Reviewers get:</p>
<ol type="1">
<li>Credit in the book: name and bio printed on the front matter as a Technical Reviewer.</li>
<li>A complimentary print copy of the book at their doorstep.</li>
<li>Free 12-month subscription to all Packt books.</li>
</ol>
<p>This can be a cool way of learning about the Semantic Kernel and AI, while also getting something for it.</p>
<div class="callout callout-style-default callout-important callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Important
</div>
</div>
<div class="callout-body-container callout-body">
<p>I’m the author of the book, but I’m not involved in the selection of the reviewers, and I don’t know how many reviewers will be selected. I’m just helping Packt to spread the word.</p>
</div>
</div>
<p>If you are interested, please fill the form below and someone from Packt will contact you.</p>
<hr>
<script charset="utf-8" type="text/javascript" src="//js.hsforms.net/forms/embed/v2.js"></script>
<script>
  hbspt.forms.create({
    region: "na1",
    portalId: "44798468",
    formId: "202dcfb9-d901-4350-abc3-38c9ebee21bf"
  });
</script>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2024-01-05-review-semantic-kernel.html</guid>
  <pubDate>Fri, 05 Jan 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/semantic_kernel_book/reviewerW.png" medium="image" type="image/png" height="144" width="144"/>
</item>
<item>
  <title>Thread Manager - Managing Social Media</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2024-01-05-thread_manager.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>Thread Manager a the web app I intend to use to schedule posts to social media sites and run basic analytics.</p>
<p><strong>I’m starting from scratch</strong> as of January 2024, replacing a previous app I wrote to post exclusively to LinkedIn.</p>
<p>I intend to post about the process of building this app on <a href="https://www.threads.net">Threads</a> as I build it. The <a href="https://github.com/lucas-a-meyer/thread-manager">code for this app is on GitHub</a>.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>This is purely a hobby project. I have a lot of personal commitments and I’ll build this very slowly, as time allows.</p>
</div>
</div>
<section id="why-build-this" class="level1">
<h1>Why build this?</h1>
<p>In 2021, I was writing a lot on LinkedIn, but I was limited by LinkedIn’s post formatting being mostly text — no inline images, no code. To have images and code, I started writing more on my blog, but usually finished my posts in the middle of the night, and I wanted to schedule them to post the next morning. I also wanted to be able to post to multiple social media sites at once. I created a simple app to post to LinkedIn, Twitter and Mastodon, and it worked well enough for my needs.</p>
<p>Now, in 2024, I’m using Threads a lot more, and an API is probably going to be available in 2024. I want to be able to schedule my blog posts to post to Threads, and I also want to share my journey learning the Threads API.</p>
</section>
<section id="technology-choices" class="level1">
<h1>Technology Choices</h1>
<p>Since I work at Microsoft, I am more familiar with Azure than other cloud providers. And since I work in data science, I am more familiar with Python.</p>
<p>My intended technology stack is:</p>
<ul>
<li>Azure App Service to host the app</li>
<li>Azure Cosmos DB to host the data</li>
<li>Azure Functions for the scheduled job (using Python)</li>
<li>Python Flask for the web app, with Bootstrap helping make the UI better</li>
<li><a href="https://www.quarto.org">Quarto</a> for the blog posts</li>
<li>GitHub Pages to hold the blog posts</li>
</ul>
</section>
<section id="project-steps" class="level1">
<h1>Project steps</h1>
<p>The list below shows the steps I am taking to build this app. I will update this list as I make progress.</p>
<ul class="task-list">
<li><label><input type="checkbox" checked=""><a href="../posts/2024-01-07-app-with-authentication.html">Create an Azure Web App with Authentication and Basic Authorization</a></label></li>
<li><label><input type="checkbox">Create a user database in Cosmos DB and link it to authentication</label></li>
<li><label><input type="checkbox">Create a user profile page</label></li>
<li><label><input type="checkbox">Connect to the LinkedIn API</label></li>
<li><label><input type="checkbox">Enable a basic text post to LinkedIn</label></li>
</ul>
There will be more steps, and I’ll add them as I go.
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2024-01-05-thread_manager.html</guid>
  <pubDate>Fri, 05 Jan 2024 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/thread_manager_logo.png" medium="image" type="image/png" height="144" width="144"/>
</item>
<item>
  <title>My experience with the reMarkable 2</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2023-12-31-remarkable2.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>Do you want to start journaling in the new year? Tired of carrying notebooks around? Maybe you just want your notes to be more organized?</p>
<p>The reMarkable is a tablet with an e-ink display, similar to the Kindle. It is designed to be used with a stylus, and it is marketed as a paper replacement. Contrary to other tablets, if <em>feels</em> like paper.</p>
<p>I got myself a reMarkable 2 in their Black Friday promotion, after having seen a colleague at work using it. Before having a reMarkable, I used a large number of disorganized Moleskines and loose paper sheets. A lot of loose paper sheets.</p>
<p>I have been using the reMarkable 2 for a little over a month, and I am happy with it. I have used it for taking notes in meetings, for journaling, and for writing down ideas. I have also used it for reading PDFs and ebooks. But it’s not without its issues.</p>
<section id="what-problems-i-want-to-solve" class="level1">
<h1>What problems I want to solve?</h1>
<p>These days, I can take handwritten notes in most of my devices. My main device is a Surface Laptop Studio 2, which folds into a tablet and has a stylus. I also have an iPad Pro 13” with a stylus, but my Surface mostly replaced it. However, I cannot do everything with my Surface Laptop Studio 2. Here are a few of its problems:</p>
<ul>
<li>It is a little on the heavy side. It is heavier than the iPad Pro 13”, that many people already find too heavy for handwritten notes.</li>
<li>Battery life is not great. I can get about 2-3 hours of battery life out of it, which is not enough for a full day of meetings.</li>
<li>It is easy to get distracted by notifications and other apps on the device.</li>
</ul>
</section>
<section id="how-does-the-remarkable-2-solve-these-problems" class="level1">
<h1>How does the reMarkable 2 solve these problems?</h1>
<p>The reMarkable 2 solves these problems in the following ways:</p>
<ul>
<li>The reMarkable 2 is very light. It is 403 grams (0.9 pounds) and 4.7 mm (0.2 inches) thick. For comparison, my iPad Pro 13” is 682 grams (1.5 pounds) and 5.9 mm (0.23 inches) thick.</li>
<li>It has no distractions. It is designed to be used for reading and writing, and it does not have a web browser or any other apps. It is very easy to focus on what you are doing.</li>
<li>It has a very long battery life. I find myself charging it about once or twice a week depending on how much I use it. It charges via USB-C, which is easily available.</li>
</ul>
</section>
<section id="what-is-the-remarkable-2-useful-for" class="level1">
<h1>What is the reMarkable 2 useful for?</h1>
<p>I have been using the reMarkable 2 for the following use cases:</p>
<ul>
<li><strong>Academic paper reading and annotation</strong>: This was my intended use case for the reMarkable 2. The reMarkable 2 works, but it’s not ideal. It has a 10.3” screen, smaller than A4 paper, so I find myself reading papers in landscape mode, which makes the text a little bigger, but requires scrolling. In addition, I don’t like the contrast of the reMarkable’s e-ink display very much. It works, but it doesn’t have contrast adjustments, and it’s not as good as the Kindle’s display.</li>
<li><strong>Journaling</strong>: I have been using the reMarkable 2 for journaling almost every day. The writing experience is exceptional. It really feels like writing on paper, and the experience is very smooth.</li>
<li><strong>Note-taking in meetings</strong>: I have taken the reMarkable 2 to meetings, and this allows me to focus a lot better on meetings. The note-taking is superb, and the lack of distractions is great. The apps in the phone and in the PC allows me to easily see and add to my notes.</li>
<li><strong>Writing down ideas</strong>: I have been using the reMarkable 2 for writing down ideas and think about projects. It works well for that because it allows me to capture my ideas in a way that is easy to share and add to them later, without being distracted by technologies. This is specifically useful when I am in a plane or in a bus commuting.</li>
<li><strong>Many templates</strong>: the reMarkable has many paper templates, including lined paper, grid paper, and even music sheets. You can even create your own templates. I used to have more than one notebook for different purposes, and now I can have them all in the same device.</li>
</ul>
</section>
<section id="downsides" class="level1">
<h1>Downsides</h1>
<p>The reMarkable 2 is not perfect. Here are some of the downsides:</p>
<ul>
<li><p>The reMarkable 2 is expensive, starting at $299 for the barebones model. You need a pen, and you probably want a cover. You can save money by buying unnoficial pens and covers at Amazon. One of my colleagues likes a $20 pen he bought online. The official pen costs $79 without an eraser and $129 with one. Covers run between $79 (cloth) to $199 (leather, with a keyboard).</p></li>
<li><p>You cannot sync Kindle e-books to it. You can read PDFs and e-books on it, but you cannot sync your Kindle library to it.</p></li>
<li><p>Its 10” screen is a little small for reading PDFs. I have been reading a lot of academic papers on it, and it’s easier to do it on landscape mode, where it gets a little bigger, but requires scrolling.</p></li>
<li><p>The lack of contrast is a little annoying. The reMarkable 2 does not have contrast adjustments, and it’s not as good as the Kindle’s display. I find myself looking for better lighting more than I expected.</p></li>
</ul>
</section>
<section id="do-i-recommend-it" class="level1">
<h1>Do I recommend it?</h1>
<p>Although I’m moderately happy with my reMarkable 2, it’s not for everyone and not for every use case.</p>
<p>It replaces my Moleskines and loose paper sheets, but it does not replace my iPad Pro completely: reading Kindle books and academic papers is still better on the iPad Pro.</p>
<p>Reading online reviews, people with ADHD seem to like the reMarkable 2 a lot, because it allows them to focus on what they are doing without distractions. <a href="../posts/2024-01-04-adhd.html">I am not sure yet whether I have ADHD or not</a>, but I likely do, and this may be biasing my opinion towards it.</p>
</section>
<section id="conclusion" class="level1">
<h1>Conclusion</h1>
<p>The reMarkable 2 is, to me, a good replacement for loose paper sheets and Moleskines. A good Moleskine costs $30, so the reMarkable 2, at $299, pays for itself after 10 Moleskines, which is not that far off. It also helps me be more organized, because I can have all my notes in one place, they are always with me and I can easily search them and add to them.</p>
<p>The downsides are still problematic, and it may be better to wait for the next version of the reMarkable. It (barely) works for my use cases, but it may not be for everyone.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>Links to Amazon.com are affiliate links. These help the site stay online.</p>
</div>
</div>
<p>You can buy the reMarkable 2 at <a href="https://l.meyerperin.com/rm2">Amazon</a>.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2023-12-31-remarkable2.html</guid>
  <pubDate>Sun, 31 Dec 2023 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/rm2.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Explaining memes and images with the Semantic Kernel and the OpenAI Vision API</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2023-12-16-skv_py.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>In this article, I’ll explain how to use the Semantic Kernel and the OpenAI Vision API to perform two tasks:</p>
<ol type="1">
<li>Explain why a meme is funny</li>
<li>Identify an animal in an image and pass the results to another function that generates three interesting facts about the animal.</li>
</ol>
<p>The examples in this article are intentionally simple. The goal of the article is to give you a good idea of the capabilities of the Semantic Kernel and the OpenAI Vision API. The plugin can be used for more complex tasks, such as figuring out whether a street intersection is accessible to wheelchairs,whether a person is wearing a mask, or to automatically generate accessible image descriptions.</p>
<p>The code for all examples can be found in the <a href="https://github.com/lucas-a-meyer/sk-vision-py/blob/main/demo.py">demo.py</a> source file in the <a href="https://l.meyerperin.com/b_skvisionpy">GitHub repository</a>.</p>
<section id="explaining-why-memes-are-funny" class="level1">
<h1>Explaining why memes are funny</h1>
<p>In this first example, I will use the plugin to explain why a meme is funny. My code instantiates and calls the <code>Vision</code> plugin. The plugin has a single function, <code>ApplyPromptToImage</code>, which takes a prompt and an image URL as input, and returns the generated text. In my example, the prompt is simply <code>Why is this meme funny?</code>, and I pass an URL of an image of a meme.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1">    </span>
<span id="cb1-2">    kernel <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> sk.Kernel()</span>
<span id="cb1-3">    vision <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> kernel.import_skill(Vision())</span>
<span id="cb1-4"></span>
<span id="cb1-5">    meme_base_url <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"https://raw.githubusercontent.com/lucas-a-meyer/lucas-a-meyer.github.io/main/images/"</span></span>
<span id="cb1-6">    meme_url_list <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"meme1.jpg"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"meme2.png"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"meme3.jpg"</span>] </span>
<span id="cb1-7"></span>
<span id="cb1-8">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> url <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> meme_url_list:</span>
<span id="cb1-9">        variables <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> sk.ContextVariables()</span>
<span id="cb1-10">        variables[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'prompt'</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Why is this meme funny?"</span></span>
<span id="cb1-11">        variables[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'url'</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> meme_base_url <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> url</span>
<span id="cb1-12">        meme <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> kernel.run_async(vision[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'ApplyPromptToImage'</span>], input_vars<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>variables)</span>
<span id="cb1-13">        <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>meme<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)     </span></code></pre></div></div>
<p>Here are the results. The Vision API does a good job of understanding the image and generating a good explanation of why the meme is funny.</p>
<table class="caption-top table">
<colgroup>
<col style="width: 50%">
<col style="width: 50%">
</colgroup>
<thead>
<tr class="header">
<th>Meme</th>
<th>Explanation</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><img src="https://meyerperin.org/images/meme2.png" class="img-fluid" width="300"></td>
<td>The humor in this meme comes from the irony and self-awareness of the situation. The person in the image is depicted as being awake at 3 AM, a time typically associated with sleep, while reading about the importance of sleep. The implication is that while they understand the value of sleep for health and well-being, they are not following this advice themselves. The relatable contradiction between what they’re doing (staying up late) and what they’re reading about (the importance of sleep) creates a humorous juxtaposition that many people can identify with, as it’s common for individuals to do things that go against their own better judgment or knowledge.</td>
</tr>
<tr class="even">
<td><img src="https://meyerperin.org/images/meme3.jpg" class="img-fluid" width="300"></td>
<td>This meme is humorous because it presents a situation where someone has comically circumvented a rule through a technicality. The text explains that while there’s a rule against having pumpkins in their dormitory, there’s no mention of pineapples. The images show a pineapple carved in a manner similar to a jack-o’-lantern, traditionally made from a pumpkin, often for Halloween. The humor arises from the person’s literal interpretation of the rules, exploiting the lack of specificity to create a loophole and do something in the spirit of what’s prohibited yet not technically against the rules. This kind of wit and playful rebellion against overly specific or seemingly arbitrary rules often resonates with people who enjoy clever ways to bend or skirt the rules without breaking them.</td>
</tr>
<tr class="odd">
<td><img src="https://meyerperin.org/images/meme1.jpg" class="img-fluid" width="300"></td>
<td>The humor in this meme arises from the juxtaposition of an inspirational saying typically found in areas of the home where food is prepared or consumed—like the kitchen—and placing it in a completely inappropriate context—the bathroom. Specifically, the framed quote says, “LIFE IS SHORT LICK THE BOWL,” which in a culinary context encourages people to enjoy life to the fullest, perhaps by savoring every last bit of their food. However, when placed in the bathroom, right next to a toilet bowl, the phrase takes on a literal and unpleasantly comical meaning. The idea of licking a toilet bowl is both absurd and humorous, evoking a reaction due to the drastic shift in the context of the phrase.</td>
</tr>
</tbody>
</table>
<p>In the next section, I’ll explain how to use the plugin to identify an animal in an image and pass the results to another function.</p>
</section>
<section id="chaining-the-plugin-answer-to-another-function" class="level1">
<h1>Chaining the plugin answer to another function</h1>
<p>In the example below, I first use the Vision plugin to identify an animal, and then use the results to ask for three interesting facts about an animal. I have createad a semantic function with the prompt <code>Provide three interesting and unusual facts about the animal {{$input}}</code> and called it <code>AnimalFacts</code>.</p>
<p>The URL I’m passing in the code below is a picture of an owl. The Vision API correctly identifies the animal as an owl, and the <code>AnimalFacts</code> function generates three interesting facts about owls.</p>
<p><img src="https://l.meyerperin.com/skv_owl.png" class="img-fluid"></p>
<div class="callout callout-style-default callout-tip callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
<span class="screen-reader-only">Tip</span>Why do this in two steps?
</div>
</div>
<div class="callout-body-container callout-body">
<p>In my example, I’m doing two steps. First I’m using the GPT-4 Vision API in the call that identifies the animal from the image, and then GPT-3.5 in the call that generates the facts.</p>
<p>It would be possible to solve the problem with a single call to the Vision API, but it can be <a href="https://platform.openai.com/docs/guides/vision/calculating-costs">expensive</a>, so I get its results and pass it to a cheaper model for the text generation step.</p>
</div>
</div>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"></span>
<span id="cb2-2">url <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"https://l.meyerperin.com/skv_owl"</span></span>
<span id="cb2-3">gpt35 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> OpenAIChatCompletion(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gpt-3.5-turbo"</span>, api_key, org_id)</span>
<span id="cb2-4">kernel.add_chat_service(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gpt35"</span>, gpt35)</span>
<span id="cb2-5"></span>
<span id="cb2-6">vision <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> kernel.import_skill(Vision())</span>
<span id="cb2-7">plugins <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> kernel.import_semantic_skill_from_directory(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"."</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"plugins"</span>)</span>
<span id="cb2-8"></span>
<span id="cb2-9">variables <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> sk.ContextVariables()</span>
<span id="cb2-10">variables[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'prompt'</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"What animal is this? Please respond in one word."</span></span>
<span id="cb2-11">variables[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'url'</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> url</span>
<span id="cb2-12"></span>
<span id="cb2-13">animal <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> kernel.run_async(vision[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'ApplyPromptToImage'</span>], input_vars<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>variables)</span>
<span id="cb2-14">facts <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> kernel.run_async(plugins[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'AnimalFacts'</span>], input_str<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>(animal))</span>
<span id="cb2-15"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"The animal from the picture is a </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>animal<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)  </span>
<span id="cb2-16"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>facts<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n\n</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>)</span></code></pre></div></div>
<p>From the call above, I got the following result about owls:</p>
<pre class="text"><code>1. Owls have specialized feathers with fringes of varying softness that help muffle 
sound when they fly. Their broad wings and light bodies also make them practically 
silent fliers, which helps them stalk prey more easily.

2. Unlike most birds, owls have both eyes facing forward which gives them better
 depth perception for hunting.

3. Some species of owls, such as the Great Gray Owl, can hear a mouse moving 
underneath a foot of snow from up to 60 feet away. Their ears are asymmetrical,
 with one ear being higher than the other, which helps them locate sounds in 
 multiple dimensions.</code></pre>
</section>
<section id="how-to-write-native-plugin-to-wrap-the-openai-api" class="level1">
<h1>How to write native plugin to wrap the OpenAI API</h1>
<p>In this section, I’ll explain all the steps involved in writing a Semantic Kernel native Python plugin to wrap the OpenAI API. I’ll use the <a href="https://beta.openai.com/docs/guides/vision">Vision API</a> as an example. The plugin code is in the <a href="https://github.com/lucas-a-meyer/sk-vision-py/blob/main/VisionPlugin.py">GitHub repository</a>.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> dotenv <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> load_dotenv</span>
<span id="cb4-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> openai <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> OpenAI</span>
<span id="cb4-3"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> semantic_kernel.skill_definition <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> sk_function, sk_function_context_parameter</span>
<span id="cb4-4"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> semantic_kernel.orchestration.sk_context <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> SKContext</span>
<span id="cb4-5"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> os</span></code></pre></div></div>
<p>We need to import the OpenAI library, the SKContext class because our function has multiple parameters (the prompt and the image URL), and the decorators in <code>skill_definition</code> that will allow us to make the Python function visible to the semantic Kernel.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> Vision:</span>
<span id="cb5-2">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@sk_function</span>(</span>
<span id="cb5-3">        description<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""Asks the GPT-4 Vision API to perform an operation described by the prompt</span></span>
<span id="cb5-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">        on an image given its url"""</span>,</span>
<span id="cb5-5">        name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ApplyPromptToImage"</span></span>
<span id="cb5-6">    )</span>
<span id="cb5-7">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@sk_function_context_parameter</span>(</span>
<span id="cb5-8">        name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"prompt"</span>,</span>
<span id="cb5-9">        description<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"The prompt you want to send to the Vision API"</span>,</span>
<span id="cb5-10">    )</span>
<span id="cb5-11">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@sk_function_context_parameter</span>(</span>
<span id="cb5-12">        name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"url"</span>,</span>
<span id="cb5-13">        description<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span>,</span>
<span id="cb5-14">    )</span></code></pre></div></div>
<p>The main code of the function consists in assembling a message that conforms to the <a href="https://beta.openai.com/docs/guides/vision">Vision API specification</a>. The message is then sent to the API, and the result is parsed and returned to the caller.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb6-1">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> ApplyPromptToImage(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, context: SKContext) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>:</span>
<span id="cb6-2">        load_dotenv()</span>
<span id="cb6-3">        client <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> OpenAI(api_key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>os.getenv(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"OPENAI_API_KEY"</span>))</span>
<span id="cb6-4"></span>
<span id="cb6-5">        response <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.chat.completions.create(</span>
<span id="cb6-6">        model<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gpt-4-vision-preview"</span>,</span>
<span id="cb6-7">        messages<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[</span>
<span id="cb6-8">            {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"role"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"user"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"content"</span>: [</span>
<span id="cb6-9">                {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"type"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"text"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"text"</span>: <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>context[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'prompt'</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>},</span>
<span id="cb6-10">                {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"type"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"image_url"</span>,<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"image_url"</span>: {<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"url"</span>: <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>context[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'url'</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>,</span>
<span id="cb6-11">            },},],}],</span>
<span id="cb6-12">        max_tokens<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">300</span>,</span>
<span id="cb6-13">        )</span>
<span id="cb6-14"></span>
<span id="cb6-15">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> response.choices[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>].message.content</span></code></pre></div></div>
<p>The full code for the plugin and associated demo can be found in the <a href="https://l.meyerperin.com/b_skvisionpy">GitHub repository</a>.</p>
</section>
<section id="have-i-ever-thought-about-creating-an-app-that-does-this-as-a-service" class="level1">
<h1>Have I ever thought about creating an app that does this as a service?</h1>
<p>Yes, and with the blog post above, you can easily create one yourself. It will probably take you less than an hour to get it working. The main challenge is to bill for it, as the Open AI Vision calls can get expensive very quickly.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2023-12-16-skv_py.html</guid>
  <pubDate>Tue, 26 Dec 2023 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/meme2.png" medium="image" type="image/png" height="121" width="144"/>
</item>
<item>
  <title>A Quick Tour of the Semantic Kernel</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2023-10-15-semantic_kernel.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>The <a href="https://github.com/microsoft/semantic-kernel">Microsoft Semantic Kernel</a> is a thin, open-source, software development toolkit that makes it easier for applications to interact with AI services.</p>
<p>It was originally designed to power the Microsoft Copilots, such as Microsoft 365 and Bing. The initial version was in C#, but it has now been extended to Python and Java, and released to the developer community as an open-source package.</p>
<section id="python-code-for-this-post" class="level2">
<h2 class="anchored" data-anchor-id="python-code-for-this-post">Python code for this post</h2>
<p>The Python code for this post is <a href="https://links.meyerperin.com/sktour">available in a notebook on GitHub</a>.</p>
<p>In order to run the code of this post, you will need an <a href="https://azure.microsoft.com/en-us/products/ai-services/openai-service">Azure subscription with access to the OpenAI API</a>, or an OpenAI subscription.</p>
<p>Although you can get a <a href="https://azure.microsoft.com/en-us/free/ai-services">free trial for Azure</a> that gives you many services, the OpenAI services in Azure are not free. You can see the prices <a href="https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/">here</a>.</p>
</section>
<section id="differences-from-langchain" class="level2">
<h2 class="anchored" data-anchor-id="differences-from-langchain">Differences from LangChain</h2>
<ul>
<li>Semantic Kernel was designed to be more customizable than LangChain. It gives you more control but requires more coding.</li>
<li>Semantic Kernel was designed to help easily add LLM features to enterprise or large-scale consumer applications.</li>
<li>If you use Python, you can use both LangChain and Semantic Kernel.</li>
<li>If you use JavaScript, you can use LangChain, but not Semantic Kernel. There’s a community-supported TypeScript API for Semantic Kernel, but it’s not officially supported by Microsoft.</li>
<li>If you use Java or .NET, you can use Semantic Kernel, but not LangChain.</li>
</ul>
</section>
</section>
<section id="a-whirlwind-tour-through-semantic-kernel-in-python" class="level1">
<h1>A whirlwind tour through Semantic Kernel in Python</h1>
<p>In the post below, I’ll quickly show how to get started with Semantic Kernel in Python using an Azure subscription.</p>
<section id="the-kernel" class="level2">
<h2 class="anchored" data-anchor-id="the-kernel">The Kernel</h2>
<p>The Semantic Kernel is just a lightweight object where you will attach everything you need to complete your AI tasks.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> semantic_kernel <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> sk</span>
<span id="cb1-2">kernel <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> sk.Kernel()</span></code></pre></div></div>
</section>
<section id="connectors" class="level2">
<h2 class="anchored" data-anchor-id="connectors">Connectors</h2>
<p>Connectors are the way you connect to AI services. You can connect multiple services to the same kernel, which allows you to perform a complex task using different services for each step.</p>
<p>For example, my subscription has two models deployed: one deployment named <code>gpt35</code> the GPT 3.5 Turbo model and one deployment named <code>gpt4</code> for a deployment of the GPT-4 model. I can load both of them into the kernel with the code below:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> semantic_kernel.connectors.ai.open_ai <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> AzureChatCompletion</span>
<span id="cb2-2"></span>
<span id="cb2-3">gpt35 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> AzureChatCompletion(deployment_name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gpt35"</span>, <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># yours may be different</span></span>
<span id="cb2-4">endpoint<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>OPENAI_ENDPOINT,</span>
<span id="cb2-5">api_key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>OPENAI_API_KEY)</span>
<span id="cb2-6"></span>
<span id="cb2-7">gpt4 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> AzureChatCompletion(</span>
<span id="cb2-8">    deployment_name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gpt4"</span>, <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># yours may be different</span></span>
<span id="cb2-9">endpoint<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>OPENAI_ENDPOINT,</span>
<span id="cb2-10">api_key<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>OPENAI_API_KEY)</span>
<span id="cb2-11"></span>
<span id="cb2-12">kernel.add_chat_service(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gpt35"</span>, gpt35)</span>
<span id="cb2-13">kernel.add_chat_service(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gpt4"</span>, gpt4)</span></code></pre></div></div>
</section>
<section id="semantic-functions" class="level2">
<h2 class="anchored" data-anchor-id="semantic-functions">Semantic functions</h2>
<p>Semantic functions are functions that use large language models to perform a task. The simple example below shows how to create a semantic function that tells a knock-knock joke.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1">prompt <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""knock, knock? Who’s there? </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{{</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">$input</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">. </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{{</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">$input</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}}</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> who?"""</span></span>
<span id="cb3-2"></span>
<span id="cb3-3">knock <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> kernel.create_semantic_function(prompt, temperature<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.8</span>)</span>
<span id="cb3-4"></span>
<span id="cb3-5">response <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> knock(“Dishes<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">")</span></span>
<span id="cb3-6"><span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">print</span>(response)</span></code></pre></div></div>
<p>The response will be something like <code>Dishes the police, open up!</code>.</p>
</section>
<section id="native-functions" class="level2">
<h2 class="anchored" data-anchor-id="native-functions">Native functions</h2>
<p>Native functions are regular Python functions. They can be used to perform any task that doesn’t require a large language model. For example, the code below classifies an image given a URL.</p>
<p>The function uses the <code>timm</code> library to download a pre-trained model and classify the image. The <code>@sk_function</code> decorator tags the following function <code>classify_image</code> as a function that can be imported by the Semantic Kernel.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> requests</span>
<span id="cb4-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> PIL <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> Image</span>
<span id="cb4-3"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> timm</span>
<span id="cb4-4"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> timm.data.imagenet_info <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> ImageNetInfo</span>
<span id="cb4-5"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> semantic_kernel.skill_definition <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> sk_function</span>
<span id="cb4-6"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> semantic_kernel.orchestration.sk_context <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> SKContext</span>
<span id="cb4-7"></span>
<span id="cb4-8"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> ImageClassifierPlugin:</span>
<span id="cb4-9">&nbsp; &nbsp; <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>):</span>
<span id="cb4-10">&nbsp; &nbsp; &nbsp; &nbsp; <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.model <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> timm.create_model(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"convnext_tiny.in12k_ft_in1k"</span>, pretrained<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb4-11">&nbsp; &nbsp; &nbsp; &nbsp; <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.model.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">eval</span>()</span>
<span id="cb4-12">&nbsp; &nbsp; &nbsp; &nbsp; data_config <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> timm.data.resolve_model_data_config(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.model)</span>
<span id="cb4-13">&nbsp; &nbsp; &nbsp; &nbsp; <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.transforms <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> timm.data.create_transform(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span>data_config, is_training<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>)</span>
<span id="cb4-14">&nbsp; &nbsp; &nbsp; &nbsp; <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.imagenet_info <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> ImageNetInfo()</span>
<span id="cb4-15"></span>
<span id="cb4-16">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">@sk_function</span>(</span>
<span id="cb4-17">&nbsp; &nbsp; &nbsp; &nbsp; description<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Takes a url as an input and classifies the image"</span>,</span>
<span id="cb4-18">&nbsp; &nbsp; &nbsp; &nbsp; name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"classify_image"</span>,</span>
<span id="cb4-19">&nbsp; &nbsp; &nbsp; &nbsp; input_description<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"The url of the image to classify"</span>,</span>
<span id="cb4-20">    )</span>
<span id="cb4-21">&nbsp; &nbsp; <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> classify_image(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, url: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>:</span>
<span id="cb4-22">&nbsp; &nbsp; &nbsp; &nbsp; image <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.download_image(url)</span>
<span id="cb4-23">&nbsp; &nbsp; &nbsp; &nbsp; pred <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.model(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.transforms(image)[<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>])</span>
<span id="cb4-24">&nbsp; &nbsp; &nbsp; &nbsp; cls <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.imagenet_info.index_to_description(pred.argmax())</span>
<span id="cb4-25">&nbsp; &nbsp; &nbsp; &nbsp; </span>
<span id="cb4-26">&nbsp; &nbsp; &nbsp; &nbsp; <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> cls.split(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">","</span>)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb4-27">&nbsp; &nbsp; &nbsp; &nbsp; </span>
<span id="cb4-28">  &nbsp; <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> download_image(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, url):</span>
<span id="cb4-29">&nbsp; &nbsp; &nbsp; &nbsp; <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> Image.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">open</span>(requests.get(url, stream<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>).raw).convert(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"RGB"</span>)</span></code></pre></div></div>
<section id="loading-a-native-function-into-the-kernel" class="level3">
<h3 class="anchored" data-anchor-id="loading-a-native-function-into-the-kernel">Loading a native function into the kernel</h3>
<p>You can load native functions into the kernel with the <code>import_skill</code> method of the Kernel object.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1">classify_plugin <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> kernel.import_skill(image_classifier, skill_name<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"classify_image"</span>)</span></code></pre></div></div>
<p>The <code>classify_plugin</code> is a collection of functions. To call the <code>classify_image</code> function, you can use the code below:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb6-1">answer <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> classify_plugin.classify_image(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"https://links.meyerperin.com/tiger_jpg"</span>)</span>
<span id="cb6-2"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(answer)</span></code></pre></div></div>
<p>For example, if I use the URL below as the parameter:</p>
<p><code>https://links.meyerperin.com/tiger_jpg</code></p>
<p><img src="http://2.bp.blogspot.com/-tG6z7DOsHNc/T6Z1DuzXs9I/AAAAAAAAGfY/YTmFDxw0Qxg/s320/animal+pictures+%252812%2529.jpg" class="img-fluid"></p>
<p>The answer will be <code>tiger</code>.</p>
</section>
</section>
</section>
<section id="plug-ins" class="level1">
<h1>Plug-ins</h1>
<p>One of the greatest strengths of the Microsoft Semantic Kernel is the ability of creating plugins. Plugins are collections of functions that can be imported into the kernel.</p>
<p>A semantic plugin is a collection of semantic functions. Each function should be in its own directory. Each directory should have two files: <code>config.json</code> and <code>skprompt.txt</code>.</p>
<p>The <code>config.json</code> file contains the configuration of the semantic function: which is the preferred engine to use, the temperature, etc.</p>
<p>The <code>skprompt.txt</code> file contains the prompt of the semantic function. The prompt is the text that will be sent to the engine to generate the response.</p>
<section id="example-directory-structure" class="level2">
<h2 class="anchored" data-anchor-id="example-directory-structure">Example directory structure</h2>
<pre><code>└───plugins
    └───jokes
        ├───cross_the_road_joke
        |   ├───config.json
        |   └───skprompt.txt
        └───knock_knock_joke
            ├───config.json
            └───skprompt.txt</code></pre>
</section>
<section id="an-example-config.json-file" class="level2">
<h2 class="anchored" data-anchor-id="an-example-config.json-file">An example config.json file</h2>
<p>The configuration file below shows a possible configuration for a semantic function that generates knock-knock jokes. The <code>default_services</code> property is an array of the preferred engines to use. The <code>completion</code> property contains the parameters that will be sent to the engine. The <code>input</code> property contains the parameters that will be sent to the semantic function.</p>
<p>I frequently use the <code>default_services</code> property to send simple tasks to cheaper services like GPT-3.5 and more complex tasks to more expensive services like GPT-4.</p>
<p>The <code>description</code> field is important, because it can be used by the <code>Planner</code> function of the kernel, if you want to let the kernel itself select which functions to call.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode json code-with-copy"><code class="sourceCode json"><span id="cb8-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb8-2">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"schema"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-3">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"type"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"completion"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-4">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"description"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Generates a knock-knock joke based on user input"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-5">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"default_services"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">[</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gpt35"</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">]</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-6">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"completion"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb8-7">        <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"temperature"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.8</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-8">        <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"number_of_responses"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-9">        <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"top_p"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-10">        <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"max_tokens"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4000</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-11">        <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"presence_penalty"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-12">        <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"frequency_penalty"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.0</span></span>
<span id="cb8-13">       <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb8-14">       <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"input"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb8-15">     <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"parameters"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">[</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb8-16">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"name"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"input"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-17">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"description"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"The topic that the joke should be written about"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-18">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"defaultValue"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Dishes"</span></span>
<span id="cb8-19">     <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">}</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">]</span></span>
<span id="cb8-20">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb8-21"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div></div>
</section>
<section id="an-example-skprompt.txt-file" class="level2">
<h2 class="anchored" data-anchor-id="an-example-skprompt.txt-file">An example skprompt.txt file</h2>
<p>The prompt below is the prompt for the semantic function that generates knock-knock jokes. The <code>{$input}</code> placeholder will be replaced by the value of the <code>input</code> parameter.</p>
<pre class="text"><code>knock, knock? 
Who's there? 
{{$input}}. 
{{$input}} who?


Repeat the whole setup and finish the joke.</code></pre>
</section>
<section id="loading-the-plugin-into-the-kernel" class="level2">
<h2 class="anchored" data-anchor-id="loading-the-plugin-into-the-kernel">Loading the plugin into the kernel</h2>
<p>You can load all the functions that are inside a plugin directory with the <code>import_semantic_skill_from_directory</code> method of the kernel object.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb10-1">jokes_plugin <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> kernel.import_semantic_skill_from_directory(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"plugins"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"jokes"</span>)</span></code></pre></div></div>
<p>The resulting object is a dictionary of functions. You can load the functions into variables or call them directly.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb11-1">knock <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> jokes_plugin[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"knock_knock_joke"</span>]</span>
<span id="cb11-2"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span> (knock(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Dishes"</span>))</span>
<span id="cb11-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Dishes the police, open up!</span></span>
<span id="cb11-4"></span>
<span id="cb11-5"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(jokes_plugin[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"cross_the_road_joke"</span>](<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Tiger"</span>))</span>
<span id="cb11-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Why did the tiger cross the road? To show the chicken it can be done.</span></span></code></pre></div></div>
</section>
</section>
<section id="calling-multiple-functions-in-sequence" class="level1">
<h1>Calling multiple functions in sequence</h1>
<p>As you saw before, we loaded the native function classify_image into the kernel. And in the block above, we loaded the jokes plugin into the kernel. We can call both functions in sequence with the code below:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb12-1">context <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> kernel.create_new_context()</span>
<span id="cb12-2">context[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"input"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> url</span>
<span id="cb12-3"></span>
<span id="cb12-4">response <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">await</span> kernel.run_async(</span>
<span id="cb12-5">    classify_plugin[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"classify_image"</span>],</span>
<span id="cb12-6">    jokes_plugin[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"cross_the_road_joke"</span>],</span>
<span id="cb12-7">    input_context<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>context</span>
<span id="cb12-8">)</span>
<span id="cb12-9"></span>
<span id="cb12-10"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(response)</span></code></pre></div></div>
<p>This code will call the first (native) function that classifies an image, and then call the second (semantic) function that generates a joke about the image. Since we loaded the image of a tiger, the response will be something like:</p>
<p><code>Why did the tiger cross the road? To show the chicken it could be done.</code></p>
</section>
<section id="conclusion" class="level1">
<h1>Conclusion</h1>
<p>I hope this brief tour helps you get started. If you want to learn more, you can check the <a href="https://learn.microsoft.com/en-us/semantic-kernel/get-started/quick-start-guide/getting-started?tabs=python">official documentation</a>.</p>
<p>You can also join the community <a href="https://aka.ms/SKDiscord">Discord server</a>.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2023-10-15-semantic_kernel.html</guid>
  <pubDate>Sun, 15 Oct 2023 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/tiger.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Getting Started with Midjourney</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2023-09-23-midjourney.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p><a href="https://www.midjourney.com/">Midjourney</a> is a generative AI tool that can generate high quality images from text prompts. It is particularly good at generating photorealistic images, and also good at blending an existing picture with a prompt. Lots of people use Midjourney as a digital diary, or to quickly create images for their social media and blog posts.</p>
<p>One unique characteristic of Midjourney is that its interface is only available on <a href="https://discord.com/">Discord</a>, a popular chat app. In order to create images, you have to chat with the Midjourney bot on a Discord server.</p>
<p>This is a tutorial on how to get started with Midjourney. While it’s mostly based on the <a href="https://docs.midjourney.com/docs/quick-start">official Midjourney getting started guide</a>, I have added a few suggestions that made my life much easier, especially my Step 2. Hopefully will help you, too.</p>
</section>
<section id="installing-midjourney" class="level1">
<h1>Installing Midjourney</h1>
<section id="step-1-log-in-to-discord" class="level2">
<h2 class="anchored" data-anchor-id="step-1-log-in-to-discord">Step 1: Log in to Discord</h2>
<p>If you don’t have a Discord account, you will have to <a href="https://support.discord.com/hc/en-us/articles/360033931551-Getting-Started">create one</a>. Discord has apps for all major operating systems, mobile devices, and also a web app. Install all apps you think you’re going to use. I frequently use the desktop app the most, but the mobile app is also very convenient and works surprisingly well for chatting with the Midjourney bot.</p>
</section>
<section id="step-2-an-easier-way-create-your-own-server" class="level2">
<h2 class="anchored" data-anchor-id="step-2-an-easier-way-create-your-own-server">Step 2: An easier way: create your own server</h2>
<p>This is where my instructions are <strong>very different</strong> from the official guide. The official guide asks you to join the <a href="https://discord.gg/2YJYJQ6Z">Midjourney Discord server</a> and chat with the Midjourney bot there. I don’t recommend doing that. The Midjourney Discord server is very active and noisy, and you will get <strong>a lot</strong> of notifications from other people’s conversations. It’s also very easy to get lost in the sea of messages. You can work around that by <a href="https://docs.midjourney.com/docs/direct-messages">directly messaging the Midjourney bot</a>, but you will still have to configure the server settings to mute all channels and notifications.</p>
<p>Although it may sound intimidating, creating your own server is very easy and it’s mostly click-based. You can create a server for free, and you can create as many servers as you want. You can see detailed instructions <a href="https://support.discord.com/hc/en-us/articles/204849977-How-do-I-create-a-server-">here</a>. Here’s a quick summary:</p>
<section id="click-the-button-on-the-left-sidebar-to-add-a-server" class="level3">
<h3 class="anchored" data-anchor-id="click-the-button-on-the-left-sidebar-to-add-a-server">1. Click the <code>+</code> button on the left sidebar to <code>Add a Server</code></h3>
<p><img src="https://support.discord.com/hc/article_attachments/360058897831/Screen_Shot_2020-06-04_at_1.11.06_PM.png" class="img-fluid"></p>
</section>
<section id="select-create-a-server" class="level3">
<h3 class="anchored" data-anchor-id="select-create-a-server">2. Select <code>Create a server</code></h3>
<p><img src="https://support.discord.com/hc/article_attachments/360058273791/Capture.JPG" class="img-fluid"></p>
</section>
<section id="give-your-server-a-name-and-optionally-an-icon-and-click-create" class="level3">
<h3 class="anchored" data-anchor-id="give-your-server-a-name-and-optionally-an-icon-and-click-create">3. Give your server a name and optionally an icon, and click <code>Create</code></h3>
<p><img src="https://support.discord.com/hc/article_attachments/360058897871/Screen_Shot_2020-06-04_at_1.40.12_PM.png" class="img-fluid"></p>
</section>
<section id="thats-it" class="level3">
<h3 class="anchored" data-anchor-id="thats-it">4. That’s it!</h3>
<p>You have created your own server. Now you need to subscribe to Midjourney and invite the Midjourney bot to your server.</p>
</section>
</section>
<section id="step-3-subscribe-to-midjourney" class="level2">
<h2 class="anchored" data-anchor-id="step-3-subscribe-to-midjourney">Step 3: Subscribe to Midjourney</h2>
<p>This is the hard part. There are no free trials anymore, and you need a paid subscription in order to use Midjourney.</p>
<ul>
<li>Visit <a href="https://midjourney.com/account">Midjourney.com/account</a>.</li>
<li>Sign in using your <strong>verified</strong> Discord account.</li>
<li>Select a subscription tier and click <code>Subscribe</code>.</li>
</ul>
<p>The least expensive subscription is good for most purposes, and I suggest you use that one to get started. This will give you about three hours of server time per month, and you lose what you don’t use. Even though I use Midjourney a lot, I rarely use up all my server time. You <strong>can</strong> top off if you need.</p>
<p><img src="https://cdn.document360.io/3040c2b6-fead-4744-a3a9-d56d621c6c7e/Images/Documentation/MJ_SubscriptionTiers.png" class="img-fluid"></p>
</section>
<section id="step-4-invite-the-midjourney-bot-to-your-server" class="level2">
<h2 class="anchored" data-anchor-id="step-4-invite-the-midjourney-bot-to-your-server">Step 4: Invite the Midjourney bot to your server</h2>
<p>Now that you have a server and a subscription, you need to invite the Midjourney bot to your server. This is also very easy, and done mostly by clicking.</p>
<ol type="1">
<li><p>In the left sidebar, right-click your server and select <code>App catalog</code> <img src="https://meyerperin.org/images/MJ-1.png" class="img-fluid"></p></li>
<li><p>Search for <code>Midjourney</code>, select <code>Midjourney Bot</code>, and click <code>Add to server</code> <img src="https://meyerperin.org/images/MJ-2.png" class="img-fluid"></p></li>
</ol>
<p><img src="https://meyerperin.org/images/MJ-3.png" class="img-fluid"></p>
<ol start="3" type="1">
<li>Select your server from the dropdown, and click <code>Continue</code>, and then <code>Authorize</code>. <img src="https://meyerperin.org/images/MJ-4.png" class="img-fluid"></li>
</ol>
<p>You can now start using Midjourney in your own.</p>
</section>
</section>
<section id="basic-usage-of-midjourney" class="level1">
<h1>Basic usage of Midjourney</h1>
<p>Now that Midjourney is installed on your server, you can start using it. I just send direct messages to it by right-clicking it and selecting <code>Message</code>.</p>
<p><img src="https://meyerperin.org/images/MJ-5.png" class="img-fluid"></p>
<p>The official guide has a <a href="https://docs.midjourney.com/docs/quick-start">very good introduction</a> to the basic usage of Midjourney, starting on step 5. I will just summarize the main command <code>imagine</code> here, and show you an example of how you can use your own image as a starting point.</p>
<section id="the-imagine-command" class="level2">
<h2 class="anchored" data-anchor-id="the-imagine-command">The <code>imagine</code> command</h2>
<p>The <code>imagine</code> command is the main command you will use to generate images. It takes a prompt as input, and generates an image based on the prompt. The prompt can be a few words, a sentence, or even a paragraph. The longer the prompt, the more specific the image will be.</p>
<p>There are a lot of prompting techniques, and I will not go into details here. You can find a lot of examples in the <a href="https://docs.midjourney.com/docs/explore-prompting">official guide</a>.</p>
<p>Here’s a quick example:</p>
<p><code>/imagine standard schnauzer sleeping on a computer keyboard</code></p>
<p>Midjourney by default will reply with four suggestions. Their numbers start on the top left and go clockwise. There will be some buttons under the suggestions, labeled U1, U2, U3, U4 and V1, V2, V3, V4.</p>
<p>You can click on the U buttons to upscale (increase the resolution) of a picture you liked, and click on the V buttons to see more variations of the current suggestion. If you’re going to use the image for social media, I suggest you upscale it before downloading.</p>
<p>There’s also a button to regenerate all images if you don’t like any of the suggestions. Here’s what I got:</p>
<p><img src="https://meyerperin.org/images/MJ-6.png" class="img-fluid"></p>
<p>I like the second one, so I clicked on the U2 button to upscale it. Then I clicked on the download button to download the image. Here’s the final result:</p>
<p><img src="https://meyerperin.org/images/MJ-7.png" class="img-fluid"></p>
</section>
<section id="using-an-existing-image" class="level2">
<h2 class="anchored" data-anchor-id="using-an-existing-image">Using an existing image</h2>
<p>You can use an existing image with the <code>/imagine</code> command. I frequently use my own headshot and ask Midjourney to put me in some weird place or situation. In order for Midjourney to use your image, you need to upload it to the web first and give Midjourney the URL. I have my own server, but you can also upload your image to Discord and use the URL Discord gives you, or upload it to GitHub. There are plenty of other services you can use, too, like <a href="https://imgur.com/">Imgur</a>.</p>
<p>My headshot is at <code>https://links.meyerperin.com/headshot</code>.</p>
<p><img src="https://meyerperin.org/images/lucas_new_headshot.png" class="img-fluid"></p>
<p>I can use it with the <code>/imagine</code> command like this:</p>
<p><code>/imagine https://links.meyerperin.com/headshot as Batman in Gotham City</code></p>
<p>And here is the final result:</p>
<p><img src="https://meyerperin.org/images/another-lucas-batman.png" class="img-fluid"></p>
</section>
</section>
<section id="conclusion" class="level1">
<h1>Conclusion</h1>
I hope this tutorial helped you get started with Midjourney. If you have any questions, please feel free to drop me a message using my socials above (LinkedIn and Threads are my preferred ones).
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2023-09-23-midjourney.html</guid>
  <pubDate>Sat, 23 Sep 2023 00:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/lucas_batman.png" medium="image" type="image/png" height="144" width="144"/>
</item>
<item>
  <title>Getting started with Midjourney</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2023-09-22-getting-started-with-midjourney.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>I’ve added a new section to my blog, called <a href="https://links.meyerperin.com/mj">A Quick Tour of Midjourney</a>.</p>
<p>You can find it here: <a href="https://links.meyerperin.com/mj" class="uri">https://links.meyerperin.com/mj</a>.</p>
<p>If you never used Midjourney before, or if you tried to use it but got quickly intimidated by the volume of messages in their Discord server, this will probably help.</p>
<p>An important note: Midjourney is now a paid service, so you have to pay for a subscription (currently $10/month) to use it. There are no trials anymore.</p>
</article>



 ]]></description>
  <guid>https://meyerperin.org/posts/2023-09-22-getting-started-with-midjourney.html</guid>
  <pubDate>Fri, 22 Sep 2023 06:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/lucas-mj.png" medium="image" type="image/png" height="144" width="144"/>
</item>
<item>
  <title>A/B testing needs a theory</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2023-06-11-ab-tests.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>Randomized controlled experiments (A/B tests) are frequently said to be the gold standard for decision-making. However, to make a good decision, you have to really understand what question is being answered.</p>
<p>Let’s say, hypothetically, that you want to try a new color palette for your website. You have the mechanisms to run proper A/B tests: you can bucket users into A and B groups such that the only difference between them is the color palette. You choose, arbitrarily, a random palette, run the experiment for a week, and find that the users in the B group have a $200k lift in revenue, which annualized represents a $10M increase in revenue. That’s not nothing. You roll it out to all users, write your performance review, book a party and wait for your promotion.</p>
<p>There’s just one important thing missing: you have no idea exactly why it worked. Here’s a few possible explanations, some of which are quite far-fetched:</p>
<ul>
<li><p>Maybe the new palette just randomly made your site look more like its main competitor, and users that would normally leave your site keep using it, blissfully unaware. The next time your competitor changes their color schema, these users will not be confused anymore and will leave. And maybe they’ll do this next week, because they just realized they lost some users.</p></li>
<li><p>Maybe the color palette of the world outside makes your new palette very attractive, but only for the season when the test ran, but it makes it less attractive in other seasons of the year. With the change you’re actually losing money for the year. That’s a little bit of a joke explanation, but it has a strong scientific basis (the “crocs and socks illusion”, also the “dress illusion”).</p></li>
<li><p>Maybe you found a cheat code, the exact combination of colors that makes other people understand you better, and could use this to improve educational outcomes and save the whole world $1T per year, rather than $200k for that week. By deploying it to your site and moving on, you have a lost opportunity cost of approximately $1T.</p></li>
<li><p>Maybe you really found something that only makes it easier for users to use your site and that it’s going to work well for one year or more, so it’s really valued at $10M per year… as long as you don’t make any other changes to the site.</p></li>
</ul>
<p>Your A/B experiment is on the top of the evidence pyramid, but for which of the cases above? Without a theory and alternative explanations, you may never know.</p>
<p>There is, however, a hint that you learned something, and that it’s familiar to data scientists: the descriptive, predictive and prescriptive/causal framework. When you really understand how something works, if you have the resources, you can make it happen at will. For example, if the reason why you got a lift in your experiment is because you were looking like your much larger competitor, you could now mimic their color palette every time they make a change and keep getting the lift. You could mimic palettes of other larger competitors and get lifts there, too.</p>
A/B tests are great evidence when you understand the question. Better make sure you do.
</article>



 ]]></description>
  <guid>https://meyerperin.org/posts/2023-06-11-ab-tests.html</guid>
  <pubDate>Tue, 13 Jun 2023 06:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/schnauzer_scientist.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Google Duplex was going to change the world five years ago</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2023-05-24-google-duplex-was-going-to-change-the-world-five-years-ago.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<section id="google-duplex-was-going-to-change-the-world-five-years-ago" class="level1">
<h1>Google Duplex was going to change the world five years ago</h1>
<section id="introduction" class="level2">
<h2 class="anchored" data-anchor-id="introduction">Introduction</h2>
<p>In 2018, Google announced its artificial intelligence (AI) system called Google Duplex, which was designed to make phone calls on behalf of users for tasks such as booking haircut appointments or making restaurant reservations. The technology sparked both excitement and concern, as people worried about the potential implications of AI taking over jobs and being used for social engineering purposes.</p>
</section>
<section id="the-reality-of-google-duplex" class="level2">
<h2 class="anchored" data-anchor-id="the-reality-of-google-duplex">The Reality of Google Duplex</h2>
<p>As with many new technologies, the initial hype surrounding Google Duplex has given way to a more nuanced understanding of its capabilities and limitations. One user review revealed that the actual experience of using Duplex was less than ideal, as it took longer to set up the AI system to make a call than it would have taken to simply make the call themselves. Google later admitted that at least 25% of calls made by Duplex failed and required human intervention, highlighting the limitations of the technology. Furthermore, the COVID-19 pandemic has significantly reduced the need for booking services. Dropping my kid at a high school in 2022 quickly confirmed to me that haircuts were not a priority.</p>
</section>
<section id="duplexs-integration-and-current-state" class="level2">
<h2 class="anchored" data-anchor-id="duplexs-integration-and-current-state">Duplex’s Integration and Current State</h2>
<p>Google Duplex has since been integrated into Pixel smartphones and the Google Assistant, allowing users to access the AI system more easily. However, user reviews on platforms like Reddit have been mixed, with many expressing the sentiment that Duplex is “great when it works.” A few people have mentioned that they have had success using Duplex to make restaurant reservations, but not all restaurants accept it.</p>
</section>
<section id="large-language-models-and-the-future" class="level2">
<h2 class="anchored" data-anchor-id="large-language-models-and-the-future">Large Language Models and the Future</h2>
<p>When Duplex was demonstrated by Google’s CEO in 2018, it seemed ready to take over the world. It’s a good reminder that demonstrations are usually very scripted and tested, and that may give the impression that things work a lot better than they would work in the real world.</p>
<p>This is also a good reminder to take a lot of what you see about Large Language Models with a grain of salt. LLMs can do amazing things, but they are not perfect, and in many cases, they are still very imperfect. Emerging technologies like chaining and goal-seeking could significantly improve the capabilities of systems based in language models, allowing them to better understand and improve automatically, but most products I see on my day-to-day are just thin wrappers on top of an existing model like ChatGPT, with minimal engineering. I don’t think GPT is going to kill engineering. I’m entirely on the opposite side of the spectrum: I think it’s going to make engineering more important than ever. Somebody will need to integrate and monitor these models and their applications, and that’s not going to be a trivial task.</p>
</section>
<section id="conclusion" class="level2">
<h2 class="anchored" data-anchor-id="conclusion">Conclusion</h2>
<p>If you see the pace of AI technology releases releases and you get caught up in the hype, you may be worried like the people were worried about Duplex in 2018. Take your time to learn about new technologies. Be intentional: it’s definitely worthwhile understanding what LLMs are and are their advantages and limitations, but not necessarily optimal worrying about every new application, like AutoGPT. Remember the Vicki Boykis rule: whatever is important will be here six months from now, a lot will not.</p>
</section></section></article>





 ]]></description>
  <guid>https://meyerperin.org/posts/2023-05-24-google-duplex-was-going-to-change-the-world-five-years-ago.html</guid>
  <pubDate>Wed, 24 May 2023 06:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/schnauzer_haircut.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>The data entry error that cost hundreds of millions</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2022-12-08-mizuho-securities.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>Have you ever had a bad day at work? On December 8th, 2005, a trader at Mizuho Securities in Japan probably had a worse one.</p>
<p>That Thursday, a trader wanted to sell one share of J-Com for 610,000 yen (back then, this was about $3,600). However, when placing the order, they swapped the quantity and price, and ended up offering to sell 610,000 shares for 1 yen each. The company tried to cancel the order on the Tokyo Stock Exchange unsuccessfuly. People scooped the cheaply priced stock and resold it for instant profit.</p>
<p>The next day, the president of Mizuho said in a press release: “Someone unintentionally ignored the alerts.” It’s a good sign that there were alerts.</p>
Every December 8th, I raise a glass to that unnamed trader and count my blessings.
</article>



 ]]></description>
  <category>technology</category>
  <category>cautionary tales</category>
  <guid>https://meyerperin.org/posts/2022-12-08-mizuho-securities.html</guid>
  <pubDate>Thu, 08 Dec 2022 06:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/mizuho-post.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Separating LeetCode grinders from coders</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2022-07-22-leetcode.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>My preferred interview type is the technical phone screen. I can track my performance even without sophisticated reporting.</p>
<p>I also face an interesting problem: how to separate LeetCode “grinders” from people who can code.</p>
<section id="measuring-screener-performance" class="level2">
<h2 class="anchored" data-anchor-id="measuring-screener-performance">Measuring screener performance</h2>
<p>I wrote before about the <a href="https://www.meyerperin.com/posts/2021-06-05-what-happens-when-a-hiring-manager-posts-a-position-on-linkedin.html">many steps in an interview process</a>. The last few steps of the process are: technical phone screen, on-site interview (now done remotely as well), and offer.</p>
<p>A technical screener can have a good idea of their performance by observing the results of the on-site interview step. If the screener is too lenient, lots of people will pass the phone screen but fail the on-site interview. The opposite is not necessarily a good sign. If the pass rate on the phone screen is low but the pass rate on the on-site interview is high, it could mean that the screener is too strict, but it could also mean that the candidate pool is bad.</p>
<p>Tech screeners should usually be <a href="https://www.meyerperin.com/posts/2021-07-02-how-to-fail-a-technical-phone-screen.html">more experienced than the average interviewer</a>. Having a screener that is too lenient is costly, as many people will talk to a candidate that passes a technical phone screen.</p>
</section>
<section id="the-problem-with-leetcoding" class="level2">
<h2 class="anchored" data-anchor-id="the-problem-with-leetcoding">The problem with LeetCoding</h2>
<p>Some people use sites like LeetCode, StrataScratch and HackerRank to learn, but many use them to “grind” and simply memorize algorithms for interviews. Now that interviews are remote, some people cheat and copy the answers from these sites during the interview. The problem I need to solve is how to separate “grinders” (and cheaters) from people who know how to code.</p>
<p>My solution is imperfect, and may not work if the candidate is experienced, practiced a lot, but is nervous. I try to work around that problem in different ways. On the other hand, I think my solution worked in some important cases.</p>
</section>
<section id="the-solution" class="level2">
<h2 class="anchored" data-anchor-id="the-solution">The solution</h2>
<p>What I do is that I get some popular questions and change their assumptions to make them <strong>a lot simpler</strong>. During the intervew, I warn candidates that I did that. Even with the warning, the “grinders” regurgitate the solution from the websites, which don’t even solve the simplified problem. Good coders solve the simplified problem, and when I reintroduce the harder assumptions, they solve that one as well.</p>
Once I started doing that, my on-site pass rate became really high, and I got <a href="https://www.meyerperin.com/posts/2022-04-05-hiring-is-a-superpower.html">some reputation for being a good interviewer</a>. My team got a lot better, too!
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2022-07-22-leetcode.html</guid>
  <pubDate>Fri, 22 Jul 2022 04:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/leetcode.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Becoming Good at AI for Good</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2022-06-21-becoming-good-ai-for-good.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>The Microsoft AI for Good Research Lab published an <a href="https://www.microsoft.com/en-us/research/publication/becoming-good-at-ai-for-good">article</a> last year about how to do AI projects in the #AI4Good domain.</p>
<p>The current training academia gives to data scientists is not very practical. People learn data science with very sanitized datasets in very idealized problems, and when they meet reality, it’s usually a big shock.</p>
<p>I think the “Becoming Good at AI for Good” article is a good summary regardless of whether you’re doing AI for Good or more “commercial” applications of AI. A lot of the problems that AI for Good projects face (and are addressed in the article) are also problems that you’ll see if you’re doing AI projects for organizations that don’t have software engineering as their primary capability.</p>
<p>I especially like the part about getting things from model to impact. Before learning of this article, I’d usually send people to an article that was written at Google in the early 2000s.</p>
<p>This is a lot better, and likely to help you understand a lot about the real-world constraints of making AI happen.</p>
<p>The article can be found at <a href="https://www.microsoft.com/en-us/research/publication/becoming-good-at-ai-for-good" class="uri">https://www.microsoft.com/en-us/research/publication/becoming-good-at-ai-for-good</a></p>
</article>



 ]]></description>
  <guid>https://meyerperin.org/posts/2022-06-21-becoming-good-ai-for-good.html</guid>
  <pubDate>Tue, 21 Jun 2022 06:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/aiforgood.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Weak baselines</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2021-11-29-weak-baselines.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>Gather close as I tell you another cautionary tale about data science.</p>
<p>The finance team of a company was trying to forecast their monthly cash. An analyst had been doing that for about ten years, and had a record of cash actually received per month going back all those years. He realized that some months, like December, received a lot more cash than others, like February.</p>
<p>The analyst used Excel to calculate the percentage change for each month. For example, if the company received $100M in October 2009 and $80M in November 2009, he recorded -20% for Nov 2009. To calculate November 2021, he would take the average of the percentage changes for November over the last 10 years (2011 to 2020) and apply it to the October 2020 number.</p>
<p>It didn’t work too well. The mean absolute percentage error (MAPE) was around 20%. For some months, the forecast missed by a lot, causing significant problems, including having to urgently borrow cash at significant cost. Eventually, the company decided to hire a data science consulting company.</p>
<p>The consulting company immediately concluded that the problem was due to lack of data. Ten years of monthly data resulted in just 120 data points. To do data science, they said, they needed more than that. Luckily, the analyst had also been recording the daily data. Now, with thousands of data points, the consulting company used an LSTM deep learning model to forecast cash by day. It also didn’t work too well, but when they aggregated it by month, it did a lot better than the analyst’s original forecast: MAPE was closer to 10%.</p>
<p>The project was celebrated as a success. A press release was issued, promoting the benefits of using cloud technologies and deep learning for more accurate financial forecasting. The person who hired the consultants was promoted.</p>
<p>A few months later, a new data scientist joined the company, looked at the monthly data, and used the Excel FORECAST.ETS function to achieve a MAPE of 1% in seconds. This time, there was no press release.</p>
<p>When starting a data science project, be careful about the baseline you’re using to determine success. Choosing a poor baseline (like the original analyst model) can result in thinking that fairly bad outcomes (like the one from the LSTM model) are good, just because they’re better than horrible outcomes. I’ve seen a number of cases in which a simple heuristic handily beat a complicated ML model.</p>
</article>



 ]]></description>
  <category>data science</category>
  <guid>https://meyerperin.org/posts/2021-11-29-weak-baselines.html</guid>
  <pubDate>Mon, 29 Nov 2021 06:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/weak-baseline.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Reverse causation</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2021-11-22-reverse-causation.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>Gather close as I tell you a cautionary tale about data science.</p>
<p>A new data scientist had just started working on the collections team for a large company. One of the problems they wanted to solve was “does contacting customers help at all?”</p>
<p>It’s an important question. After all, the cost of collections is directly related to the number of contacts. The more contacts, the more people.</p>
<p>The DS had a table with of invoices and payment dates, and a table with a history of contacts by invoice. Using the invoice issue date and invoice payment date, he created a payment delay metric. Then, he joined the contact history and created a metric for number of contacts. Finally, he calculated the correlation between the number of contacts and payment delay.</p>
<p>He was startled by the results: the more contacts per invoice, the <em>longer</em> it took customers to pay.</p>
<p>He told the collections manager, who got very excited. He long suspected that the contact center had too many people. Now he had some proof.</p>
<p>The DS carefully separated the data in a training and a test sample, and trained a regression model using XGBoost. Given inputs such as the invoice country and amount, the model predicted the delay in days given an estimated number of contacts. The model had a very low error (&lt;0.5d) in the training dataset. When using the unseen test dataset, the error was also minimal.</p>
<p>The DS and the collections manager made a plan to deploy a tool to collectors. When assigned an invoice, collectors should enter the characteristics of the invoice in the tool and add 1 contact. For example, if a collector was assigned an invoice with 10 days delay and 2 contacts, they should enter 3 as the number of contacts. If the model returned a number larger than the current delay, the collector should not make the contact. They should just wait.</p>
<p>Once the tool was deployed, the number of contacts initially dropped a lot. To the collections manager surprise, however, the payment delays also increased. Customers were paying much later than usual, and ended up having to be contacted anyway.</p>
<p>As you may already have realized, the problem with the idea was that customers were not delaying because they were annoyed with too many contacts. The relation went in the other direction: customers were being contacted because they were late. While contacts and lateness was very correlated, lateness caused contacts, not the other way around.</p>
<p>The project was scrapped. The team decided that data science was all hype. The data scientist moved on to another team. The collections team went back to the old way of contacting customers: about once per week until they paid.</p>
<p>It took a while (and a new data scientist) for that team to trust data science again: a few years later, a new data scientist started experimenting with assigning different strategies to randomized customers, and that improved collections a lot. But that’s another tale.</p>
</article>



 ]]></description>
  <category>data science</category>
  <guid>https://meyerperin.org/posts/2021-11-22-reverse-causation.html</guid>
  <pubDate>Mon, 22 Nov 2021 06:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/reverse-causation.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Reverse laziness</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2021-10-27-reverse-laziness.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>I’ve noticed a behavior in both my professional life and with my kids doing their homework: people spend a lot of time on things they don’t like.</p>
<p>Many years ago, I had that realization about myself. For some things I didn’t like doing, I’d procrastinate, complain, but ultimately ended up having to be engaged with that task over a long period of time. During that time, the stress of the task kept hanging over me.</p>
<p>I decided to do a deal with myself: I’d set up a short but reasonable time to do the task and during that time I’d do the best I could. At the end, I’d be <em>done</em>. Back then, I didn’t know about the #pomodoro method, which can actually be used in a similar way.</p>
<p>After that, the “dwell time” of tasks that annoyed me dropped sharply: once I booked the time, the task was mostly out of my mind. Once I finished the allocated time, I’d be in peace. Things that used to annoy me for a week became things that bothered me for an hour.</p>
<p>I even started to like some of the tasks I thought I wouldn’t. Getting started is half the battle.</p>
<p>I still couldn’t convince my kids.</p>
</article>



 ]]></description>
  <category>concepts</category>
  <guid>https://meyerperin.org/posts/2021-10-27-reverse-laziness.html</guid>
  <pubDate>Wed, 27 Oct 2021 06:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/reverse-laziness.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Optimization algorithms for your career</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2021-10-05-optimization-algorithms-for-your-career.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>Most people in science and engineering professions are very familiar with the concept of optimization.</p>
<p><img src="https://meyerperin.org/images/optimization-career.png" class="img-fluid"></p>
<p>We are also familiar with the traps of local versus global optimization and how to protect our code and models against those traps. However, we frequently fail to realize that the same concepts can be applied to our careers.</p>
<section id="the-trap" class="level2">
<h2 class="anchored" data-anchor-id="the-trap">The Trap</h2>
<p>It’s usually not hard to find what’s required to progress on your current job. You might have access to “career stage profile” documents that describe the characteristics of that job for different levels and what’s required to move from one level to another. Your colleagues or your manager can tell you what it takes to get promoted. Getting promoted is a good thing: it will give you a better salary and sometimes an expanded scope that will allow you to have opportunities to grow even further.</p>
<p>I had many career conversations in my tenure, and in a large proportion of cases, “getting promoted on the job I currently hold” is not a typical lifelong objective, but it’s typically an objective that people have a plan for. On the other hand, people frequently tell me about major goals/objectives (change professions, open their own company, get a Ph.D., etc.) for which they don’t have an action plan.</p>
<p>It’s like their algorithm is stuck near a local maxima (in this text, optimizing will mean maximizing) and the only thing they can see is the short-term objective, so they can only trace paths to get to the local maxima.</p>
<p><img src="https://meyerperin.org/images/local-minima.jpg" class="img-fluid"></p>
<p>Eventually, people reach a local maxima and realize it’s not their global objective. They may not know what their global objective is, they just know “it’s not this”. Interestingly, the way that people solve that problem is not that different from what an algorithm would do if it realized it got stuck in a local maxima: take a big step, and start again from someplace else.</p>
</section>
<section id="a-simple-fix" class="level2">
<h2 class="anchored" data-anchor-id="a-simple-fix">A simple fix</h2>
<p>The more you know about the neighborhood about your global maxima, the easier it will be go trace a path to get there. Even if the path is not exact, at least you can tell whether you’re getting closer or further away from that goal.</p>
<p>Career coaches (and good managers) keep asking you about your “global objective”. By learning more about it, they can tell you more the path to get there, or find someone that can. Over time, you may accumulate enough information to make a better move when you decide to.</p>
</section>
<section id="the-multi-armed-bandit" class="level2">
<h2 class="anchored" data-anchor-id="the-multi-armed-bandit">The multi-armed bandit</h2>
<p>The hardest problem to solve in career optimization is lack of information. People may have some general idea of where they want to go, but usually not much. For example, I commonly hear people saying that they want to become managers, but after just a few questions I realize that they don’t really know what that means and whether they actually want that.</p>
<p>One way to maximize happiness under incomplete information is to explore several different things, and once you find what makes you happy, then you exploit it (although “exploit” is a somewhat aggressive word in English, it’s actually the technical term), getting as much happiness as you can from it. There are algorithms to find a good balance between exploration and exploitation. One example is the “multi-armed bandit”.</p>
<p>The concept comes from slot machines in casinos. Because a slot machine has one lever that people pull to activate it and people usually lose money on it, it’s called an “one-armed bandit”.</p>
<p>Imagine a slot machine that has multiple levers, therefore a “multi-armed bandit”. When pulled, each lever gives a prize based on a statistical distribution that is unknown to the player. You pull the first lever and get $100. Should you pull the same lever again or switch? Maybe one of the other levers has a constant payoff of $1,000. Maybe that lever you just pulled is actually the best one.</p>
<p>Without knowing the distributions, you can’t know the optimal action to take. In real life, not only one doesn’t know the distributions, but the distributions for each lever can change over time. If that was not complicated enough, you also have infinite levers to choose from. Nobody said it was supposed to be easy.</p>
<p><img src="https://meyerperin.org/images/multi-armed-bandit.png" class="img-fluid"></p>
<p>The multi-armed bandit problem doesn’t have a general optimal solution, but has several strategies that achieve approximate solutions. It is a classical reinforcement learning problem.</p>
<p>Many strategies are very simple “epsilon” strategies, for which one decides a proportion epsilon (between 0 and 1) of exploration (trying to figure out the distributions of the available actions), and the remainder of the time is used for exploitation (performing the action that is expected to yield the best results).</p>
<p>In career conversations, I always suggest that people take the time to explore new things, especially things that are in the (conceptual) neighborhood of what they think they want. Because I’m older than the average tech person, they may think this is some form of ancient wisdom, but I’m just pointing them towards a classical (and somewhat modern) reinforcement learning algorithm.</p>
<p>That’s using data science in production.</p>
</section></article>
<bluesky-comments-section post="https://bsky.app/profile/lucas.meyerperin.org/post/3lgszv7hx6a2o"></bluesky-comments-section>




 ]]></description>
  <category>career</category>
  <category>data science</category>
  <guid>https://meyerperin.org/posts/2021-10-05-optimization-algorithms-for-your-career.html</guid>
  <pubDate>Tue, 05 Oct 2021 06:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/optimization-career.png" medium="image" type="image/png" height="65" width="144"/>
</item>
<item>
  <title>Writing good emails</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2021-09-18-writing-good-emails.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>Earlier in my career, I took quite a while writing emails. As most people earlier in their careers, my main concern was to “not be wrong” rather than actually “moving the discussion forward efficiently” (or ending it). That was especially true when senior people were in the thread.</p>
<p><img src="https://meyerperin.org/images/grad-student-email.jpg" class="img-fluid"></p>
<p>I have been in threads with CEOs and CFOs of big companies, and these people are extremely efficient in their email handling. Bezos famously forwarded emails with a single question mark for action. Another CEO I worked for would simply write “fix this”. Those threads tend to cascade really fast until they got to the person that could actually do something. One thread I was on got five forwards in about eight minutes (I was the lucky person that couldn’t forward the thread to anyone else!).</p>
<p>At my age, I already know a bunch of keyboard shortcuts, and can get through dozens of emails in just a few minutes. A few tricks I picked up along the way: - Owners for questions or actions should be clearly identified - If someone is an owner, they go on the “to:” line - Don’t fork threads, especially if they have actions - Actions go at the end</p>
<p>I’m still not at the professor level in the cartoon, but I’m working towards it.</p>
</article>



 ]]></description>
  <category>career</category>
  <guid>https://meyerperin.org/posts/2021-09-18-writing-good-emails.html</guid>
  <pubDate>Sat, 18 Sep 2021 06:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/grad-student-email.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Data science and 9/11</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2021-09-11-data-science-and-911.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>After the 9/11 attacks, the CIA was looking for Bin Laden all over the world.</p>
<p>One promising lead was a figure that lived in a compound in Pakistan that always shielded his face when walking outside.</p>
<p>The spies named this mysterious figure “The Pacer”, and got several aerial pictures of him.</p>
<p>In order to try to find out whether this was really Bin Laden, they tried to figure out the the person’s height from the pictures, since Bin Laden was quite tall at 195 cm (6’5”).</p>
<p>The National Geospatial Intelligence Agency examined the images, and their photo analysts proudly reported that the man was “somewhere between five feet (150 cm) and seven feet (210 cm) tall”.</p>
<p>A lot of #datascience is like that: technically correct, but not super helpful for decision-making. What we are really looking for is the “lift”: how does some piece of analysis bring us closer to a decision? Does your model provide better result than a naïve baseline that someone can quickly put together?</p>
<p>Many practitioners (including myself) think that before you start thinking hard, and WAY before you start thinking about #machinelearning, you should start with a naïve model. Sometimes that’s all you need. Sometimes that’s all you have data for. Your model should at least beat that.</p>
<p>(The Pacer was actually Bin Laden)</p>
</article>



 ]]></description>
  <category>data science</category>
  <guid>https://meyerperin.org/posts/2021-09-11-data-science-and-911.html</guid>
  <pubDate>Sat, 11 Sep 2021 06:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/bin-laden-compound.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>The neurodivergent manager</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2021-08-17-the-neurodivergent-manager.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>You can be a people manager if you are #neurodivergent and an #introvert. I am.</p>
<p>I spend a lot of my time talking to people. Most of them don’t know that I’m neurodivergent and an introvert. Instead, I have been described as example of an extrovert and “avid networker”.</p>
<p>The easiest way to notice my neurodivergency is that I rarely make eye contact. When I do, it’s because my conscious mind reminded me that I’m not making eye contact and pushed me to do it. It takes effort and concentration. That doesn’t help me as an interviewee. When I’m an interviewer, I tell people that I like taking copious notes (because it’s true) and this usually sets them at ease with my lack of eye contact.</p>
<p>I also suffer from “meeting recovery syndrome”, a concept I learned about in the book “The Surprising Science of Meetings”. After meeting people, especially a large group, I need some time alone to recover, usually by being alone for at least 15 minutes.</p>
<p>By knowing these things about myself, I can plan around them better. For example, I book time to work on an individual project (e.g., writing a promotion document) after a large meeting, and book breaks after small meetings. And I can have the career I chose and love even if I don’t fit the mold of most other people in that role.</p>
</article>



 ]]></description>
  <category>management</category>
  <guid>https://meyerperin.org/posts/2021-08-17-the-neurodivergent-manager.html</guid>
  <pubDate>Tue, 17 Aug 2021 06:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/goonies.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Why you should blog if you are a data scientist</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2019-02-13-why-you-should-blog-if-you-are-a-data-scientist.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>A while back, David Robinson offered the following advice on Twitter:</p>
<blockquote class="twitter-tweet blockquote">
<p lang="en" dir="ltr">
When you’ve written the same code 3 times, write a function<br> <br>When you’ve given the same in-person advice 3 times, write a blog post
</p>
— David Robinson (<span class="citation" data-cites="drob">@drob</span>) <a href="https://twitter.com/drob/status/928447584712253440?ref_src=twsrc%5Etfw">November 9, 2017</a>
</blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>He also followed it up with an <a href="http://varianceexplained.org/r/start-blog/">excellent blog post</a> aimed at aspiring data scientists. I think that the most important idea from that post is actually from a presentation he gave. Here’s the key idea, as reported by Amelia McNamara:</p>
<blockquote class="twitter-tweet blockquote">
<p lang="en" dir="ltr">
"Things that are still on your computer are approximately useless." -<a href="https://twitter.com/drob?ref_src=twsrc%5Etfw"><span class="citation" data-cites="drob">@drob</span></a> <a href="https://twitter.com/hashtag/eUSR?src=hash&amp;ref_src=twsrc%5Etfw">#eUSR</a> <a href="https://twitter.com/hashtag/eUSR2017?src=hash&amp;ref_src=twsrc%5Etfw">#eUSR2017</a> <a href="https://t.co/nS3IBiRHBn">pic.twitter.com/nS3IBiRHBn</a>
</p>
— Amelia McNamara (<span class="citation" data-cites="AmeliaMN">@AmeliaMN</span>) <a href="https://twitter.com/AmeliaMN/status/926509282874585089?ref_src=twsrc%5Etfw">November 3, 2017</a>
</blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>In summary, things that you keep to yourself have very little value, and things that you share with the world have a lot of value. I think that’s a very cool idea for the Economics of information. For an extreme case, imagine you have a trade secret that allows you to solve a specific class of problems faster than others. In this case, it’s understandable that you want to keep the trade secret to yourself. But it makes sense to advertise to the world that you can quickly solve some kinds problems faster than others. That makes your trade secret more valuable.</p>
<section id="the-discipline-of-writing-about-what-youre-doing" class="level2">
<h2 class="anchored" data-anchor-id="the-discipline-of-writing-about-what-youre-doing">The discipline of writing about what you’re doing</h2>
<p>When I was a wee little kid and had just entered college, one of my first classes was “Physics Lab”, and the first class of that was to measure “gravity”, more specifically, the gravitational acceleration . Of course, mathy Computer Science studies that we were, we all knew that the gravitational acceleration would be <img src="https://latex.codecogs.com/png.latex?g%20%5Capprox%209.8%20m/s%5E2">.</p>
<p>Measuring it, however, it’s not very easy. First, remember that this is in the early 90s in Brazil, so digital cameras were very rare. Part of the problem is that 9.8 meters (approximately 30 feet) is quite high, so if we wanted our experiment to take around 1s, we would need a big ladder. Or, as it happened, we’d need to run an experiment that took less than a second for each run. In a class with 25 students, that was the preferred route.</p>
<p>We set up a vertical track attached to a device that would spark every 1/60 of a second, and we attached grid paper to the track. This is called a <a href="https://cse.wwu.edu/physics/acceleration-due-gravity-behr-free-fall-apparatus-and-spark-timer">Behr free fall apparatus</a>.</p>
<p>We would release the device from the top of the track, and the sparks would mark the grid paper. We would then manually measure the distances between the sparks and the differences between the distances would tell us the acceleration. In theory. Again, remember this is the early 90s, so there’s no Windows 95 or Excel easily available, there were several steps that were prone to error.</p>
<p>The desired outcome was not only to calculate the acceleration due to gravity, but also to generate a lab report.</p>
</section>
<section id="writing-the-lab-report" class="level2">
<h2 class="anchored" data-anchor-id="writing-the-lab-report">Writing the lab report</h2>
<p>It was a simple experiment, but there was a twist. Another class of 25 students would have to replicate the experiment following the lab report from the first 25. Oh boy. We quickly found out that the best lab reports were the ones in which the experimenter would document the experiment <em>as they were executing</em> the experiment. Another thing that worked was going through the experiment more than once. What <strong>did not work</strong> was to perform the experiment and then go to where the computers were to type up a report from memory.</p>
<p>I thought that was very insightful, shortly afterwards, we would learn about Donald Knuth’s proposed paradigm of <a href="https://en.wikipedia.org/wiki/Literate_programming">literate programming</a>, which lives on in Jupyter Notebooks and R Markdown and that is very popular in data science today. Behind it is the same concept of explanations interspersed with technical work to produce something that is reproducible and easy to understand.</p>
</section>
<section id="blogging-helps-you-train-to-write-about-your-work" class="level2">
<h2 class="anchored" data-anchor-id="blogging-helps-you-train-to-write-about-your-work">Blogging helps you train to write about your work</h2>
<p>Besides the excellent reasons offered by David Robinson for aspiring data scientists to blog, I think experienced data scientists can also benefit from blogging. David argues that aspiring data scientists should blog for technical practice, to build a portfolio, and to get feedback. Experienced data scientists might benefit for similar reasons, but not exactly the same.</p>
<p>Similar to “practicing”, blogging about technical topics helps you learn how to communicate better about them, and also helps you learn how to write better research reports. Similar to “portfolio”, it signals a data scientist’s body of work. For an early-in-career data scientist, it may be more showing that you can do analysis, but for an experienced data scientist is more about what type of analysis you like to do. Finally, I think that “feedback” also works differently for early-in-career and experienced data scientists. I’m not sure random strangers in the internet will sweep in and correct your posts, but you can find out what people are reading and sharing. You may be surprised when you learn what parts of your work people find interesting.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2019-02-13-why-you-should-blog-if-you-are-a-data-scientist.html</guid>
  <pubDate>Wed, 13 Feb 2019 06:00:00 GMT</pubDate>
  <media:content url="https://meyerperin.org/images/useful-output.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>Praising Conway’s Data Science Venn Diagram</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2018-07-08-praising-conway-s-data-science-venn-diagram.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>Drew Conway’s <a href="http://drewconway.com/zia/2013/3/26/the-data-science-venn-diagram">Data Science Venn Diagram</a> is perhaps the most well-known description of data science.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://static1.squarespace.com/static/5150aec6e4b0e340ec52710a/t/51525c33e4b0b3e0d10f77ab/1364352052403/Data_Science_VD.png?format=750w" class="img-fluid figure-img"></p>
<figcaption>Data Science Venn Diagram</figcaption>
</figure>
</div>
<p>Drew published it more than five years ago. Since then, a ridiculous number of Venn diagrams have been created, <a href="https://www.datasciencecentral.com/profiles/blogs/the-data-science-venn-diagram-revisited">one</a> <a href="https://whatsthebigdata.com/2016/07/08/the-new-data-scientist-venn-diagram/">worse</a> <a href="https://twitter.com/kirkdborne/status/961452846821597184">than</a> <a href="https://twitter.com/kirkdborne/status/739446818233327616">the</a> <a href="https://blog.zhaw.ch/datascience/files/2014/06/SkillSet.png">other</a>.</p>
<p>Five years later, people still struggle with the definition of Data Science. A recent famous example was Lyft <a href="https://eng.lyft.com/whats-in-a-name-ce42f419d16c">changing the titles</a> of their Data Scientists, Data Analysts and Research Scientists.</p>
<section id="should-we-care" class="level2">
<h2 class="anchored" data-anchor-id="should-we-care">Should we care?</h2>
<p>There’s a lot of information asymmetry in the Data Science market, the position being so new, and <a href="https://www.jstor.org/stable/1882010">“Michael Spence signaling”</a> matters. Having a “Data Scientist” title from a reputable company is an important signal. For example, a medium-sized company might not be aware that Lyft <a href="https://eng.lyft.com/whats-in-a-name-ce42f419d16c">recently changed</a> their data analysts title to “Data Scientist”</p>
<p>Part of the problem is that the “Data Scientist” title is itself poorly defined, as shown by the litany of Venn and not-Venn diagrams listed above. A short, non-comprehensive list of alternative titles for Data Scientists I’ve met in the last month:</p>
<ul>
<li>Tableau/Power BI Report Designer</li>
<li>Machine Learning Researcher</li>
<li>Python Programmer who knows how to call scikit-learn functions</li>
</ul>
<p>Using the same name for vastly different abilities makes matching between employers and employees difficult. In addition, it may make career progression difficult. For example, job responsibilities and objectives are likely to be related to a company’s interpretation of the title of “Data Scientist”. Employees in that company may be “Data Scientists” under other interpretations, and may be really good, but may have their performance evaluated under different guidelines: <em>“I know your ML model saved $1B for our company, but the color scheme of your Tableau reports is horrible!”</em> As per the (not really) Albert Einstein’s quote “(…) if you judge a fish by its ability to climb a tree, it will live its whole life believing that it is stupid.”</p>
</section>
<section id="conways-venn-to-the-rescue" class="level2">
<h2 class="anchored" data-anchor-id="conways-venn-to-the-rescue">Conway’s Venn to the rescue!</h2>
<p>Here’s where I think Conway’s Venn diagram comes to the rescue in a useful way. We could rule out the Tableau Designer if they lack programming or statistical skills. We could rule out the ML Researcher if they don’t have domain expertise. We could rule out the Python programmer if they don’t know Math and/or have domain expertise. Perhaps this makes Data Scientists hard to find (it would justify their salary)!</p>
<p>Depending what the company you work for was before the Data Science profession started, a lot of people from different jobs may have re-branded themselves as data scientists. I think Conway’s diagram is helpful as it points to where people should focus their development in order to be more effective as data scientists. For example, former Economists most likely need more hacking skills, former computer scientists need more Statistics and domain expertise, and former management/technical consultants likely need more Statistics.</p>
<p>It can also serve as a check for inclusiveness. For example, a team of former machine learning engineers is struggling to deliver meaningful projects because they lack domain expertise. However, they can’t hire domain experts because the domain experts keep failing the machine learning interviews. Comically, the machine learning engineers think that domain expertise “is easy”.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://meyerperin.org/img/here_to_help.png" class="img-fluid figure-img"></p>
<figcaption>Everything you don’t know is easy</figcaption>
</figure>
</div>
<p>The most useful part of Conway’s diagram for me is that I’ve time and again observed projects that don’t have representatives that can communicate well with each other in all three areas to fail. Conway’s diagram even makes it easy to show how the project failed (“nobody with domain expertise”, “missing knowledge of statistics”). Having a single person that can do all three is difficult, but it certainly solves the communication problem. That being hard or impossible, having specialists that can communicate with each other will usually be sufficient.</p>
</section>
<section id="but-conways-venn-is-missing-something" class="level2">
<h2 class="anchored" data-anchor-id="but-conways-venn-is-missing-something">But Conway’s Venn is missing something!!!</h2>
<p>True to the famous Box aphorism that “all models are wrong but some are useful”, Conway’s Venn is not perfect. In my experience, one of the things that I usually miss is “intensity”. Someone that knows how to write a “Hello World!”, knows how to take averages, and know how to calculate compound interest has knowledge in all three areas of the Venn diagram, but is probably not the ideal person to lead a data science project in Finance. Conway’s is best used to tell you what’s missing for career development, for a team, for a project, but it will not solve all problems.</p>
<p>It will, however, solve many problems that matter, and will provide useful information very elegantly. You can’t ask much more from a model.</p>
</section></article>




 ]]></description>
  <guid>https://meyerperin.org/posts/2018-07-08-praising-conway-s-data-science-venn-diagram.html</guid>
  <pubDate>Sun, 08 Jul 2018 06:00:00 GMT</pubDate>
</item>
<item>
  <title>Centralized vs. Distributed Data Science Teams</title>
  <dc:creator>Lucas A. Meyer</dc:creator>
  <link>https://meyerperin.org/posts/2018-06-25-centralized-vs-distributed-data-teams.html</link>
  <description><![CDATA[ 




<article data-clarity-region="article">
<p>In a recent <a href="https://www.datacamp.com/community/podcast/organizing-data-science-teams">DataFramed podcast</a>, <a href="https://github.com/hugobowne">Hugo Bowne-Anderson</a> and <a href="https://github.com/jnolis">Jonathan Nolis</a> discuss whether data scientist teams should be centralized or distributed. I have my own opinions about that question.</p>
<section id="should-data-science-teams-be-centralized-or-distributed" class="level2">
<h2 class="anchored" data-anchor-id="should-data-science-teams-be-centralized-or-distributed">Should data science teams be centralized or distributed?</h2>
<p>I was happy to find that Jonathan Nolis has the same opinion as I do: that a data science team works best when distributed into client areas. In addition, we agree that if you distribute your data science team among several different client areas, you have to find a way of keeping the data scientists feeling as if they’re part of a cohesive group. And that’s hard.</p>
<p>For me, a key part of the argument to distribute data scientists in client areas relies on Drew Conway’s (ancient) <a href="http://drewconway.com/zia/2013/3/26/the-data-science-venn-diagram">Data Science Venn diagram</a>.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://static1.squarespace.com/static/5150aec6e4b0e340ec52710a/t/51525c33e4b0b3e0d10f77ab/1364352052403/Data_Science_VD.png?format=750w" class="img-fluid figure-img"></p>
<figcaption>Data Science Venn Diagram</figcaption>
</figure>
</div>
<p>Assigning data scientists to a client area will help build the “Substantive Expertise” part of the diagram. A common trap here is “Well, we can do the same if we keep the data scientists centralized and give them specializations that match the client areas.”</p>
<p>Although I think that <em>can</em> work, I worry about what happens when data scientists have downtime.</p>
<section id="incentives-for-client-areas-and-centralized-data-science-teams" class="level3">
<h3 class="anchored" data-anchor-id="incentives-for-client-areas-and-centralized-data-science-teams">Incentives for client areas and centralized data science teams</h3>
<p>In my observations (low sample size), the client areas usually have a better sense of ROI<sup>1</sup> - if they invest resources on something, they expect returns. On the other hand, most centralized data science teams I’ve seen have two problems: data scientists hired without a clear purpose and an incentive to overengineering: a current example is to use Deep Learning where a regression would do.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://cdn-images-1.medium.com/max/2000/1*YfxWi5QZkShNHS0XLuKlMQ.png" class="img-fluid figure-img"></p>
<figcaption>Overengineering</figcaption>
</figure>
</div>
<p>It may be simply that client areas are more mature and battle worn than centralized data science groups, at least in my experience. I think that if a data scientist is assigned to a client area and there’s downtime, they’ll put the data scientist to work in business intelligence or decision analysis. On the other hand, if the data scientist is assigned to a centralized data science group, they’ll be pulled into a project away from their “client area specialization” or will find a way to work in something novel that’s cool (as of 2018, autoencoders, GANs or something with “Bayesian” in its name).</p>
<p>And here the ROI appears again: as the data scientist works through the BI project, they’ll learn more about the data and more about the business and its data, and that investment has a large likelihood of return in their next project in the same group. Whereas if they spend this time learning a new technique, the return is uncertain.</p>
</section>
<section id="but-wait-if-you-do-that-data-scientists-will-never-learn-something-new" class="level3">
<h3 class="anchored" data-anchor-id="but-wait-if-you-do-that-data-scientists-will-never-learn-something-new">But wait, if you do that, data scientists will never learn something new</h3>
<p>Here’s where the hard part of keeping a data scientist community pays off. That community may be weekly “Lunch &amp; Learns” if the number of data scientists is small, or quarterly conferences if the number of data scientists is very big and your company is very, very rich.</p>
<p>If you don’t forcefully push towards having such a community, it’s very likely that data scientists will reinvent the wheel over and over again, and if their time is as precious as their high salary indicates, that will be a waste of money. For example, if you have 5 data scientists It’s better to spend $50k on travel to bring them together a few times a year than to have one of them wasting three months reinventing something that another of them already done.</p>
</section>
<section id="where-i-disagree-with-jonathan-nolis" class="level3">
<h3 class="anchored" data-anchor-id="where-i-disagree-with-jonathan-nolis">Where I disagree with Jonathan Nolis</h3>
<p>In the podcast, Jonathan suggests that the data scientists should be assigned to client areas and report “solid line” to a Chief Data Scientist, while reporting “dotted line” to the client areas. I would do the opposite. However, listening to the podcast, it seems that Jonathan didn’t have a strong opinion that one is better than the other, and again in agreement, neither do I.</p>
<p>One big advantage of having a “solid line” group of data scientists is career advancement. It’s easier to compare data scientists with other data scientists. Leaving them in the client areas will require to compare them with other members of the client area, and they may be seen as wizards (and overvalued) or “too hard to understand”, and be undervalued.</p>
<p>One disadvantage is estimating the value of the data scientist to the client area. Data scientists that are more creative in their value calculations or less scrupulous are probably going to get ahead. I’ve lost count of the times that a data science project saved more revenue than the customer ever had.</p>
</section>
<section id="so-what-do-i-do" class="level3">
<h3 class="anchored" data-anchor-id="so-what-do-i-do">So, what do I do?</h3>
<p>Honestly, if you’re starting a data science team, your best bet is to hire someone like Jonathan or Drew Conway to plan the team for you. You’re going to spend a lot of money on Data Science, and it’s important to make sure that it will be well spent.</p>
<p>If you’re on a budget and all you can invest is the time to read some blog posts, well… you probably don’t have enough money to hire data scientists. But if you do, I recommend assigning them to the client areas and keeping them there. Even when they’re not doing modeling or machine learning, they’re learning more about the business and its data.</p>
</section></section></article>






<div id="quarto-appendix" class="default"><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>In Finance circles, we use NPV, but the idea is the same↩︎</p></li>
</ol>
</section></div> ]]></description>
  <guid>https://meyerperin.org/posts/2018-06-25-centralized-vs-distributed-data-teams.html</guid>
  <pubDate>Mon, 25 Jun 2018 06:00:00 GMT</pubDate>
  <media:content url="https://cdn-images-1.medium.com/max/2000/1*YfxWi5QZkShNHS0XLuKlMQ.png" medium="image" type="image/png"/>
</item>
</channel>
</rss>
