Skip to content

James Williams

Category: Technology

Your boss is being taken for a ride on Generative AI

You know how it’s pretty easy to spot an online post or comment that was written with ChatGPT? It’s not that they use em-dashes—plenty of great writers use em-dashes—it’s that they have a general air of disconnectedness and they write with a patronizing, prescriptive structure. It’s easy to spot most AI slop.

However, I am cognizant of survivorship bias. There is a lot of AI-generated content that slips by unnoticed. If you have a strong command of both your subject matter and written prose you can prompt your way to a high-quality output, but in order to do that you have to give it more than it gives back. It’s a lopsided, toxic relationship, and these days there are a lot of lovesick people out there.

Unfortunately, your boss is one of those sad saps standing on the curb with nothing but a bouquet of flowers and a dream.


In my mind there are three different types of tasks that we might get a Generative AI tool or “Agent”1 to do for us:

  1. There is little disagreement that Generative AI is bad at anything that might loosely be described as art, which I’ll define for myself as anything unique that is created with a mixture of curiosity, imagination and craftsmanship.
  2. Generative AI is great at tasks and processes that require no imagination, like extracting structured data from written text, summarizing a transcript, or repetitive clerical tasks.
  3. Generative AI might be good at domains like programming or law, which can span a massive spectrum between the utterly routine and the very incarnation of elegance.

The third category is the most interesting to me. There are wildly divergent opinions on the utility (or futility) of Generative AI for programming. The world of bits and bytes can be intractable because what you build is limited only by the guardrails of your imagination. The substrate is what you decide it is. The raw materials are undefined until you declare them.

In the physical world of atoms, we have a general idea of the forces, chemicals and components that we need to build a bench, a bridge or an airplane. That isn’t to discount the expertise required to build those things, I note only that there is an inherent tactile structure to building things in physical space that doesn’t exist in the digital one. Things fit together, or they don’t. Most failure modes are known in advance, unlike the sly bugs that lie in wait within every digital codebase.

Because it is not grounded in the physical world, software complexity fits a fat-tailed distribution. A lot of applications follow the tried and true recipe of reading and writing information to a database with a bit of business logic and reporting mixed in. The techniques for creating such an application are well documented and well understood, and this is the sort of thing that Generative AI can do very well with little correction or oversight on the part of the operator. For this reason, prototyping greenfield projects is a great use of the technology.

As we navigate further to the right on our fat-tailed distribution of software complexity, we quickly enter a different world. Any piece of software is comprised of layers—each layer building upon the layer below it, disconnected from and naive to the specific implementation details underpinning it. This is called abstraction and it’s a foundational concept in programming. In simple software we might be coding at just one layer of abstraction above a well-documented framework that the Generative AI is pre-trained on. But in complex and/or mature software, we’ve already written a few layers of our own, and the AI hasn’t been trained on the building blocks we’re asking it to use.

I believe most of the disparity in lived experience with Generative AI among software developers can be explained by plotting their work on this approximate log-normal (fat-tailed) distribution. I suspect this shape holds true in many other professional domains as well.

Generative AI attitudes

Complexity extends rightward. Most AI evangelists either have misaligned incentives or work on simple projects (denoted by the gray-shaded area).

It is possible to get a modern Generative AI to work on complex projects in the tail part of the graph, but it needs much more attention and guidance. That is, the operator needs to know both the subject area extremely well, and the nuances of the Generative AI tool extremely well. Obtaining a positive outcome at this end of the chart is a difficult skill to master in itself, with novel risks and pitfalls. In many instances it is faster and less error prone to write the code yourself, since you will need to step in and course correct the AI a lot. There is ample value in certainty, and you forego that at the outset. If you don’t have the expertise to identify its missteps (and there will be many), then you are firmly out over your skis. You can see how frustrating this is for everyone involved in real time: take a look at Microsoft’s own engineers losing their minds trying to get AI to write correct code for them (also here, here and here).


Your boss has been sold a grand lie. It’s not their fault. They have been set adrift in a sea of misdirection, misaligned incentives, grift, absolutism, desperation, and stupidity. I have never seen a manic hype cycle like this one and neither have they.

ChatGPT came along just as 15 years of free money dried up, leaving an overweight tech industry clamoring for something, anything to keep the capital spigot flowing. Not only could Generative AI create an entirely new class of products and start-ups, it could also be used as cover to lower headcounts and put those pesky, overpaid software developers back in their place.

So the salespeople started buying seafood towers and the scrum masters scrummed with renewed vigor, and every downstream middle-manager through C-suite executive were convinced that they didn’t just want AI… they needed AI. There’s an AI button on your keyboard now. notepad.exe has Copilot built in. There’s no budget for anything without AI. If you don’t use AI, don’t bother showing up on Monday.

To state the quiet part out loud, the promise of Generative AI is “efficiency”, and “efficiency” simply means doing the same amount of work with fewer people. So if you came to this post wondering if your job is at risk, the answer is probably yes, but not because of AI—because your boss has been pumped full of hot air.

Your boss has been told that an AI “Agent” is equivalent to a person. That you can set it on a task, and with enough iterations of the loop, it will arrive at the correct solution/feature/output. They’ve been told that their competitors have many such tools and “Agents”. They’ve been told they’re falling behind. They can’t know it for sure, and they can’t dispel it, but everyone is saying it, including their own boss, and their boss’s boss, and those well-dressed chaps on that panel last week. It’s not your boss’s fault, they just have to to keep up with <dastardly competitor>, who no doubt is using Workday: the powerful AI platform that keeps your most important assets on track, every decision on point, and your fleet of AI agents at peak performance. That’s Workday.©

Jesus Christ.

The reality is this: in order to apply Generative AI to a task, there needs to be a human operator in the loop who understands that task BETTER than the AI. The classic adage of “garbage in; garbage out” applies. You cannot take someone who lives on the median of the distribution in the chart above, give them a replit account, and expect top-tier output.

The upper bound on what your company can accomplish with Generative AI is the level of your most proficient colleague. It is as true in 2025 as it was in 2020 as it was in 1820: you cannot do great things without great people. What you can now accomplish are more middling things with the same amount of great people. That has business value, to be sure. There is plenty of middling work that needs to be done. To quote my man Alec, “world needs plenty of bartenders!”2

Now, I have no empirical basis for these numbers aside from my own experience and intuition, but if I’m being VERY generous I would peg my own efficiency improvements using Generative AI at somewhere around 20% to 30% on average. On prototyping and side projects, I’d guess that I’m approaching 100%, but that’s not real work. On my mature main project and its components, the improvement is well below 10%. Your mileage may vary depending on your mix, but it’s not a human ass in a human seat, that much is true. I would estimate that an AI-forward company could drop a single junior developer for every 3 or 4 senior developers, which would be on the order of a 10% to 15% reduction in compensation expense in the base case of a company with only 4 or 5 employees, all but one of them senior. The savings would be well below 10% in a larger, more balanced pool.


How might you protect yourself from the whims of your stupid, gullible boss who hasn’t been enlightened by my napkin math? This hype cycle will crash in due time as they all do, but Generative AI isn’t going to uninvent itself. Our environment has indeed changed, so it’s time to adapt.

If my assertions prove correct, it will be the mundane work that ultimately gets carved out and handed off to an AI under the supervision of an overqualified operator. This runs counter to the current narrative that you can have average people accomplish above-average things with AI, but I don’t think that’s how it will shake out (much to the chagrin of MBA consultants everywhere, themselves rather average).

If you’re already in that “skilled operator” category and using Generative AI to expedite your more menial work, continue advancing your ability to use the tools but remember not to let your basic skills atrophy. I can say my spelling is far worse now with the ubiquity of spell-check than it was in grade school. Likewise, how many people can still parallel park under pressure without a backup camera, or navigate across town without GPS navigation? Take your cue from the aviation industry where pilots regularly fly manually despite auto-pilot to keep their skills up.

If you’re starting out, that’s a trickier spot to be in, but it’s not impossible. Lean into fundamentals and keep your pencil sharp, because there will always be a place for the person who actually understands the code that is deployed, and that will only amplify in the future. Your colleagues or classmates are all learning to ride a motorcycle before they know how to pedal a bicycle. Learn how to pedal the bike.

I stated early on that art is safe. I believe that will always be true because art is how we express humanity. It is the antithesis of machine. There may be no artistry apparent in your day job, but curiosity, craftsmanship and imagination are the ingredients of mastery no matter what you do. An AI agent cannot replicate your taste. It has none of your flourish or flair. It has no style. It is not cool.

If you’re a programmer, recognize that programming is design. If you’re a labourer, approach your job like an engineer. If you’re an engineer, approach your job like an architect. Carve your name into your work and you’ll be alright.


1

The industry defines an Agent as a language model that can iterate on it’s own output (runs in a loop) and use tools. Personally, I define an agent by its correct definition of something or someone that acts on behalf of something or someone else. The agent works for you, not in place of you.


2

Proof of life

I’m still here! Made a few updates to the website, including adding (for the first time) a little bit of javascript to lazy load the image gallery posts. I’m still on the fence about javascript as a hard dependency, but I like the look of these long-form galleries and a 35 mb initial payload is downright rude.

In other news, I’ve migrated my primary social media presence from mastodon over to bluesky. A lot of ink has been spilled on this topic, especially in the wake of the election, and I don’t have much new to add. I believe they are two fundamentally different tools, with mastodon best suited for small, niche communities and bluesky a suitable successor to twitter as a general-purpose network. Sensible, top-down moderation of any social network at scale is extremely difficult, and they’ve acknowledged that by building incredible tools for community-first moderation and community-first algorithms. It’s a great community full of kind people and I hope to see you there.

Pulling a Smartsheet table into Microsoft Excel using Power Query

Well if you thought my first post in eight months would be exotic, go ahead and smash that back button.

I use this technique when we have one-off assignments at work where I need a quick and dirty web-based data store that several people can collaborate on, and that can be easily queried in Excel without any intermediate infrastructure or processing. This would be quite trivial if not for Smartsheet’s intractible API format.

Assuming you have a Smartsheet grid you want to mirror in Excel and that can be refreshed on the fly, you’ll need the sheet’s ID and an API bearer token for a user with viewer permissions.

In Excel, open Power Query and create a new query using the advanced editor. Make sure to replace $SHEET_ID and $BEARER_TOKEN. The query will bring in both your data and column headers.

let
    Source = Json.Document(
        Web.Contents("https://api.smartsheet.com/2.0/sheets/$SHEET_ID", [
            Headers=[
                #"Content-Type"="application/json",
                Authorization="Bearer $BEARER_TOKEN"
            ]
        ])
    ),

    // Process rows
    RowsData = Source[rows],
    RowsTable = Table.FromList(RowsData, Splitter.SplitByNothing()),
    ExpandedRows = Table.ExpandRecordColumn(
        RowsTable,
        "Column1",
        {"id", "rowNumber", "expanded", "createdAt", "modifiedAt", "cells", "siblingId"},
        {"ID", "RowNumber", "Expanded", "CreatedAt", "ModifiedAt", "Cells", "SiblingId"}
    ),
    ExpandCells = Table.ExpandListColumn(ExpandedRows, "Cells"),
    ExpandedCellsDetails = Table.ExpandRecordColumn(
        ExpandCells,
        "Cells",
        {"columnId", "value", "displayValue"},
        {"ColumnID", "CellValue", "CellDisplayValue"}
    ),
    RemovedCellsMetaColumns = Table.RemoveColumns(
        ExpandedCellsDetails,
        {"ID", "Expanded", "CreatedAt", "ModifiedAt", "CellDisplayValue", "SiblingId"}
    ),
    PivotedCellsByColumnId = Table.Pivot(
        Table.TransformColumnTypes(RemovedCellsMetaColumns, {{ "ColumnID", type text }}),
        List.Distinct(Table.TransformColumnTypes(RemovedCellsMetaColumns, {{ "ColumnID", type text }})[ColumnID]),
        "ColumnID",
        "CellValue"
    ),
    CleanRowData = Table.RemoveColumns(PivotedCellsByColumnId, {"RowNumber"}),

    // Process columns
    ColumnsData = Source[columns],
    ColumnsTable = Table.FromList(ColumnsData, Splitter.SplitByNothing()),
    ExpandedColumns = Table.ExpandRecordColumn(
        ColumnsTable,
        "Column1",
        {"id", "title"},
        {"ColumnID", "ColumnTitle"}
    ),
    ColumnTitlesMapped = Table.Pivot(
        Table.TransformColumnTypes(ExpandedColumns, {{ "ColumnID", type text }}),
        List.Distinct(Table.TransformColumnTypes(ExpandedColumns, {{ "ColumnID", type text }})[ColumnID]),
        "ColumnID",
        "ColumnTitle"
    ),

    // Add headers
    CombinedDataTable = Table.Combine({ColumnTitlesMapped, CleanRowData}),
    FinalData = Table.PromoteHeaders(CombinedDataTable)

in
    FinalData

How to checkout and edit a pull request locally

Let’s say you have a dependabot pull request and Charlie Marsh has added a new check to Ruff that causes your lint check to fail. You can fix the lint error and push the changes back to the pull request branch!

First, checkout the pull request locally:

# In this case, I'm updating ruff to v0.0.278
git fetch origin dependabot/pip/ruff-0.0.278
git switch --track origin/dependabot/pip/ruff-0.0.278

We’ve now checked out the PR branch and set it to track the remote. We can use this pattern to keep tabs on long-running PRs, or as in this case, simply push an additional patch before merging. If you’d like a more friendly local branch name, you can append the :my-branch-name to the end of the git fetch call, and then call git switch my-branch-name to check it out; just keep in mind that this won’t set the local branch to track the remote.

In my case, this ruff release does not provide any new rule categories and my lints still pass, however I’d like to update the ruff version in my .pre-commit-config.yaml file so that it’s consistent with my requirements.txt. I’ll make that change, commit and push back to the remote.

git add .pre-commit-config.yaml
git commit -m "Update pre-commit config."
git push

At this point, your checks should fire again and you can merge using your preferred merge method into your trunk. Check out real pull request to see how this looks server side.

Running a local Kubernetes cluster with Kind: A step-by-step guide

It has happened. I thought I could avoid it, but here we are. As if getting your program to run on one computer wasn’t hard enough, now we have to run it on multiple computers at the same time? They have played us for absolute fools.

Anyway, assuming we have some shared experience with Docker, let’s introduce some terminology:

  • A Pod is the smallest deployable unit in Kubernetes, often a single instance of an application. As I understand it, a pod is the logical equivalent of a container.
  • Nodes are the machines that host these pods. More nodes allow for more redundancy.
  • A Cluster is a set of nodes with the same job. A cluster can run multiple nodes, and a node can run multiple pods, and a pod typically consists of between two and fifteen orca whales.
  • A Service is an abstraction which provides a single network entry point to distribute traffic across the cluster.

For local development I am using Kind, a tool which allows you to run Kubernetes clusters in Docker containers. It is a lightweight way to run docker containers inside kubernetes inside a docker container (pause for effect).

The command to create a cluster is: kind create cluster

To deploy the application, it needs to be packaged as a Docker image. After creating the Dockerfile, the image is built and loaded into the Kind cluster with the following commands:

docker build -t my-image-name .

kind load docker-image my-image-name

I should note that in addition to Kind, there is a tool called minikube which is similar, though it requires you to set up a container registry.

The next step is creating a deployment and a service for the application by creating kubernetes manifest files in your project directory. The simplest possible configuration is something like so:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-image-name-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-image-name
  template:
    metadata:
      labels:
        app: my-image-name
    spec:
      containers:
        - name: my-image-name
          image: my-image-name
          imagePullPolicy: Never # Use for local image
          ports:
            - containerPort: 8000 # Use the port your application runs on

Note that the imagePullPolicy is set to Never because we are using a local image with the implied tag latest. Specifying a specific tag should make this unnecessary, otherwise the default behaviour is to try to pull the image from Docker Hub, which will fail each time (or worse, deploy something unexpected).

In addition to matching the exposed port of the container, your application should be configured to bind to any incoming address (0.0.0.0), not just localhost.

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-image-name-service
spec:
  type: NodePort
  ports:
    - port: 8000
      nodePort: 30080
  selector:
    app: my-image-name

With these files in place, we can create the deployment and service respectively using kubectl apply -f <file-name> for each. They can be verified using: kubectl get deployments and kubectl get services.

If there are any issues, logs can be checked using: kubectl logs <pod-name>, and the pod name can be found using kubectl get pods.

Remember to specify environment variables in the deployment.yaml file under env in the containers specification if your application requires them.

If you’re running docker inside a linux virtual machine, port 30080 should already be exposed. If you’re running using Docker Desktop, there’s one more step which requires forwarding a local port to the service port. This can be done using:

kubectl port-forward service/my-image-name-service 30080:8000

This will map the service to localhost:30080 on your local machine. Launch it in tmux or append the command with an ampersand as it will block the terminal otherwise.

Fin. Now deploy to prod on a Friday afternoon and you’re done!

Notes from Stephen Wolfram's ChatGPT primer

Source Material: Stephen Wolfram, 2023-02-14

ChatGPT is a large-scale transformer-based language model that is designed to predict the next word in a sentence given the context of what has been said. It is a neural network with 175 billion parameters that has been trained on a vast corpus of text, enabling it to form and apply a semantic structure to human language.

The name ChatGPT stands for Generative Pre-trained Transformer. Generative means that the model is capable of generating new text rather than just recognizing patterns in existing text. Pre-trained means that the model has been trained on a large corpus of text before being fine-tuned for a specific task. Transformer refers to the specific type of neural network architecture which is designed to better handle long-term dependencies between words in a sentence.

To accomplish its task, ChatGPT uses a technique known as unsupervised learning, which allows it to learn patterns in the data without being explicitly taught. Instead of being trained on explicit examples of inputs and their associated outputs like in supervised learning, the model is given a large corpus of text and is trained to predict the next word in a sentence by masking the latter part of the sentence and having it predict what should come next. It then compares what it generated with the masked text, and iteratively adjusts its parameters to minimize the error.

To evaluate how well the model performs on each iteration, a loss function is used. The loss function calculates how far away the model’s predictions are from the desired outcome, and the neural net weights are adjusted in a way that minimizes the result of the loss function.

Training the model both optimizes the neural net weights and produces embeddings, which are a way of representing the meaning of words as arrays of numbers (in the vague, undefinable sense of ‘meaning’). Nearby words are represented by nearby numbers. ChatGPT takes this concept further by generating embeddings not just for individual words, but for entire sequences of words.

These embeddings are then used to predict the probabilities of different words that might come next in a sentence. This is accomplished using a transformer architecture, which is designed to better handle dependencies between tokens in the input and output even when they are far from each other in the input sequence. One of the defining features of the transformer is its use of an attention mechanism, which involves certain neurons focusing more on relevant parts of the sequence than others. This allows ChatGPT to take into account the context of the conversation that’s taken place, which can inform the next token that’s generated. The attention mechanism also allows ChatGPT to capture context from the prior conversation even when that context is not adjacent to the token being generated. This is the main reason ChatGPT comes across as a coherent entity.

Finally, ChatGPT uses a temperature setting to introduce a degree of randomness into its predictions, which can make the output more diverse and interesting.


What strikes me as the most profound point Stephen makes is the success of ChatGPT as a scientific discovery in that it shows that there may be simple rules that describe how the semantics of human language can be arranged that we ourselves don’t yet understand. Studying the pathways and structures ChatGPT uses could help deepen our own understanding of human language.

LLM Round-up

When I asked ChatGPT what type of AI it is, this is what it said:

ChatGPT is a type of language model, specifically a variant of a transformer model called the Generative Pretrained Transformer (GPT). It is an example of artificial general intelligence (AGI), which means that it is capable of performing a wide range of natural language processing tasks, such as text generation, summarization, translation, and question answering, among others.

I know some of these words 🤦‍♂️.

I’ll never be an AI researcher, but I do want to have a laymen’s understanding of what the tool is capable of and how it works. I’ve been searching for educational resources that lie somewhere in the sweet spot of accessibility and depth. I want to understand the basic terminology, like what a transformer is, and also understand a large language model’s shortcomings. And I want to understand where a language model fits into the broader landscape of AI.

To that end, I’m compiling a few resources that seem to be well regarded. I’ll treat this post as a digital bookshelf, and will update it with my thoughts and notes as I work through the material.

We’ll start with 35,000 words from Stephen Wolfram as a primer, then move into Andrej Karpathy’s Zero to Hero lecture series.

How to convert a subdirectory to its own git repository/submodule

# Make a clone of the repository
git clone <your_project> <new_submodule>

# CD into the new repository
cd <new_submodule>

# Use filter-branch to isolate the subdirectory
git filter-branch --subdirectory-filter 'path/to/subdirectory' --prune-empty -- --all

# Remove the old remote
git remote rm <remote_name>

git filter-branch lets you rewrite Git revision history and apply custom filters on each revision. It should be used with caution! From the Git Manual (emphasis mine):

git filter-branch has a plethora of pitfalls that can produce non-obvious manglings of the intended history rewrite (and can leave you with little time to investigate such problems since it has such abysmal performance). These safety and performance issues cannot be backward compatibly fixed and as such, its use is not recommended.

Nevertheless, since we live dangerously around these parts, here’s what’s happening with the filter-branch command should you choose to use it:

  • --subdirectory-filter is the main command. It takes a path to a subdirectory and filters the repository to only include that subdirectory.
  • --prune-empty removes commits that don’t change anything.
  • -- --all is a way to pass arguments to the internal git rev-list command. In this case, it’s telling git to run the command on all branches.

Once the operation is done, you’ll have a new repository with only the subdirectory you specified. You can then push it to a new remote and add it as a submodule to your original repository.

git filter-branch is a destructive operation. While I’ve used the above command with success on my own repositories, your mileage may vary and I probably can’t help you if something goes wrong.

How to configure a python script as the default build task in VS Code

This snippet goes in your .vscode/tasks.json file. It will run the current python file as the default build task.

  • command will invoke the python interpreter that is configured in your workspace settings.
  • group will make this task the default build task.
  • presentation:focus will focus the terminal window when the task is run.
  • presentation:reveal will reveal the terminal window when the task is run.

Change args to any python file if you’d like to run the same file each time.

{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Python: current file",
      "type": "shell",
      "command": "${command:python.interpreterPath}",
      "args": ["${file}"],
      "problemMatcher": [],
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "presentation": {
        "focus": true,
        "reveal": "always"
      },
      "options": {
        "cwd": "${workspaceFolder}"
      }
    }
  ]
}

Introducing bulletproof-python

I owe a debt of gratitude to Claudio Jolowicz for introducing me to modern python tooling. His Hypermodern Python template and associated blog series are a great resource.

The Hypermodern system is built around nox and poetry.

  • nox is similar in spirit to tox but uses python scripts instead of configuration files.
  • It’s very powerful, but since I’m not particularly clever, I find the hypermodern stack difficult to understand.

I decided to throw together a simple template that uses tox and pip-tools. The template uses:

It strives to be clean, simple and hard to break, albeit incomplete and likely suitable only for small libraries without broad distribution.

🌱 The template is available here: bulletproof-python.


Note: The original version of this post incorrectly stated that Poetry does not play nice with pip-installs from git source. This is not the case; I simply had my pyproject.toml file incorrectly configured to use setuptools instead of poetry-core as the build backend.

TIL about AlDente by AppHouseKitchen

The ‘optimize battery charging’ feature in macOS has never worked well for me. It tends to stay at 100%, which is not great for my battery.

I recently discovered AlDente by AppHouseKitchen. It’s a paid app that allows you to set a maximum charge level for your battery. There is a free tier I believe, but I almost always eat the paid versions of apps that I like and use.

I’ve been using it for a few days now and it’s been working great. If you’re similarly annoyed by macOS’s battery charging behavior, I highly recommend it. Not a sponsored post, just a (so far) happy user.