<![CDATA[Bread & Circuits]]>https://blog.humphd.org/https://blog.humphd.org/favicon.pngBread & Circuitshttps://blog.humphd.org/Ghost 2.15Sat, 31 Jan 2026 13:22:35 GMT60<![CDATA[eget-wasm]]>Most of my focus these days is on LLMs. I have a million things to say, and I'll break my blogging-sabbatical at some point to write more in-depth on the topic. Until then, I wanted to say something about eget-wasm.

For one of the projects I'm working on, I need

]]>
https://blog.humphd.org/eget-wasm/685c8d024a8ffa03eff4dfdcThu, 26 Jun 2025 00:29:29 GMTMost of my focus these days is on LLMs. I have a million things to say, and I'll break my blogging-sabbatical at some point to write more in-depth on the topic. Until then, I wanted to say something about eget-wasm.

For one of the projects I'm working on, I need to be able to download binary releases from GitHub. Doing this reliably for arbitrary projects is surprisingly hard, since no one can agree on how you should format your URLs (e.g., how to name things so that you include the platform, arch, etc). A year ago Taras introduced me to eget, this amazing little Go binary that solves the problem perfectly. Since then, we've used it for lots of things, and it's my go-to solution.

But for eget to work, you need to have it installed on your system. There are various ways to install it, but for my use case, I need to be able to use it via TypeScript and node.js, and have it work on any platform/arch combo that the user might be running using only the NPM ecosystem.

There are various solutions to this problem. For example, esbuild uses optionalDependencies with pre-built versions for more than a dozen platform/arch combos and the hurl project uses a post install script. The Sentry Engineering blog has a nice write-up on how they do it.

Another way to do this is using WebAssembly WASM and the WebAssembly System Interface (WASI). For whatever reason, I wanted to try doing it this way. So a few weeks ago I tried to casually bait Taras into porting eget to WASM for me. The next day I woke up to this on Discord:

i have eget in wasm for you

LLMs are great, but having your friends write code for you while you sleep is even better and I highly recommend it.

Taras and I write a lot of code relay style, where one of us starts something, pushes it to GitHub and goes to bed, only to have it get picked up by the other person and improved. We worked on eget-wasm this way, with Taras doing the Go bits, and me working on the node.js part.

You can read about Taras' approach to overcoming a lack of networking in WASI here, which was really clever. For my part, I used this as an excuse to try playing with doing a pure JS thing that also does full type checking and supports TS types. I'm not sure I'd uses this style for something any larger than eget.js, but it was fun to try using JSDoc comments and jsconfig.json on this project.

Using eget-wasm, you can do something as simple as this to download the latest version of sops from GitHub for the current system:

import { eget } from 'eget-wasm';

await eget('getsops/sops');

The rest of the API is documented in the repo and the package lives on NPM as eget-wasm.

Working on this has sparked a renewed interest in WASM, and I hope to find other uses for it down the road.

]]>
<![CDATA[ChatCraft 2.0 and a Visit from Taras]]>This week I'm finishing the Winter 2024 semester, which means scaling another mountain of marking. And much like climbing any great peak, the key to success is to wend your way instead of trying to move in straight line. Survival demands regular rest, and at this time of year, my

]]>
https://blog.humphd.org/chatcraft-2-0-and-a-visit-from-taras/6623ca634a8ffa03eff4dca6Sat, 20 Apr 2024 16:05:41 GMTThis week I'm finishing the Winter 2024 semester, which means scaling another mountain of marking. And much like climbing any great peak, the key to success is to wend your way instead of trying to move in straight line. Survival demands regular rest, and at this time of year, my colleagues and I start to talk about our "ABM projects": Anything-But-Marking is a time honoured tradition among professors in our department, started decades ago by one of my mentors, Evan Weaver. ABM is how you survive marking.

For the past year, my ABM project has been ChatCraft. It's become so important to me that in December I decided to make it the focus of my open source project course in the winter. After students learn the skills necessary to work in open source, the second of my two open source courses allows them to go deeper on a single open source project over 14 weeks. No quizzes, no labs, no assignments, no exam–just pull requests, code review, triage meetings, and weekly releases. It's never the same thing twice, which keeps me on my toes. Over the past 15 years I've used all kinds of different open source projects as our focus, many with Mozilla, sometimes in low-level languages, often involving web technologies, and now focusing on AI and LLMs via ChatCraft.

The first problem with getting students involved in working with LLMs is finding funding. It's not ridiculously expensive to talk with AI, but it does involve cost, and despite what you might imagine, it's almost impossible to get funding for something like this in the context or a course. You need "private investment" or an "industry partner" to help you. I was lucky enough to find that investor: my friend and project founder, Taras Glek. When I told Taras what I was thinking about doing, he immediately said he'd be willing to help fund and support the students' work. Thank you Taras!

Armed with an exciting open source project and credits for OpenAI and OpenRouter.ai, I welcomed a small team of eager students to the project and got them started researching technologies, looking at existing issues, and planning our first release.

Since then, we've spent the past three months doing weekly triage and planning meetings, reviewing each other's work, interacting with the growing community of users, and shipping almost weekly releases. There have been 10 releases in all, with Katie tagging v2.0.0 yesterday. You can see the how they worked and what they did each week in the course Submissions Page and read their blogs, which are full of interesting insights and learning.

Together we shipped some amazing new features this term. I'd like to talk about them in more detail in a follow-up to this post, but here are some highlights for me:

  • Support for any OpenAI-compatible LLM provider, including easy switching between many providers and models
  • Support of mathematical notation rendering
  • Full Text-To-Speech and Speech-To-Text support (talk to ChatCraft or have ChatCraft talk to you!)
  • Ability to download chat messages as text, image, or audio
  • Support for image generation and multi-modal image support (share a screenshot or image as part of your chat)
  • Support for WASM based language runtimes in browser via WASI (Python and Ruby), allowing developers to run code in the browser other than JS
  • Provide free model support so you can use ChatCraft without an API Key
  • RSS feed of a user's shared chats
  • Addition of WebHandlers, allowing easy integration with your own backend function calls
  • Many, many, many bug fixes!

Part of what made this term so much fun was that it was my first time not only allowing, but encouraging students to use AI when programming. You can't build ChatCraft effectively without also using it. Often dogfooding your open source project is a chore; but ChatCraft makes you so much more productive, it's easy to integrate it into your workflow. I use it every day.

I asked the students if they were allowed to use AI in their other courses and the consensus was that they weren't. It meant that during many of our meetings, not only would I teach them how to work on open source and code, but also show them new ways to use AI in their programming. For example, as they were learning how to do proper code reviews of each other's work, I showed them how to use the /import command to pull in a DIFF or patch from a GitHub PR and get ChatCraft to help explain the changes.

Slowly they started to add ChatCraft to their workflows, and over time they became really adept at extracting useful things from it. For example, in our final meeting I heard them tell stories about using it to refactor code ("I'd paste in the whole file plus my changes and start asking for help..."), debug a UI problem with a mix of screenshots and code, figure out TypeScript types for complicated changes, edit blog posts, understand what "most users expect," etc.

Part of what made ChatCraft pair so nicely with a course on opens source is that writing on GitHub Issues/PRs and writing in ChatCraft are so similar. I have found that the key is to talk to ChatCraft aspirationally, pretending that you're working with a colleague on a GitHub PR. The difference is that with ChatCraft, if you're clear enough on what you need, you can literally prompt your colleague to do the work.

Our efforts continued to be noticed, and the small community of passionate ChatCraft users has steadily grown. Every week there has been some new person to meet on Discord, a tweet of encouragement, or an email out of nowhere from someone using ChatCraft in an interesting new way:

ChatCraft.org Star Count on GitHub

This growth was made possible because of the energy that the students have poured into the project. New features take weeks to plan, implement, and then find and fix the bugs. You need significant mass to create a centre of gravity capable of drawing in new people, and the GitHub contribution graph for the winter term shows how much the students did:

Contributions to ChatCraft by the team

As I was writing this, I was also reflecting on my own contributions. It's interesting for me to look at these graphs on a personal level, because it's clear exactly when my dad passed away in February, and the impact his death has had on me all term. This winter has also been a difficult time for me personally.


As much as the project benefited from the students' work, it also needed leadership. I was thankful for the reviews, testing, and guidance Taras gave this term. We spoke every few days about progress, evolving features, next priorities, and how to help the students succeed. Over the years doing this course, I've found that the most successful situations involve a bunch of junior developers being paired with someone senior. Having students only work with other students on a project team can't possibly produce the same outcomes or offer so rich an experience.

At our last meeting on Wednesday, Taras joined the call and had a chance to meet everyone and listen to the final presentations. We also had a chance to ask Taras questions about industry and his career. I thought he had some interesting things to say.

For example, he spoke at length about the importance of exploring new technologies (like AI), but also how critical it is to bring stubbornness and depth to your work. He explained that the difference between a good programmer and a bad programmer isn't necessarily how much they know, but rather, how stubborn they are to get to the bottom of a problem. You can't hope to succeed if you aren't willing to go deep with your tech stack in an effort to understand and fix bugs. Taras shared how his own work has required him to move all the way from the browser down into the kernel. Doing so was only possible because he was willing to ask for help and reach out to experts along the way.

He went on to connect these ideas with your effectiveness at building a network of peers. Here he emphasized the role of professional networking beyond the superficial "connect on LinkedIn" style that so many people at the students' stage suggest. He encouraged them to build meaningful relationships with people they genuinely enjoy working with and who enjoy working with you. From his own experience, he suggested that one effective way to do this is to spend time in your youth in a tech-hub city, where these types of connections and opportunities to meet other smart people are easier to come by.

On the topic of open source, Taras told many stories about his own contributions, blogging, and other publicly-visible work leading to interesting personal opportunities and jobs that would have been hard to get in any other way. The students were able to share how their own blog posts have been received, with thousands of views and some even getting offers to come and work on similar AI products.

Overall, I'm really pleased with the choice to focus on ChatCraft this term, and thankful for the small community of people who joined me. Thanks to Taras for his generosity and to the students for their willingness to trust me when I said they were capable of doing this work, despite it looking nothing like the rest of their courses. I'll never forget the meeting where Yumei announced, "I'd like to try something hard for the next release..." (she followed it up by adding support for running Python in the browser). Unfortunately, I need to get back to marking, so I'll leave you with this benediction:

May you find Yumei's courage and try something hard in the next release.

]]>
<![CDATA[In 2023]]>I've been enjoying the break between semesters and the quiet that only seems possible in the days between Christmas and New Years. This fall has been one of the busiest most challenging I can remember and I've needed this time to pause and recover. However, since I'm not capable of

]]>
https://blog.humphd.org/in-2023/658c6ef14a8ffa03eff4d796Fri, 29 Dec 2023 21:57:01 GMTI've been enjoying the break between semesters and the quiet that only seems possible in the days between Christmas and New Years. This fall has been one of the busiest most challenging I can remember and I've needed this time to pause and recover. However, since I'm not capable of relaxing as such, I thought I'd add a bit of year-end reflection. What was 2023 for me?

Professionally, 2023 was the year I fell in love with AI. I've actually tried to break into AI a number of times before, but the technology wasn't yet geared to programmers like me. I'm not a data scientist or statistician, and to be honest, I don't care that much about algorithms for optimizing loss, the merits of various model architectures, or the many other things that go into creating machine learning models; nor do I want to become any of this. Instead, I want to use AI, to bolt it into programs and workflows the same way I use open source libraries and tools.

Amazingly, in December 2023, the version of me that is writing this post is doing exactly what I always hoped would be possible and I'm now immersed in all kinds of technologies that make this way of working with AI possible. I can finally do the kind of work I've always wanted to, without needing to transform myself into something I'm not. Ironically, my ML-adjacent skillset is allowing me to go faster than many who were focused solely on ML-proper.

One such skill has been my previous investment in learning AWS, which has been an excellent foundation, enabling me to build things much larger than before. While teaching open source still occupies the larger place in my heart, I think my AWS course has likely become the more popular of my advanced courses at Seneca. I get why, too, since it's helped many former students jump into great jobs immediately upon graduation--something I find very satisfying. Being able to scale-up ideas in code is becoming an important skill.

At the start of last year, I was very much still finding my way with AI, trying to square it with my teaching practices, and feeling skeptical about its utility for programming. I spent many months writing about it, sitting on academic committees exploring it, discussing it with peers, and trying to use it myself. Those posts, and the discussions they began (both online and offline) were formative in my transition toward embracing the possibilities of AI in my work. However, by far the most significant outcome of that period was that it reconnected me with an old friend, and launched me in a direction I couldn't have anticipated.

Since my Mozilla days, I've understood the importance of writing about the work you do. It might seem pointless, since it's often unclear how a thing you're thinking about or building will connect with someone else's work. Writing in this way is a kind of intellectual fishing, whose aim is to lure interesting conversations and relationships into your life. Over 15 years ago I was doing the same thing, writing about my work, when I bumped into Taras Glek and started DXR .

In early 2023, Taras and I were both finding it hard to engage our peers on the possibilities we saw with AI and programming. Each of us was writing about it in our blogs, and seeing the other do the same. For me, what was starting to feel obvious, was still too far removed from the familiar patterns and "best practices" of academia. In industry, Taras was having similar frustrations. I think it was our lack of community, combined with a prior understanding of how to work on experimental things quickly, that allowed us to rebuild our relationship so effectively around exploring AI together.

Taras had a baby AI web project he was having fun building, and wanted me to contribute. I really hated the idea of getting sucked into a front-end programming project on the side of everything else I was doing (CSS, really?), but I agreed to help him improve it. Together we built ChatCraft.org and used it throughout the summer as a laboratory for exploring the many evolving features of LLMs, often in real-time as they were released.

Since then, ChatCraft has become the most productive tool I've ever built. I use it daily, as do hundreds of other people, amazingly, given how saturated this space is. I checked just now and I have nearly 900 separate chats in my local ChatCraft database! I realized earlier this week that I use ChatCraft more than Google and Stack Overflow combined. It's shocking how deeply integrated it has become into my software development workflows. Surprisingly, I don't use any other AI tools (e.g., Copilot). Being able to shape how my AI tooling works has been extremely powerful. When I've needed ChatCraft to do something new, I modify the code (usually with ChatCraft's help) and it does it. I can't think of another software project that I've worked on that has had this much value to me personally.

AI is part of what made ChatCraft so successful, but it's only one ingredient. Like all successful applications of AI, how well you connect it back to humans is what ultimately determines its usefulness. ChatCraft is AI, yes, but it's also the working dynamic I have with Taras. He and I are completely different in our skills, approaches, preferred tech stacks, backgrounds, etc. If I was going to engineer a partnership, I'd never think to start it this way. And yet, our different approaches, when combined, have been the key to our success.

For example, I'm obsessed with doing things The Right Way and Taras revels in being completely unorthodox. Where I know that you're supposed to build things this way using these prescribed technologies, Taras uses whatever will let him go the fastest, mercilessly switching when he reaches the limits of an earlier choice. Where I rule out using tools I don't know, Taras is fearless in adopting previously unknown tech.

But my careful, buttoned-up, read-the-manual approach has also influenced him, and the effect we each have on the other has moved us both to an amazing new productivity plain. The same pattern happens over and over: one of us will start a branch in git, get stuck, and ping the other, who will push 5 more commits to unblock it, and throw it back. What I can't do, Taras is an expert at, and vice versa. All of this happens asynchronously, remotely, across multiple timezones. In fact the effectiveness of this style of working is amplified by our timezone differences--you go to bed stuck and wake up to a solution in your inbox. Lots of people talk like remote work can't be as productive as in-person, but this way of working is the most productive either of us has ever been.

A few times in my career, working on software projects has felt more like playing in a band than doing "computer science" or "software engineering." In 2023, my work on AI with Taras has very much been "making music with a friend." The fuzziness of AI has also made the experience more creative. I've enjoyed it all immensely and learned so much. Thank you, Taras.

Ultimately what this work made possible was for me to explore more areas of interest by expanding my abilities. Many of the tools and technologies I've been using this fall look vastly different to what I've used in the past:

  • TypeScript is now my favourite programming language (sorry JavaScript). I never thought I'd say that, but working on and maintaining larger code with TS has been a dream. TS + pnpm + vite/vitest has become my typical starting point.
  • I can finally use Python productively, and have used it to fulfill a personal goal and build multiple conservation AI projects for audio and video analysis of acoustic recorder, and camera trap data. A secret weapon in the fight against Python dependency problems has been pixi, which I love and always use now.
  • IaC tools like Terraform, Terragrunt, Ansible, GitHub Actions and many more have allowed me to build reliable cloud infrastructure more quickly. I'm eager to find ways to bring more of these into my teaching this year.
  • Docker and Containers in general have been increasingly important as I've worked on deploying AI models. I've been using containers in non-traditional ways and even in development with Development Containers (this has been a fascinating way to share knowledge across a dev team). In 2024 I'm going to be adding Kubernetes to this list, which is another one of those "not for me" technologies I've avoided that I'm having to contend with now.

On a more personal note, another major theme of 2023 has been learning more about what it means to be part of the Sandwich Generation. My dad isn't well, and we've been helping he and my mom pack up and move out of the home they've lived in together for 50+ years. That's been hard for everyone, and it's helped me understand more about my own limits and how fragile we all are as we age.

The reality of life for my dad (and my mom, who is caring for him) has had a profound impact on me. Because I've been so connected to the plight of an immunocompromised senior, and paired that with personal learning about the evolving science on Covid and stayed connected with data like this recent wastewater graph in Toronto, I've found it hard to make sense of the total collapse of public and personal health in 2023:

Toronto Wastewater Data, Christmas 2023

The disconnect between what's actually happening and how people are dealing with it (or not dealing with it), has been made worse by my refusal to pretend. I've literally taught thousands of students during the pandemic, and with that many people coming and going in my life, the variety and severity of stories I've heard has been heartbreaking. So many have lost parents, spent weeks or months in hospital, had to drop out due to their own long covid or so that they can care for loved ones suffering the same. Just this past week I got an email from a woman in my class who is withdrawing after spending the entire fall at a local children's hospital with her 1 year old, who never recovered from Covid in September. Maybe in my position, I've seen more than most people; or maybe I've just been willing to see what others have decided to avoid looking at altogether. In either case, it's changed me.

There's not a lot one can do anymore, but for my part, I spent all of 2023 lecturing in a mask, and I was grateful to the students who joined me, despite my school removing all protections and helping to re-enforce a culture that ignores the most vulnerable amongst us. I'll continue do the same in 2024. Despite the obvious challenges, masking has kept our entire family from getting sick with anything since March 2020 (no Covid, no RSV, no flu...nothing), which is remarkable given where our society has been and is heading right now. Masks have worked for me.

Another reason I wanted to write this is that I've really missed my professional connections on Twitter and learning from my friends and colleagues--I realize I have to write in the same ways I wish others would do for me. So many people I used to read on a daily basis are gone from social media now. I can't say I blame them. Twitter died in 2023, and I've found its demise a sad one. I still use it, because certain communities I value only exist there; but I'm there less and less, and I wonder if I'll still be there a year from now. Like so much of my life, I'm learning how to exist and be happy in smaller spaces. So I'm thankful for this blog, which is turning 20 soon.

Thanks to so many who wrote and maintained the code I relied on this year. Thanks to those who taught, encouraged, and supported me. I've appreciated you. May God bless you in 2024.

Happy New Year, internet.

]]>
<![CDATA[Teaching ChatCraft to do Citations]]>Back in the summer, I did a bunch of work to give chatcraft.org the power to use functions. Since then, Taras has improved it to allow you to use TypeScript and JavaDoc-style comments to build your function with full metadata understood by OpenAI. I didn't really have a reason

]]>
https://blog.humphd.org/teaching-chatcraft-to-do-citations/654514b84a8ffa03eff4d73fFri, 03 Nov 2023 15:56:36 GMTBack in the summer, I did a bunch of work to give chatcraft.org the power to use functions. Since then, Taras has improved it to allow you to use TypeScript and JavaDoc-style comments to build your function with full metadata understood by OpenAI. I didn't really have a reason to use it at the time, but it was the kind of "nice to have" feature that seemed good to include.

But this week I did need it. I was talking with some people about how they were using ChatCraft to help format citations properly, but they were frustrated that it would make up details. "It's too bad it can't get the real info and use that." But it can!

I wrote a small TypeScript function to use the CrossRef REST API to get all the metadata that's known about a given DOI or DOI URL. I then gave ChatCraft access to the function by including this in my prompt @fn-url:https://gist.github.com/humphd/74cf88283239c62f53caff5cddf4cfe5. This @fn-url:... syntax tells the app it can use a function hosted at the given Gist URL. The function describes what it can do, and the inputs/outputs. OpenAI can then decide when/if to use it.

Here's an example of what it looks like when you use it:

https://chatcraft.org/c/humphd/CVwmeKqB8iwwIO9CYKeV2

I prompt the LLM like so:

I need to cite https://doi.org/10.1093/ajae/aaq063 in APA, MLA, and Chicago

And it calls the function, gets back the metadata, and returns a response:

To cite the article "An Analysis of the Pricing of Traits in the U.S. Corn Seed Market" in APA, MLA, and Chicago styles, you can use the following formats
APA: Shi, G., Chavas, J., & Stiegert, K. (2010). An analysis of the pricing of traits in the U.S. corn seed market. *American Journal of Agricultural Economics, 92*(5), 1324-1338. doi:10.1093/ajae/aaq063
MLA: Shi, Guanming, Jean-paul Chavas, and Kyle Stiegert. "An Analysis of the Pricing of Traits in the U.S. Corn Seed Market." *American Journal of Agricultural Economics*, vol. 92, no. 5, 2010, pp. 1324-1338. doi:10.1093/ajae/aaq063.
Chicago: Shi, Guanming, Jean-paul Chavas, and Kyle Stiegert. "An Analysis of the Pricing of Traits in the U.S. Corn Seed Market." *American Journal of Agricultural Economics* 92, no. 5 (2010): 1324-1338. doi:10.1093/ajae/aaq063.
Please note that the DOI (Digital Object Identifier) is included at the end of each citation.

Extending ChatCraft with extra powers is really easy by adding @fn:name (for functions you write in the app itself) or @fn-url:url for functions you want to share with others.

]]>
<![CDATA[Releasing ACTION]]>There are many milestones as a father, from watching your children learn to walk and talk, to learning to ride a bike and drive a car. Sometimes you're lucky enough to be involved in these experiences and get to share in the joy of discovery and feelings of success. This

]]>
https://blog.humphd.org/releasing-action/6542aa614a8ffa03eff4d4e4Thu, 02 Nov 2023 12:08:30 GMTThere are many milestones as a father, from watching your children learn to walk and talk, to learning to ride a bike and drive a car. Sometimes you're lucky enough to be involved in these experiences and get to share in the joy of discovery and feelings of success. This week was like that for me. I got the chance to help my youngest daughter release her first open source project.

Automated Camera Trapping Identification and Organization Network (ACTION) is a command-line tool that uses AI models to automate camera trap video analysis. It works on both terrestrial (mammals, etc.) and aquatic (fish) camera footage. Typically a project will have hundreds or thousands of videos, and manually scrubbing through them to find positive detections is incredibly time consuming and error-prone.

Using ACTION, one or more videos can be analyzed with AI models specially trained to detect animals in camera trap footage or fish in underwater video. It uses the open source YOLO-Fish and MegaDetector neural networks to build a video analysis pipeline. The models are able to detect an "animal" or "fish" with a given confidence score in an image:

ACTION finding "Animal"s via MegaDetector in a frame of video
ACTION finding a "Fish" via YOLO-Fish in a frame of video

However, as powerful as these AI models are, they aren't programs you can run on their own to accomplish real-world tasks. I don't think people realize this, since it seems like everything has "AI" in it these days. But the reality is quite different: you can use powerful AI models to do super-human things, but they require you to build custom pipelines around them to extract video frames, resize images, process detections, draw bounding boxes, process clips, create new videos, and allow a user to customize many settings. Then you have to package it up in a way that others can use without downloading and installing half the internet. The process turns out to be quite involved, and, dare I say, worthy of its own open source project!

Like all useful tools, ACTION was born out of genuine need. My daughter, who is studying biodiversity at university, is currently completing an internship with a local conservation authority that is studying the endangered Redside Dace in their aquatic ecosystems.

ACTION successfully detecting a Redside Dace, along with a White Sucker

Specifically, their researchers have been comparing the effectiveness of different conservation monitoring technologies, from electrofishing to eDNA (environmental DNA) to "aquatic camera trapping." The project seemed like a great fit, since my daughter is an expert on camera trapping, with years of existing field experience. However, applying the same ideas to underwater cameras was a totally new challenge.

Typically camera traps use motion and/or heat sensors to detect animals and trigger recordings. In underwater environments, these same techniques don't map as well, since temperature, visual noise (i.e., debris and water turbidity), and light levels don't work the same as on land.

In her work, she needed to be able to record underwater environments at various sites using multiple cameras (i.e., multiple cameras upstream and downstream) and compare this to results from eDNA samples taken at the same time. The fish she is studying are all small (i.e., minnows) and move very fast--nothing stays in frame for more than a second or two, sometimes less.

The footage ends up being periods of cloudy water with no fish, then suddenly a fish! then nothing again. It's difficult to manually find these fish in the collected video footage and so easy to miss them completely as they race past in the current.

Not all fish are camera shy

"Finding the fish in these videos is going to take you forever," she was told. Challenge accepted! So we built ACTION together. Using the existing AI models with our custom pipeline, we are able to transform hours of video footage into short, separate video clips that contain all the fish. After this, the serious work of species analysis and identification is much easier.

The process of building the tool together with my daughter was a lot of fun. In addition to the internship, another of her current courses is Bioinformatics, where she's learning Unix, Bash scripting, and Python. This made it easy for us to choose our tech stack. As we iterated on the code, we were able to incorporate real-time feedback from the fieldwork and data she was collecting. The feedback loop was amazing, since we quickly understood what would and wouldn't work with actual data.

For example, unlike with terrestrial camera traps, it became clear that having sufficient amounts of buffer (e.g., video frames before and after a detection clip) was critical: while a Racoon might amble in front of your camera for 20 seconds, fish are there and then not. You need extra frames to be able to slowly scrub through when identifying a flash of colour (was that a Redside Dace?).

ACTION detecting a family of Raccoons via MegaDetector

The real-world testing helped us come up with a useful set of options that can be tweaked by the user depending on the circumstances: what should the confidence threshold be for reporting a positive detection? how long should each clip be at minimum? where should the clips get stored? should the detection bounding boxes get displayed or saved along with the clips? Week by week we coded, refined our approach, and tested it on the next batch of videos. Real-world testing on live data turns out to be the fastest path to victory.

Eventually the fish detection was working really well and we wondered if we could extend it to work for terrestrial camera traps. Would the code work with MegaDetector, too? The answer turned out to be "yes," which was very rewarding. I've wanted to play with MegaDetector for years, but never had the reason to get started. Once the pipeline was built, swapping in a different AI model was doable (though not easy). We think that having this work for traditional camera traps, in addition to aquatic cameras, adds a lot to the potential usefulness of ACTION.

ACTION detecting a Green Heron via MegaDetector

After we finished the project, we knew we had to share it. Imagine if this had existed when the internship started (it didn't, we looked!). We would love to see other people benefit from the same techniques we used. Also, the fact that YOLO-Fish and MegaDetector are both open source is an incredible gift to the scientific and software communities. We wanted to share, too.

Together we packaged things with pixi (thanks to the maintainers for accepting our fixes), learned how to do proper citations on GitHub, and used GitHub Releases to host our ONNX model files (protip: you can't use git-lfs for this). It was excellent real-world experience to see what's involved in shipping a cross-platform project.

For me, the most difficult problem technically was figuring out how to optimize the pipeline so it could run efficiently on our machines. We don't have access to fancy GPUs, and needed something that would work on our laptops. The answer was two-fold. First, converting the models to ONNX format and using ONNX Runtime instead of Yolo or PyTorch/TensorFlow as our model runtime. This was a game changer, and also let us throw away a whole host of dependencies. Finding the right magical incantations to achieve the conversions kept me up a few nights, but thanks to https://github.com/parlaynu/megadetector-v5-onnx and https://github.com/Tianxiaomo/pytorch-YOLOv4 we eventually got there. We also learned that we didn't need to analyze every frame in a video, and could use some tricks to make things work faster by doing less (as always, the fastest code is the code you don't run).

ACTION detecting a Flying Squirrel mid-flight via MegaDetector

I learned a ton working on this project, and I'm thrilled to have been able to get involved in some of the amazing conservation biology research that I see my daughter doing all the time. At the start of the summer, I set myself the task of figuring out how to use AI and conservation technology together, and this was the best possible realization of that goal. It's very rewarding to have my skillset compliment my daughter's (neither of us could have done this project on our own), and I'm excited that I finally got a chance to work with my favourite scientist!

Do me a favour and please go give https://github.com/humphrem/action a star on GitHub, try it out, tell your friends, and let us know if you use it. Until then, we'll look forward to seeing your citations roll in on Google Scholar.

]]>
<![CDATA[A month with AudioMoth]]>In my personal time over the past few months, I've worked on a lot of code to connect machine learning models to various conservation, species-detection problems. A bunch of this work has focused on automating species detection in camera trap videos, both terrestrial and aquatic. I'll write about that work

]]>
https://blog.humphd.org/a-month-with-audio-moth/6519ad634a8ffa03eff4d33cSun, 01 Oct 2023 18:46:40 GMTIn my personal time over the past few months, I've worked on a lot of code to connect machine learning models to various conservation, species-detection problems. A bunch of this work has focused on automating species detection in camera trap videos, both terrestrial and aquatic. I'll write about that work in another post at some point, but today I wanted to talk about a related project: automatic species detection for birds using audio recordings done with an AudioMoth.

AudioMoths are small, relatively inexpensive acoustic loggers used by bird, bat, and other biologists, ecologists, and people like me, to monitor wildlife species based on sound. Imagine a device, smaller than a deck of playing cards, with a built-in microphone, SD storage card, and microprocessor that can be programmed to record audio on a specific schedule.

AudioMoth
AudioMoth (without case)

Our family has spent years working with camera traps, which are great for collecting photos or videos (we prefer video) of wildlife based on movement. The camera traps we use do record some audio, but it's poor quality. I really wanted to see how much better I could do with an audio-only device like the AudioMoth. I've loved using Merlin Bird ID to do bird species detection on my phone, and I wanted to see if I could do this kind of monitoring long-term in an environment.

Unfortunately (for me), AudioMoths are very popular with researchers, and can be hard to acquire. Last winter I started stalking the site where you can buy them in North America. You have to wait for them to have enough backers to build a new run of these units. Eventually in early summer, I was able to buy one and it shipped at the end of August.

I spent a number of days learning how to use it. You need 3 AA batteries, a micro-SD card, and a cable to connect to it via USB. There is an open source app that lets you create a config file, defining your recording settings, schedule, etc.

AudioMoth USB Connection
Programming the AudioMoth

I also opted to buy a waterproof case, which comes with a velcro strap you can use to attach the unit to a tree limb. After this you mount it wherever you want to do your recording.

AudioMoth Waterproof Case

It took me the better part of a week to figure out the right settings to use (I'm still not 100% sure what I'm doing is optimal). I've found the AudioMoth to be a bit more like a research tool vs. a commercial product, in terms of its ease of use for an amateur/enthusiast like me. However, I'm used to learning-as-I-go with technology, and if you're willing to tinker and experiment, you can get good results.

Once I was satisfied that my recordings were working, I moved the device into a better spot to try a longer experiment. I picked an area where a number of different ecosystems meet: mixed deciduous forest, scrub brush, swamp wetland, edge of a creek, etc. This is usually a great birding spot, and also unlikely to cause issues with recording human activity (too early, too buggy, too dense):

AudioMoth Location at intersection of different habitats

My goal was to record the bird chorus for 2 hours every morning (5:30 am - 7:30 am) for the month of September. I wanted to collect this data in order to understand which species were moving through our woods during fall migration.

Yesterday I went to collect my data. However, when I got there, the AudioMoth was gone! Something (raccoon, likely) had undone the velcro strap and it was laying in the long grass beneath the tree. I'm going to need a better mounting option!

After downloading all the .wav audio files from the SD card, I wrote a bunch of Python code to use BirdNet as my species detection algorithm. BirdNet can detect ~6,000 species, and uses latitude and longitude, as well as historic sighting data from eBird to determine likely species lists for a given location/period.

My code uses the open source BirdNet-Analyzer via birdnetlib, both of which are amazing. I was able to very quickly go from concept to working code in a matter of hours. The BirdNet lib is able to extract data and audio recordings for each detection. For example, you might get a detection like this, along with a 3 second .mp3 file:

{
	"common_name": "Common Yellowthroat",
	"scientific_name": "Geothlypis trichas",
	"start_time": 3207.0,
	"end_time": 3210.0,
	"confidence": 0.732090950012207,
	"extracted_audio_path": "20230831_053000_3206s-3210s.mp3"
}

This is amazing! However, coming from a "camera trap" background, I really wanted to have something larger than a single clip. I decided to analyze all of the detections to find the top 10 by confidence (i.e., how confident is the model about the species) for each species, then use that to create a kind of "greatest hits" sample that combines each individual detection one-after-another.

This produced fantastic results, making it so much easier to compare audio for a species to other recordings, even those that occurred on different days. Having multiple samples to listen to back-to-back also helped get a better sense of the range of what each species sounds like.

I did some more analysis on the data to get a complete list of the 39 species for the month:

- American Crow
- Yellow-rumped Warbler
- Common Yellowthroat
- White-breasted Nuthatch
- Black-capped Chickadee
- Cedar Waxwing
- Common Grackle
- Blue Jay
- Downy Woodpecker
- American Goldfinch
- Swamp Sparrow
- White-throated Sparrow
- Eastern Bluebird
- Pileated Woodpecker
- Ruby-throated Hummingbird
- Hairy Woodpecker
- Gray Catbird
- Golden-crowned Kinglet
- Song Sparrow
- American Robin
- Eastern Phoebe
- Killdeer
- Eastern Kingbird
- Canada Goose
- Northern Flicker
- Barn Swallow
- Swainson's Thrush
- House Finch
- Rose-breasted Grosbeak
- Yellow Warbler
- Magnolia Warbler
- Blackpoll Warbler
- White-crowned Sparrow
- Common Loon*
- Savannah Sparrow
- Chestnut-sided Warbler
- Greater Yellowlegs
- Lesser Yellowlegs
- Ruby-crowned Kinglet
- Red-bellied Woodpecker

I've been able to confirm most of these manually (to the best of my ability), however, the Common Loon detections are for sure not right (the recordings were of coyotes howling). I'll need to tweak my parameters to the AI model.

Here's a graph of all the species and how many days each one was detected, giving a sense of frequency across the month. I was surprised that the Yellowthroats were the most numerous.

Overall, I'm really impressed. This was my first major test of the setup and my software, and other than nearly losing the AudioMoth in the swamp, everything worked great.

My next goal is to build a web-based explorer for the data. For example, I don't have a great way to share the audio files and I'd love to pull in open licensed images of each bird species to build a multi-media experience that makes this data more accessible to others. Thinking about this, it strikes me that it would be cool to build a bird-species visualization toolkit, since so much of what I need (open licensed images, audio, video, text) would also be needed by anyone else who wants to work on bird web apps. Maybe I'll find that this already exists, which would be great.

In the meantime, if you're interested in wildlife audio recording, I'd recommend picking up an AudioMoth and doing your own experiments. With very little knowledge, equipment, or code, I was able to start answering some interesting questions about my own local ecosystems.

]]>
<![CDATA[Teaching ChatCraft to use Tools]]>It's been a month since I wrote about the work we're doing on ChatCraft.org. Since then we've been focused on a few things and I wanted to write about how they work.

First, Taras added support for OpenRouter, which was extended by Chigala Kingsley. Previously, we could only work

]]>
https://blog.humphd.org/teaching-chatcraft-to-use-tools/64c819a44a8ffa03eff4d253Mon, 31 Jul 2023 21:06:02 GMTIt's been a month since I wrote about the work we're doing on ChatCraft.org. Since then we've been focused on a few things and I wanted to write about how they work.

First, Taras added support for OpenRouter, which was extended by Chigala Kingsley. Previously, we could only work with OpenAI models, but OpenRouter also gives us access to Google's PaLM, Anthropic's Claude, and Meta's Llama models. This is fantastic, because it pairs so nicely with our existing ability to work with different models in the same chat (i.e., send a prompt to any model, or retry a response with a different model). It's amazing being able to mix and match responses from so many LLMs across all these providers within the same tool. This is the real power of ChatCraft, which isn't beholden to any particular LLM or vendor.

Second, we've been working a lot on adding support for function calling and the ability to create tools. This is a feature that Taras and I have discussed at length for months, and it's been extremely difficult to nail down the UX. However, we've finally managed to ship an initial version.

OpenAI and ChatGPT have had the ability to use functions for a while. The concept is this:

  1. You define the interface for a function (name, description, and schema for the arguments). Imagine a function that can add two numbers, download a file and process it somehow, etc.
  2. You let the LLM know about the existence of the function and either tell it to call it, or let it decide if it needs to use it when you send your prompt (i.e., functions can be optional or required)
  3. If the LLM wants to call your function, it sends a special message in JSON format with the name and arguments it wants to pass to the function
  4. The LLM never calls the function–you have to do this on your own. When you're done executing the function on its behalf, you send the LLM the result and it continues processing

ChatGPT lets you do this, too, but you're not in control of the process or functions. We wanted to give users the ability to easily define and run their own custom functions, and to do so directly in their browser.

ChatCraft defines a function as an ES Module that includes the function and its metadata:

  1. The name of the function
  2. The description of the function
  3. The schema of the parameters (simplified JSON schema)
  4. A default function export, which is the function itself

Here's a simple example, which can calculate the sum of a list of numbers. LLMs aren't great at doing math, so a function that can do the calculation is really useful. A function can do whatever you want, as long as it's doable in the context of browser-based JavaScript (or via CORS and API calls). You return a Promise with your result and we send it back to the LLM. If you want your data to get formatted in the UI you can wrap it in a Markdown codeblock (e.g., 3 backticks with a language).

These function modules can be written and hosted inside ChatCraft itself: https://chatcraft.org/f/new will create a new one. Or you can host them (as plain text) on the web somewhere (e.g., as a Gist). In both cases we load them dynamically and run them on demand for the LLM.

You tell the LLM about these functions using the syntax @fn: sum or @fn-url: https://gist.github.com/humphd/647bbaddc3099c783b9bb1908f25b64e, where @fn refers to the name of a function stored in your local ChatCraft database, and @fn-url points to a remotely stored function.

I shared a chat earlier today to demonstrate how this works, and you can see it here:

https://chatcraft.org/c/humphd/LtEkj_J3p66MkDadwzwrH

Here I use my remote function via the @fn-url syntax, and GPT-4 incorporates it into its answer. We show all the steps of the process in the UI: the function call, the result, and what the LLM does with it. In theory you could also collapse this all into one message, but we think "showing your work" is a more powerful approach.

That said, we're just getting started with the UX for this, and every time we've tested things we've realized new ways to make it better or bugs that need to be fixed. However, it actually works and we're really excited about it! It's very easy to experiment and get things running fast.

The real power of this feature is going to come from combining it with custom system prompts, where we give the LLM an initial context and set of functions it can use to achieve specific goals. Getting both of these features in place over the summer has opened up a very interesting space that we're excited to start exploring.

If you want to try this today for yourself, you're welcome to do so (it's live on ChatCraft.org). Let us know what you build, what breaks, and what you think we should do next.

]]>
<![CDATA[Time vs Size]]>I've been thinking about how much I favour measurements in time vs. size and how the shift to a chronological perspective allows me to overcome my fear of engaging with large tasks. This week a few things have overlapped in my mind to reinforce this idea.

First, during my summer

]]>
https://blog.humphd.org/time-vs-size/64b29fb94a8ffa03eff4d076Sat, 15 Jul 2023 15:16:56 GMTI've been thinking about how much I favour measurements in time vs. size and how the shift to a chronological perspective allows me to overcome my fear of engaging with large tasks. This week a few things have overlapped in my mind to reinforce this idea.

First, during my summer holidays I've been binge-watching the Escape to Rural France YouTube channel, where Dan Preston is restoring a burnt-out château ruin (the Château de Chaumont). My eldest daughter has long been a fan of various château restoration channels, which is partly why I suspect this series ended up in my YouTube recommendations. "Dad, you'd love this," and she's right. I initially watched one episode, then another, and now I'm hooked. The task he's taken on is enormous, and the hubris and total disregard for practicality is what makes it so engaging.

It's slow, which I love. Each episode is focused on a day of work, which might entail clearing trees, shovelling debris, repairing brickwork, or discovering a beehive in an old bathroom wall. There is no urgency to the pace. The scale is both human and beyond reach: nothing big happens, and yet over time and bit by bit, large transformations occur. In an episode I watched a few days a go, Dan pauses his work laying floor joists to reflect: the key to this work is thinking about everything as a set of small jobs vs. the whole, which would overwhelm.

One of my favourite regular characters on the channel is Nick, the tree surgeon. Dan and Nick spend a lot of time thinking about how to open up and restore the grounds, which have been badly overgrown. Nick even spends one episode removing a massive tree that's grown inside the Château. He also reflects on the need to think in years and decades, planning for the woodland, but recognizes how small actions taken now, in the present, will come to influence the future.

Watching Dan and Nick slowly pick away at massive jobs has been inspiring for me. This past week it motivated me to start work on a project I've wanted to do for a few years. On our property we've been overrun with Spotted Knapweed, an invasive plant from Europe that takes over Ontario grasslands, outcompetes native species, and increases fire risk.

In the past decade these "purple flowers" went from being something we thought were pretty to being the dominant plant in many areas of the property. My youngest daughter has got me thinking more and more about the negative effects on biodiversity that invasive vs. native species have. I've long wanted to do something about it, but every time I've contemplated it, the job has felt impossible. You can control Knapweed with chemicals, but I don't want to go that route. The alternative is to hand-pull it.

The weather here recently has been rainy, and with the rain the soil has been perfect for going to work on the Knapweed. I've spent days carefully removing plant after plant, making sure to get the root. Some plants have been taller than me, while others are just starting to come up. Regardless, they all get pulled.

The task seemed ridiculous at first. And yet, bit by bit and slowly, the patches of Knapweed are giving way. Because I'd always thought about this project as a whole, starting it never made sense: surely there is no way to do this by hand, the job is too big! But by converting the size of the task to a series of steps in time (i.e., "I'll do this patch today, that patch tomorrow, ..."), I've been able to make an impact.

The consequence of approaching problems from the perspective of time is that it becomes possible to begin. I'm not worrying about completing the project, which in the case of the Knapweed will likely never be done; rather, I'm deciding to participate today. The problem is no longer measured in square meters but hours and days.

As I've been pulling Knapweed, I've also come to realize that this is my preferred approach to software as well. I spend most of my time working on software vs. natural ecosystems, and in that work I've long known that many small fixes are invariably better than one massive effort. I learned this first when working on Mozilla, where the scale of the code was inhuman (no one knew or understood it all, least of all me), but everything was accomplished through the repeated contributions of individuals.

I've since been reminded of this in my work with Taras on chatcraft.org. I would never have started this project on my own because it would have looked too big. Taras coming to me with something already started made it feel possible–I'm much more comfortable fixing bugs and improving code vs. writing it from zero. That's the risk of "product thinking," focusing on applications vs. their code, where the former requires everything to exist, but the latter can be useful at various stages. The reality is that by picking away at something, editing and refactoring what was already there, and slowly fixing and cleaning-up the code, you're able to make something really amazing. Especially if you don't get fixated on being "finished" and instead learn to embrace the half-finished nature of the work, it's possible to go very far.

I'm generally not good at being on vacation, but by allowing myself to exist at the intersection of a French château, Spotted Knapweed, and chatcraft.org, I'm enjoying the possibilities of existing in time.

]]>
<![CDATA[What Edit Unlocks]]>Last week I wrote about a bunch of new ChatCraft features we shipped, including the ability to edit any message in a chat. This week we explored what this new edit feature could enable, extending the idea to the system prompt, which was previously hidden and fixed. By combining a

]]>
https://blog.humphd.org/what-edit-unlocks/6495f0eb4a8ffa03eff4cf50Fri, 23 Jun 2023 20:13:03 GMTLast week I wrote about a bunch of new ChatCraft features we shipped, including the ability to edit any message in a chat. This week we explored what this new edit feature could enable, extending the idea to the system prompt, which was previously hidden and fixed. By combining a visible system prompt with an edit button, we've been able to unlock some really interesting abilities in the app.

A lot has been written about the need to create detailed prompts for LLMs. However, I've been equally interested in exploring what I could do by altering the behaviour of the model via a good system prompt. System prompts (or system messages) are often the first message in a chat. Most apps don't expose them to users. From the OpenAI docs:

Typically, a conversation is formatted with a system message first, followed by alternating user and assistant messages. The system message helps set the behavior of the assistant. For example, you can modify the personality of the assistant or provide specific instructions about how it should behave throughout the conversation. However note that the system message is optional and the model’s behavior without a system message is likely to be similar to using a generic message such as "You are a helpful assistant."

The system prompt helps to set the tone of your interaction with a model. Do you want output in a particular format? Are certain types of responses required and/or forbidden? Are there underlying assumptions that the model needs to know about when interacting with the user? The system prompt, like stage direction in a play, lets you guide the model on how to best play the part of a character in the story that's about to unfold.

As I mentioned above, most apps try to hide their system prompt, and there's lots of interest when someone can goad a model into revealing what it is. I understand why many apps can't disclose this, and don't want people trying to circumvent it. However, one of the things I love about ChatCraft is that we don't have to establish a wall around how the system works. Instead, everything is editable!

Our current system prompt looks like this:

I am ChatCraft, a web-based, expert programming AI assistant. I help programmers learn, experiment, and be more creative with code.

I follow these rules when responding:

- Use GitHub flavored Markdown
- ALWAYS include the programming language name (js) or type of data (csv) at the start of Markdown code blocks
- Format ALL lines of code to 80 characters or fewer
- Use Mermaid diagrams when discussing visual topics

It's worked quite well, not least because we aren't trying to dramatically alter the nature of the LLMs we use (it can be hard to get a model to go against its training. For example, try getting a model to respond about code, but never give actual source code).

As we've built ChatCraft, we've iterated on this prompt a few times to fix bugs, or nudge the responses toward or away from certain types of output. It's pretty easy to change, but requires you to submit a PR to GitHub.

With our new edit feature, we wanted to see if we could make this process more open to experiments by users. For example, I've wanted to test-drive the ChatCraft UI as a way to do research into biology and ecology vs. programming, and Taras has been exploring business topics. Obviously these changes aren't something that we're going to land in git, so doing it was possible but frustrating.

By replacing our old "I'm a helpful assistant!" greeting message with an actual system prompt we expose in the UI, we make it possible to let the user take a more active role in establishing the limits and potential of the conversation. Here's the new UI:

Greeting converted to System Prompt

The system prompt is truncated, but clicking "More..." will reveal it. The user can also click "Edit" in the message's menu to customize it:

Edit a System Prompt

Now it's easy to do both minor tweaks or major surgery. Here are 2 examples of the same prompt, but each chat uses a customized system prompt. Notice how different each conversation becomes as a result of these changes–one in French, the other in short sentences:

System Prompt altered to get French responses
System Prompt altered to get short responses

These examples are really minor adjustments, but they show how powerful the technique can be. I think that as we begin to include the system prompt in the scope of what we allow the user to alter, we'll discover new ways to work with the tool more quickly.

Taras and I are excited to apply this technique to our upcoming explorations into adding functions to ChatCraft. Imagine being able to use ChatCraft to help you write functional tools that can then be used to solve problems in the conversation. We think it's possible, and this is taking us one step closer!

Let us know if you manage to do something interesting with a custom system prompt or have more ideas.

]]>
<![CDATA[My Spring with ChatCraft.org]]>It's been a month since I last wrote our work on ChatCraft.org, the developer focused AI open source project that I've been working on with Taras. In that time, we've been able to iterate on the UX, work with users to add new features, and implement a number of

]]>
https://blog.humphd.org/my-spring-with-chatcraft-org/648b23c54a8ffa03eff4cca2Thu, 15 Jun 2023 17:24:26 GMTIt's been a month since I last wrote our work on ChatCraft.org, the developer focused AI open source project that I've been working on with Taras. In that time, we've been able to iterate on the UX, work with users to add new features, and implement a number of the ideas I described in my last post. It's coming along really well, so I thought I'd take a few minutes to show you what we have right now.

As I've worked on the code, and used it myself, the definition of what we're building has evolved. At its most basic level, ChatCraft.org is a personal, web-based tool for discussing code and software development ideas with large language models. "So it's ChatGPT, right?" Sort of, but increasingly less so. ChatCraft is really a tool for iteratively writing and thinking about code with LLMs.

As an open source software developer and educator, I spend the majority of my day in the GitHub UI: writing feedback, reviewing code, reading and adding comments to issues and PRs, and searching. The flows and conventions of reading and writing on GitHub are now built into how I want to work: linkable, editable Markdown everywhere. As such, I've had a strong desire to find a way to replicate my approach to talking to developers with my LLM conversations. These are obviously completely different activities, both in aim and outcome; but they share an important idea. In both cases, I'm thinking through writing.

Much as I'm doing now in this post, I write to understand and solidify my thinking. When I'm blogging, I'm trying to communicate what I'm experiencing, first to myself, but also to others. Similarly, when I'm working on GitHub, I'm at once attempting to develop both a line of thought as well as a line of commits.

Taras laughs at me for doing everything in pull requests vs. simply landing small things on main. However, I find that I need the extra layer of explanation and writing to wrap around my code. Just as I'm hoping to cleanly merge my branch with yours, I also need to find a way to turn what I'm thinking into something that can be integrated with what you are thinking. I do this through writing, sometimes in code, more often in prose.

Because I write so much on GitHub, I've come to appreciate its affordances. I think this comfort and familiarity with its UX has influenced how I wanted to see ChatCraft evolve. Let me show you some of what I mean.

Here's the start of my most recent chat with ChatCraft, in which I'm fighting with a re-rendering bug in a hook within ChatCraft:

All chats have their own URL and get stored in a local database. This chat is https://chatcraft.org/c/W1F3Tgin1QE5UFb5MMtBy. You won't be able to open that link and get my data because it's stored in IndexedDB in my browser. We're using Dexie.js to interact with the database, and it's been fabulous to use with React. Taras has dreams of using SQLite with WASM in a Worker down the road, but I wanted to get something working now.

Like Chats, each Message also has its own link, allowing me to deep-link to anything in my history. The first message above by me is reachable via https://chatcraft.org/c/W1F3Tgin1QE5UFb5MMtBy#DnrwATIZiCsQ_C6JMCX95 by clicking on the date Jun 14, 2023, 3:53 PM

I can also open the sidebar (via the hamburger menu), revealing previous and shared chats (more on that below), making it easy to go back to something I was working on before:

Sidebar to navigate Previous and Shared Chats

If I instead want to search for something, all of my old chats and messages are indexed. Here I'm searching for context, which returns a bunch of messages in various chats. Clicking on any of their links (i.e., dates) will take me to the chat itself:

Searching within old Chats and Messages

Every message supports various actions via a "dots" menu on the right. For example, here are examples of 1) a message written by me; followed by 2) a message by an LLM:

Actions for Human Messages
Actions for AI Messages

In both cases, I can Copy, Download, Duplicate, Edit, and Delete. Most of these are self-explanatory, but I'll call out Duplicate and Edit.

We had users asking to be able to "fork" a message, thereby taking a conversation in multiple directions at once. We let you do that at the level (i.e., create a new chat in the DB with its own URL, but copy all the messages) or from a particular message in a chat (i.e., use this message as my end point and go back vs. the whole thing).

Creating a blank or duplicate chat

This is really useful when you want to experiment with different paths through a conversation, trying alternate prompts without erasing your current work. Because each chat has its own URL, you can work in multiple tabs at the same time.

The Edit feature is also both obvious and amazing. I've wanted it for a long time. Using it, I can put myself in the driver seat for all interactions. Rather than passively reading and accepting an LLM's response, I can (and do!) edit it to reflect the way I want things to move. Having the ability to edit and delete text anywhere in a chat gives incredible freedom to experiment and explore. I'm no longer beholden to AI hallucinations, my own typos, or Markdown formatting issues. Also, I'm not locked into a past version of how things went in a chat. I can always rework anything to fit new directions. Remember that a chat is really a context for the messages that will follow, so it's helpful to be able to alter your current context to meet new expectations as the chat unfolds.

Extending this idea even further, another feature we've recently shipped is the ability to Retry an AI message with a different language model. Imagine you're 5 messages deep into a conversation with GPT-3.5 and you wonder what would GPT-4 might have said in response to the third message. Or maybe you'd like to mix GPT-3.5 with GPT-4 in the same conversation, or compare how they work. It's really simple to do:

Pick a Model to Retry a Response
New Model's response is streamed into current chat
Old versions are saved and easily switched between

Here I've taken a response from GPT-4 and retried it with ChatGPT. The old response is saved as a version along side the new one, and the new response is streamed into the current message. I can easily switch back and forth between the two, making it easy to compare (I'm amazed how little they differ most of the time). I can also edit one of the responses and create a third version:

Editing an LLM response, creating a new version

The flexibility to mix and match LLMs, edit and retry prompts, delete messages that don't make sense, duplicate some or all of a message and keep going in new directions–all of this has made writing about code with LLMs incredible productive for me.

Taras has also been really bullish about getting sharing to work. We've built an initial version on top of CloudFlare Pages and Functions, with GitHub OAuth for authorization and R2 as our object store. I've wanted to learn CloudFlare's serverless tools for a while in order to compare with what I know from AWS. I have been impressed so far, and filed issues on things that have been harder than they need to be.

Here's what sharing looks like:

Sharing a Chat

After authenticating with GitHub, I can manually or automatically summarize my chat and click Share Chat. This creates a public URL that I can share with friends (NOTE: As I write this I notice that we've broken something with loading shared chats, which I've filed to fix later. You can try this one, which still works: https://chatcraft.org/c/humphd/3HuOFWRlV-kY28C2tjuGk):

Creating a Public URL for a Chat

When I open a shared URL in a browser where I'm not logged in, I can read and duplicate the chat, whereas if I was logged in I could also edit:

Opening a Shared chat

I'm excited to extend and fix bugs in what we have so far. Taras has a PR up to add dynamic model support, which will make it easier to try out more models. We're also hoping to add models from vendors other than OpenAI. I'm looking forward to being able to have a more "social" experience by mixing different LLMs in the same chat, which is easy now with the features I just outlined. We're also interested to explore adding tools and functions to the mix.

I don't think I'm alone in my belief that writing, and tools that support good writing, are the keys to unlocking LLMs. This week I had a tweet about people reflex-accepting Copilot suggestions do close to a million views (lots of people seem to recognize what I was concerned with). I think AI has tremendous power to support software developers, but as an educator that spends so much time with the next generation trying to learn our craft, I'm not convinced that we're getting this right in all cases.

With ChatCraft, I'm hoping to do the opposite. I want to read, write, edit, review, and compare text with LLMs. I'm not interested in having text dropped into my editor, word processor, or other tools as-is. I'm looking for, and trying to build, tools for thought.

I suspect that ChatCraft will look different again in another few months. Maybe that will be because you've gotten involved and helped us do something cool. In the meantime, I'm having fun in my spare time exploring what I think AI and coding can do.

]]>
<![CDATA[Thinking about Context]]>I've written recently about my work on ChatCraft.org. I've been doing a bunch of refactoring and new feature work, and things are in a pretty good state. It mostly works the way I'd expect now. Taras and Steven have filed a bunch of good ideas related to sharing, saving,

]]>
https://blog.humphd.org/thinking-about-context/646262194a8ffa03eff4c9eeMon, 15 May 2023 19:17:49 GMTI've written recently about my work on ChatCraft.org. I've been doing a bunch of refactoring and new feature work, and things are in a pretty good state. It mostly works the way I'd expect now. Taras and Steven have filed a bunch of good ideas related to sharing, saving, and forking chats, and I've been exploring using SQLite Wasm for offline storage. But over the weekend I was thinking about something else. Not a feature exactly, but a way of thinking about the linear flow of a chat. The more I've worked with ChatCraft, the more I've learned about this form of dialog. Because a number of separate features flow from this, I thought I'd start by sketching things out in my blog instead of git.

A Chat

The (current) unit of AI interaction is the chat. A chat, in contrast to a conversation, dialog, debate, or any of the other ways one might describe "talking," is a kind of informal talk between friends. The word choice also gives a nod to the modern, technical meaning of "chat" as found in "chat app" or "chat online." When we "chat," with do so without formality, often in short bursts.

What informal chats depend upon is an existing, shared (i.e., external) context between the speakers. I want to chat with you about details for some event we're planning, or to clarify something you said on the phone, or to quickly ask for help. I can duck in and out of the conversation without ceremony, because this "chat" does not represent anything serious or lasting. That is, the relationship of the participants is independent of this interaction--we're just chatting.

Something similar is at work when I'm talking to an AI. Most of what I'm saying is not present in the chat. Maybe I want to know the specific syntax for performing an operation in a programming language. I'm not interested in learning the language, talking about why the syntax evolved the way that it did, debating other approaches, etc. I might write only 2 or 3 sentences, but everything I don't write is also necessary for the interaction to work.

Just as with a friend, I have to signal to an AI the type of thing I'm after. Lots has been written about prompting an AI, but increasingly I'm becoming aware of the need to evolve that prompt over a series of messages, to refine the idea (both my own and the AI's), and work toward an understanding. It's less about coming up with the right magical incantation to conjure an idea into existence, and more like a conversation over coffee with a colleague. So there's always going to be an enormous, shared context that we generally won't discuss; but in addition to this we necessarily need to build a smaller, more immediate context within the discussion itself.

My interest in AI doesn't include "training LLMs from scratch," which is to say, I'm not concerned with the larger, shared context. It's fundamentally important, but beyond me. However, I am fascinated by this more intimate, smaller context that develops within the conversation itself.

Context in ChatCraft

A chat in ChatCraft looks like this:

Typical ChatCraft chat

To begin, we've got the usual back-and-forth you'd expect. Beneath the UI, we actually have the following:

  1. A system prompt, helping to define the way our assistant will behave.
  2. AI message, the first one we seed
  3. User message
  4. AI message,
  5. Repeat...

Lots of apps need to hide their system prompt (it's hard to do well!), but ours is easy to find, since ChatCraft is open source:

You are ChatCraft.org, a web-based, expert programming AI.
You help programmers learn, experiment, and be more creative with code.
Respond in GitHub flavored Markdown. Format ALL lines of code to 80
characters or fewer. Use Mermaid diagrams when discussing visual topics.

When the user enables "Just some me the code" mode, we amend it with this:

However, when responding with code, ONLY return the code and NOTHING else (i.e., don't explain ANYTHING).

By the time you read this, it will probably have changed again, but this is what it was when I wrote this post.

The AI/Human Message pairs are kind of an obvious construct, but after using this paradigm for a while, new things are occurring to me.

There is Only One Author

When I'm chatting with a friend, there are two (or more) people involved. An effective and emotionally safe interaction will involve all parties getting a chance to speak and be heard. Furthermore, it's important that neither party manipulate or intentionally misrepresent what the other is saying.

These ideas are so obvious that I almost don't need to mention them; and it makes sense that they would find their way into how we model interactions with an AI as well.

In terms of manipulation, much has been said about AI hallucinations and how you have to be careful not to swallow whole any text that an AI provides. This is true. But I haven't read as much about people tinkering in the other direction.

When I first started working on ChatCraft, Taras had already added a very important feature: being able to remove a message from the current chat. If the AI gets off on some tangent that I don't want, I can delete a response and try again. It doesn't even have to be the last message I delete.

This seemingly simple idea has some profound implications. By adding the ability to remove a message from anywhere in the current context, we establish the fact that only one party is involved: I am at once the author, editor, and reader. There is no one else in the chat.

This realization becomes a foundation for building other interesting things. Let me give you a simple example. ChatCraft takes advantage of GPT's ability to create Mermaid diagrams in Markdown, and lets us render visual graphics:

Example Mermaid Diagram in ChatCraft

It can create some really complex diagrams, which makes understanding difficult relationships much easier. But it also makes silly mistakes:

Syntax Errors in Pie Chart Diagrams

In these examples, our inline renderer has blown up trying to render a diagrams with syntax errors (i.e., the numeric values shouldn't include %, per the docs). For a while, I was embracing the typical notion of what a "chat" should be and pointing out the error. "My apologies, you're right..." would come the reply, and the error gets fixed.

But when I'm the only author in the chat, I should be able to manipulate and edit any response, be it mine or the AI's. Fixing those graphs would be as simple as adding an EDIT button I can click to fix anything in an AI's response, thus unlocking my follow-up messages:

Fixed Pie Diagram

Mixing Multiple AI Models

Another idea that becomes possible is swapping out the AI for one or more of the messages in the chat. This would be unthinkable in a real chat, or even when chatting to an AI using a commercial product (why would anyone let me bring their competitor's AI into this interaction?). But in an open source app, I should be able to move effortlessly between chatting with ChatGPT, GPT-4, Claude, Bard, etc. I owe no fielty to an API provider. I should be able to pull and mix responses from various AI backends, leveraging different AI models where appropriate for the current circumstance.

Speaking of different models and context, I've also been thinking about context windows. While chatting with ChatGPT in ChatCraft, I often hit the 4K token limit (it's 8K with GPT-4). Rarely am I asking a question and getting an answer. More often than not, it's a slow evolution of an idea or piece of code. Up til now, that's meant I have to start manually pruning messages out of the chat to continue on. But I've realized that I could implement a sliding context window, which would allow me to chat indefinitely with a ~4K context window that includes the most recent messages.

Working with Data

Part of what takes me over the 4K/8K limit is including blocks of code. I usually write to ChatCraft the way I'd discuss something in GitHub: Markdown with lots of code blocks. I even find myself copy/pasting 3 or four whole files into a single message. It's made me realize that I need to be able to "upload" or "attach" files directly into the chat. I want to talk about a piece of code, so let me drag it into the chat and have it get included as part of the context. If I want to deal with it as piece of text, I can still copy/paste it into my message; but if all I want is for it to ride along with the rest of what I'm discussing, I should be able to add it easily.

The same is true for other kinds of data. ChatCraft can already render HTML, to build things like charts:

Rendering a Chart.js line chart in ChatCraft

Maybe I want to draw a graph using a bunch of CSV data. What if I could drag or attach that data into the chat just like I mentioned above with code? "I need a line graph of this CSV data..."

Conclusion

I want to build a bunch of this, but thought I'd start by writing about it. Using ChatCraft to build ChatCraft has evolved my understanding of what I want in an AI, and it's fun to be able to prototype and explore your own ideas without having to wait on features (or even access!) from big AI providers.

]]>
<![CDATA[ChatCraft.org]]>I've been continuing my experiments with AI development. I wrote previously about my attempts to use ChatGPT more intentionally, as a way to better understand how my students are encountering it. Since then, I've been focusing on contributing to https://chatcraft.org/ and wanted to talk about what it is

]]>
https://blog.humphd.org/chatcraft-org/6459376e4a8ffa03eff4c8c9Tue, 09 May 2023 11:40:00 GMTI've been continuing my experiments with AI development. I wrote previously about my attempts to use ChatGPT more intentionally, as a way to better understand how my students are encountering it. Since then, I've been focusing on contributing to https://chatcraft.org/ and wanted to talk about what it is and what it's been like to build it.

My recent AI posts prompted an old friend from my Mozilla days (Taras Glek) to reach out on Twitter. He wanted to talk about our shared interest in AI and programming. Both of us learned to program long before AI, but we also see the tremendous potential of using AI to accelerate our work going forward. We've also been finding that many of our colleagues and peers aren't as interested as we are, and having someone else to talk to and work with on this stuff has been important.

Taras wanted to show me an experiment he'd been building to create his own open source, programming-focused, ChatGPT web client. It already had a lot of cool features like being able to render Mermaid diagrams and HTML in responses from the ChatGPT API. He'd also hooked up langchainjs, which is a project I've been following with interest.

Seeing a pure browser-based web app (no server-side code) really inspired me. For some reason, all of my AI work thus far has been done using two-tier web apps or with node.js all on the server. I don't know why it never occurred to me to do all of this in the browser. Seeing what Taras was doing, it all suddenly clicked for me: I really want my AI to be in the browser.

Before this, I was using various AI Assistants in VSCode to see what that's like. I've tried Cody from Sourcegraph, Amazon CodeWhisperer, and a few more. So far this process has convinced me that what I really want is the ability to reason with, and explore ideas in code with an AI vs. having it dump suggestions in my editor. I love having this be a browser tab vs. an editor extension.

Like me, Taras had started using GPT via the OpenAI Playground. We both loved it. You could try things in a web page, use it or delete it, and keep trying again until you were happy. It was so easy to experiment. The ephemeral nature of the output (nothing being saved, not integrated with anything you're working on) encouraged playfulness and exploration. Then OpenAI brought out ChatGPT. I don't need to tell you what it is. Again, the "it's just a website" phenomenon really struck me.

Rather than give up on his own UI and using ChatGPT, Taras kept going with what he was building. He wanted to know if I'd help him with the UI. It felt like old times, when I was building DXR (a web UI) on top of his Dehydra gcc plugins.

Since then I've been working with Taras to rebuild the UI for what's become chatcraft.org. It's now gotten to the point that I only use it vs ChatGPT or VSCode assistants. I like how much freedom it gives: paste in your OpenAI API key and you're ready to go. No logins, annoying rate limits, and the UI and responses are tailored to what a programmer wants vs. being a general purpose chatbot.

I've also loved being able to build it the way that makes sense to us. I don't have to wait on OpenAI or some other company to give me what I want--I can build it myself. The cheap and ready access we have to the underlying models, and the flexibility of the web as a UI and rendering platform is amazing.

Another unexpected benefit of having an AI-based project is that it's helped me get over the hump of using AI to program. I don't naturally think to use AI when programming: I've never had access to one in the past, and old habits die hard. However, writing an AI app has made it obvious that I should be using AI to build it. I've had all kinds of help from ChatGPT and GPT-4 while writing the code.

When I'd get stumped on something, I paste in the code and start talking about my bugs. Because I work in Markdown, it's very similar to writing issues on GitHub. Often I get what I need back: a push in the right direction and sometimes complete code as well. I've also been amazed at how it has been able to replace automate tests. For example, the other day I was working on a bug in the syntax highlighting code, and I worked with ChatCraft on ChatCraft. I'd ask it for examples of code blocks, fix the code, repeat, ask about bugs I was seeing, fix things, repeat. Using the app as an AI-REPL is extremely productive and unlike any programming I've done before. It's like assembling a robot with the robot's help.

I'm excited to try using it for some other AI experiments over the next few months. I have a few other collaborations I'm wanting to do with friends who are interested in AI, and I'm going to suggest we use ChatCraft to do the work.

ChatCraft.org is still pretty young, but I love it and wanted to share. If you'd like to give it a try and contribute, please do. Let Taras and I know what you think.

UPDATE: Taras has also written his own post about ChatCraft.

]]>
<![CDATA[Starchart 1.0.0: My.Custom.Domain]]>Over the Winter 2023 semester, my open source students and I have been working together to build a new project. This past week we were able to make our 1.0.0 release and get it into production. I wanted to write about what we made and how we made

]]>
https://blog.humphd.org/starchart-1-0-mycustomdomain/6447da484a8ffa03eff4c73aTue, 25 Apr 2023 15:07:57 GMTOver the Winter 2023 semester, my open source students and I have been working together to build a new project. This past week we were able to make our 1.0.0 release and get it into production. I wanted to write about what we made and how we made it.

My.Custom.Domain is a web app that allows Seneca faculty and students to create custom domains and obtain SSL certificates without spending any money or having to give any third-party personal information. The project itself we call Starchart. We built the app in partnership with Seneca ITS, who chose the My.Custom.Domain name, and have provided all our infrastructure and production support.

A Tour of My.Custom.Domain

I'll start with a quick tour of what the app looks like.

Users begin by authenticating using their Seneca SSO accounts:

Initial login screen

After authenticating with Seneca's Active Directory via SAML2, users are taken to the landing page (I'm logged in as a regular user below in development, because my UI looks different as an admin in production):

Landing page for managing DNS Records and Certificate

Users can create and manage DNS Records (A, AAAA, TXT, or CNAME) using a {name}.{user}.mystudentproject.ca domain:

Managing DNS Records
Creating a new DNS Record

Users can request a new SSL Certificate, which is created for all of their subdomains (i.e., *.{user}.mystudentproject.ca). The process takes some time, so we send a notification email when it's done:

Certificate Request being Processed

Once the certificate is issued, we provide instructions and various ways to download, view, or copy it. Users can also renew the certificate when it approaches its expiry date:

Working with a Certificate

Finally, admin users can "impersonate" regular users to help fix problems. In the screenshot below, I'm logged in as "user3" (an admin user) but my effective username is "user1" (a regular user):

Admins can Impersonate Regular Users to Help Solve Problems

We also provide information and instructional pages about DNS Records and SSL Certificates to users who are new to the concept (most of our users will be):

Informational Pages to Help Users

Building the App

The app was built by a passionate team of developers:

Name GitHub Blog
Stefan Frunza sfrunza13 https://dev.to/sfrunza13
Mario Leonardo Ririio https://dev.to/ririio
Denes Adam Dolhay dadolhay https://dev.to/dadolhay
Wonkeun No Genne23v https://dev.to/Genne23v
Chen-Yuan Chu cychu42 https://dev.to/cychu42
Taimoor Dawami SerpentBytes https://dev.to/tdaw
Eakampreet Singh Eakam1007 https://dev.to/eakam
Tymur Levtsun myrfion https://dev.to/myrfion

We also had fantastic contributions from various community members:

We also worked closely with Seneca ITS, represented by Mehrdad Ziaei. Mehrdad was fantastic to work with, and helped me solve many infrastructure and deployment issues, providing us with:

  • Load Balancing, Domains, and Clusters for Staging and Production deployments, including shell and sudo access
  • SAML2 Active Directory Access
  • MySQL Staging and Production databases
  • AWS IAM Users and Route53 Hosted Zones for Staging and Production
  • Root domains (i.e., mystudentproject.ca)
  • Email Accounts for notifications on Staging and Production

I think it's important to call out how successful this partnership was, since my colleagues and I often like to complain about how difficult ITS is to work with when trying to build our custom infrastructure. While it's true that we often don't understand each other, when we actually do collaborate on a project together, it's amazing how fast it can get done.

I also relied on Chris Tyler and his Linux knowledge, to support me in properly configuring various things in production. Thank you, Chris!

Technologies

Doing these term-long open source projects with my students, I often try to experiment with new technologies. It's a great way for me to get caught-up on new ways of working in our industry.

This term was no different, and we worked with a bunch of things that were new to me, including:

  • Remix.run for our front-end and back-end. I really like Remix. It's a great fit for how my brain thinks about web development, with the flattening of front-end and back-end code, emphasis on web platform features over JS when possible, tight integration of all the various pieces, etc. The only thing I didn't love was the way the development hot-reloader made some server-side patterns hard or impossible. In the future, I'd probably break some pieces out into separate containers vs. trying to do it all as a Remix app.
  • TypeScript, which I've used lots before, but never with students. This went quite smoothly. Our students take many C/C++ courses in parallel to learning JS, so TypeScript seems to fit nicely into their understanding of how a programming language should work. The added benefits in reviews and tests, where TypeScript helped us solve problems before we merged them was amazing. I'll add more TypeScript to future courses.
  • AWS and the AWS SDK for Route53 with Moto for mocking Route53 locally
  • Let's Encrypt for SSL Certificates, and their Pebble containers for testing
  • Nodemailer for email notifications and MailHog for mocking and testing
  • Chakra-UI for front-end components and styles. I think Chakra-UI is the best React component framework I've ever used. I'm using it on other projects now too, and it's so productive.
  • Prisma for our ORM. Prisma was great. I've suggested we add it to our node.js course, where students learn about ORMs
  • Redis and BullMQ for background job processing. I love Redis, and I still find BullMQ needlessly complicated in places. But we made it all work.
  • Samlify for our SAML SSO code and SimpleSAMLphp for mocking and testing
  • Playwright for E2E testing. Playwright gets better and better with every release. I can't say enough positive things about it.
  • Vitest for Unit Testing. I've been bumping into Vite and Vitest more and more lately, and it's been pretty smooth.
  • Docker, docker compose, and docker swarm for development, testing, and deployment. I've used and taught Docker for many years, but this was my first experience using Docker swarm. It's so good. I don't understand why more people don't use it for smaller apps like this. The way it manages secrets, does rolling updates, handles load balancing, etc. is all amazing.
  • GitHub Actions for CI/CD. We ran into the usual small bugs, but it's still amazing to use, and did everything we wanted.
  • Lots of other things that I won't bother to mention, but you can see in our repo

I'm really happy with the tech stack, and would use just about all of this again in future projects.

Becoming a User and Maintainer

I'm excited to start using My.Custom.Domain with my students. In future versions of my AWS class, I want to have the students use it to work with HTTPS based APIs, which is something you have to pay for in AWS (not supported in the Learner Lab).

I'm also keen to get new students involved in maintaining this code. I think that one of the benefits of projects like this is that it allows students to jump into different parts of the software lifecycle. Building something takes one kind of skill, but maintaining is another.

If you're a Seneca prof or student, and want to try using this, please let me know. Also, if you're interested in getting involved in the project, we still have a bunch of issues to fix.

It feels good to get to this point in the term and have a project ship with so little fuss. Not every software project is the right scope for a group of students to do in a semester. It's fun when you can find one that fits like this.

Now go and make some domains and certificates!

]]>
<![CDATA[Unnecessarily Hard]]>Denes and I have been trying to figure out a bug with our Starchart DNS code. After hooking up the Let's Encrypt Certificate flow, we were finally in a position to test working with _acme-challenge TXT records in Route53.

Locally and in CI, we've been using the amazing moto route53

]]>
https://blog.humphd.org/unnecessarily-hard/6422f5964a8ffa03eff4c631Tue, 28 Mar 2023 14:56:59 GMTDenes and I have been trying to figure out a bug with our Starchart DNS code. After hooking up the Let's Encrypt Certificate flow, we were finally in a position to test working with _acme-challenge TXT records in Route53.

Locally and in CI, we've been using the amazing moto route53 server to test things. It's given us the ability to develop the majority of the code quickly, but production is obviously different than mocking. Route53, and DNS in general, is hard to simulate. There's just so much that can go wrong, so many interconnected pieces, timing issues, etc.

Over the weekend we got into a loop of finding a bug and fixing a bug, finding another, fixing another. Eventually this has to work, right? How many can there be...

The process of finding, debugging, and fixing these bugs with AWS is unnecessarily hard. I really like AWS and I even teach an upper-semester course on it. But I feel like AWS makes things harder than they need to be for no apparent benefit.

Let me show you what I mean with one of the bugs. Let's Encrypt needs us to set these _acme-challenge TXT records in Route53. I need to put the string value 8cV4hs2A8VmH3a2f2QYkvANYtXZWm9I93kUXYZtiGgE into _acme-challenge.whatever.com. as a TXT record. To do this in node.js, you need to use the AWS SDK and the ChangeResourceRecordSetsCommand. It lets you specify an array of changes you want to apply: CREATE, DELETE, UPSERT. For example:

// The following example creates a resource record set that routes Internet traffic to a resource with an IP address of 192.0.2.44.
const input = {
  "ChangeBatch": {
    "Changes": [
      {
        "Action": "CREATE",
        "ResourceRecordSet": {
          "Name": "example.com",
          "ResourceRecords": [
            {
              "Value": "192.0.2.44"
            }
          ],
          "TTL": 60,
          "Type": "A"
        }
      }
    ],
    "Comment": "Web server for example.com"
  },
  "HostedZoneId": "Z3M3LMPEXAMPLE"
};
const command = new ChangeResourceRecordSetsCommand(input);
const response = await client.send(command);

This is creating an A record with a single IP address Value. We need to use a TYPE of TXT, but there is no example or mention of this. Surely that means it's the same, right? The API docs seem to imply this, noting of Value:

ResourceRecords: [
  {
    Value: "STRING_VALUE", // required
  },
],

"So, ResourceRecords is an Array of Objects with a string Value, go it." No other details or examples of working with TXT records.

Off we go, and despite everything we throw at it, back comes a 400 error with InvalidChangeBatch. Let's check the docs on this error:

Throws: InvalidChangeBatch (client fault)

This exception contains a list of messages that might contain one or more error messages. Each error message indicates one error in the change batch.

"Might contain," eh? I'll skip ahead and tell you that it does not contain anything useful:

"Code":"InvalidChangeBatch", "Type":"Sender", "name":"InvalidChangeBatch"

Alright, so we're doing something wrong, but what? We try half-a-dozen things, and fix some other bugs, but nothing will unlock this InvalidChangeBatch problem. Now we're neck deep in AWS browser tabs, with articles about every possible way to deal with Route53 other than what we need to do.

Eventually, deep in a Stack Overflow comment (not the answer!) I see someone toss out a lifeline: "FYI, I needed to wrap my value in \"...\" in case that helps anyone." That not only helps me, but it's what should have been written in the official API docs to begin with! Why is this so hard to find?

Armed with this new crumb of information, I again go searching in the AWS documentation labyrinth, and discover this:

A TXT record contains one or more strings that are enclosed in double quotation marks (").

Furthermore, there's all kinds of other special cases you should deal with, including:

A single string can include up to 255 characters, including the following:
  - a-z
  - A-Z
  - 0-9
  - Space
  - (hyphen)
  - ! " # $ % & ' ( ) * + , - / : ; < = > ? @ [ \ ] ^ _ ` { | } ~ .

If you need to enter a value longer than 255 characters, break the value into strings of 255 characters or fewer, and enclose each string in double quotation marks ("). In the console, list all the strings on the same line:

"String 1" "String 2" "String 3"

For the API, include all the strings in the same Value element:

<Value>"String 1" "String 2" "String 3"</Value>

The maximum length of a value in a TXT record is 4,000 characters.

So, in other words, a bit more than Value: "STRING_VALUE", // required. There's no mention of any of this in the API "docs."

As we're looking through this list of DOs and DON'Ts for a TXT record value, I ask myself: "surely someone has written this code already, I wonder where we can get it?" Denes jokingly says, "you mean like having it in the AWS SDK?" I laugh. of course it isn't there. Why would this be part of the SDK? But seriously, AWS knows that I'm setting a TXT record value Type, and when I give them a Value, why not simply do the right thing? Or at the very least, expose a function that formats a TXT record according to their own specifications! Or give me an error message that says "invalid TXT value, USE QUOTES!"

It doesn't need to be this hard. These docs could link to each other, so you can find the details instead of relying on the kindness of strangers on the internet. The errors could actually tell you what's wrong, even link to URLs with the info you need. The API could expose methods to help you get your job done and show you examples for edge cases (are TXT records really an edge case?).

If AWS was some fledgling startup who just shipped a beta and hadn't had time to get to the documentation, or if Route53 was some new service that hadn't been battle-tested yet, I'd be more sympathetic. But that's not what this is. This is unnecessarily hard.

]]>
<![CDATA[A Week with ChatGPT]]>ChatGPT continues to rule the news cycle. It's also increasingly finding its way into my work on our department's Academic Integrity Committee, where we've seen it pop up more and more in reported cases: students using it on tests, both paper-based (cell phone pic of a question uploaded to a

]]>
https://blog.humphd.org/adventures-with-chatgpt/642069764a8ffa03eff4c34bSun, 26 Mar 2023 18:50:12 GMTChatGPT continues to rule the news cycle. It's also increasingly finding its way into my work on our department's Academic Integrity Committee, where we've seen it pop up more and more in reported cases: students using it on tests, both paper-based (cell phone pic of a question uploaded to a WhatsApp group, where a friend types it into ChatGPT) and electronic, as well as in assignments. Much of the academic dialog online and at our institution has been focused on ChatGPT as a vector for cheating.

Meanwhile, a decade's worth of innovation has happened in the past few weeks as an arms-race plays out for control of AI in big tech. I was trying to keep track of everything I saw being released, but it's impossible. The rate of change is beyond anything I've ever experienced. The idea that an instructor or institution can write a policy that gets on top of this is laughable.

Despite my concerns over academic integrity, I'm more interested in understanding how to properly use AI in teaching, learning, and development. As a result, I've been trying to take inspiration from people like Simon Willison, who is using ChatGPT and Copilot to learn new technologies (e.g. Rust, AppleScript) and taking notes as he goes.

So this past week I challenged myself to try and use ChatGPT in order to better understand how my students are encountering it, and what lessons I could learn in order to teach them how to make better use of the responses they are getting.

I started the challenge by building ChatGPT in CSS for my second-semester web programming class. We were studying CSS Layout, and the ChatGPT site is a fairly simple one to create, using the topics for the week:

ChatCSS - recreating ChatGPT in CSS for my Web Programming class

Writing the code, I spent a long time using their interface. I was struck by the Limitations column, which I doubt most people have read:

  • May occasionally generate incorrect information
  • May occasionally produce harmful instructions or biased content

These are incredible warnings to see displayed on a web site. "This site might harm you." Wow! We could argue that this warning should be placed on lots of the web, but to see it called out like this is fascinating and not something I encounter very often. I thought about that a lot this week.

I went to class on Monday, plugged my laptop in, and started with this ChatCSS tab open. "Does anyone recognize this site?" Silence! Eyes down. Nervous laughter. It was like I had just begun a lecture about sex. It's clear that ChatGPT has quickly earned taboo status in academic discourse between professors and students. That's too bad, because I believe it needs to get discussed openly.

Spending two lectures this week building the ChatCSS UI and talking about how it works allowed me to start to engage with the students, hear their ideas, discuss their questions. They are absolutely using it, but mostly having to hide that fact. They need help navigating this new world. From this experience I realized that I need to talk about it more, not less.

Next, I did a bunch of reviews for my open source students working on the Starchart DNS/SSL project. A number of times as I read their code, I asked questions about what I was seeing, and the answer came back, "ChatGPT recommended this." I've never encountered this before. ChatGPT was a third entity in our conversations, always there but also completely absent. For example, it wanted to use a 403 HTTP status code to indicate that a resource being created asynchronously isn't ready yet (I'd use a 409). Or writing regular expressions to cover validation cases on IPv4, IPv6, and domain names. I wanted to know about edge cases that were missed. Having ChatGPT's output, but not ChatGPT itself, made the process a bit painful. The code raised questions that couldn't be answered, which felt unsatisfying. I learned that detaching text from a ChatGPT response, but still insisting that "ChatGPT wrote it" won't work. Once you pull that text out of the ChatGPT window, you have to own it, answer for it, clean up after it.

I had a number of times this week were I struggled to name something in code, and I tried using ChatGPT to help me explore possible options. For example, we have been dealing with overlaps between name vs. subdomain vs. fqdn in our Starchart code. I didn't end up using a lot of what it suggested, but I think the exercise of forcing myself to get it out of my head, and into written form, helped me come to a decision faster. In this way, using ChatGPT was a forcing function. Much as writing an email to a colleague, or discussing with peers on Slack, writing my problem out kickstarted the problem-solving process.

Later in the week I used ChatGPT to solve a real problem I had. I was struggling to write some GitHub Actions code for a Docker build and webhook deployment flow. GitHub Actions are annoying to test, because you have to do it for real vs. running things locally. I couldn't figure out or remember how to do the following:

  • Interpolate environment variables into JSON strings
  • Get a short git sha (I only wanted the first part of the string)

I found ChatGPT was eventually able to get me where I needed to be, but it took half-a-dozen questions/responses to get there. For example, it told me to use the substring function:

However, there is no such function (GitHub, please add it!), so I had to press it:

It did help, and I was able to write my workflow YAML file and get automatic deploys to work.

Next, I struggled to make sense of the BullMQ API for dealing with errors in flow jobs. We have a series of asynchronous worker jobs that run in the background, and we need to chain them. When one of them fails, we need to execute an error handler, and the way we were doing it was not working.

I was pleased to discover that simply mentioning the name of the library, BullMQ, was enough and it knew what I was talking about. However, once again, I had to be patient to arrive at my desired destination. I went back and forth with it many times before I could get it to stop hallucinating imaginary APIs and had to explicitly call out its inconsistency:

Not only is ChatGPT going to give you incorrect info, but it also has no problem contradicting itself in a single response. This is something I'm not used to. If a person did this to me, I'd get annoyed; in fact, I was annoyed in the exchange above. Humans won't tolerate being lied to or manipulated like this. This erosion of trust breaks down communication: "How can I trust you if you just lied to me?" The truth is, you can't trust ChatGPT. It neither lies nor tells the truth, flitting effortless between the two, giving what Neil Gaiman called "information-shaped sentences" on Twitter this week.

You have to approach ChatGPT with zero-trust and plan to verify everything. Knowing this, I can route around incorrect information, and try to aim deeper at the truth I really need. This tells me that using responses in areas I know nothing about is going to leave me vulnerable to misinformation. My students, who don't know enough yet to push back at the AI, will struggle when it gives reasonable but false code.

Later in the week, I asked it to rewrite a TypeScript file I was reviewing. I wanted to see what other styles it could imagine for the code, which was somewhat complex to read. It did a nice job of renaming things, switching loop types to make it more readable, using early returns (amen!), etc. It also broke the logic of the code. When I shared it with the original author, he wasn't impressed. Where I found the style vs. the substance of the code interesting, he was 100% focused on the substance, which had problems. This taught me that I need to be clear about my expectations for the use of the text I share from ChatGPT: am I looking for truth or form or both? What does my audience need and expect?

Next, I had some free time and wanted to figure out how to get Microsoft's Megadetector model running with node.js and TensorFlow. I'm really interested in Camera Traps and biodiversity tracking using technology. I spent a long time going back and forth with ChatGPT on this one, before eventually giving up. First, it gave me code to use it remotely via Azure vs. locally (not a bad idea, actually). Once I told it that I wanted to run the model locally, it got me 80% of the way there. However, it gave me invalid model URLs, combined Python/pip and node.js/npm dependencies, and made-up an npm module for the image processing. Once again, I had to be willing to sort through the response and push back in order to have it correct its mistakes.

This can feel uncomfortable, too, since a human at the other end of my needy and critical responses would quickly tire of our conversation. ChatGPT doesn't, and will happily try again. Eventually it became clear that the 2021 model cut-off date wasn't going to work with the current state of the Megadetector repo, and I ran out of time. However, if I'd been willing to keep going a bit longer, I could have got something working.

Next, I decided to port some Python code I'd read online to Zig. I don't know why I've been interested in Zig lately, but I keep coming back to it (I think where Rust feels too much for me, Zig somehow feels just right). I was pleasantly surprised to see how easily this process went. Once again, it hallucinated a mix of pip modules and zig standard library code; however, by now I was used to sorting out the 20% of the response that would need to be thrown away. I liken the process to working with a senior dev who only has 5 minutes to answer your question: the response they give is "right" but includes a mix of real and pseudo-code that you have to expand and debug yourself. Knowing how to set my expectations (that I'll almost never be able to use code it returns as-is, that responses won't be idiomatic to the ecosystem I'm targeting and will often mix ideas from different languages) has made the process less frustrating for me.

Next I tried something completely different and more risky. Having read this thread about a guy who used ChatGPT to help save his dog's life, I tried doing some medical research. Given everything I've said above about needing to be able to verify all responses, not being able to trust at least 20% of what comes back, etc. you might think this is insane. Again, expectation setting is key here. I can't use what it gives back as-is, but by now I don't plan to ever do that. Here, I'm interested in having it guide and inform my research.

I asked about a condition I wanted to know about, starting by asking what the usual way of treating it would be (i.e., without me saying anything). I wanted to see if it could give me information I already know to be true, before going into areas I'm less sure about. It was able to confirm current treatment options, which I know are correct. I then asked for the state of the research into newer methods: what's emerging? It gave me 3 amazing jumping off points, which led me on a fruitful literature review and opened my eyes to some interesting possibilities I hadn't heard about. Here I needed to pair ChatGPT with research into the scientific literature. Unlike code, where I can lean on a mix of my own understanding and tools to verify what I'm being told, I have no ability to assess the value of medical information. One needs to be ready to jump out of ChatGPT and into proper research, but doing so can be a really valuable exercise. I know from experience that I could never get Google to give me this info (I've tried)--I don't know how to put it into a searchable question format. However, after working with ChatGPT, Google once again becomes useful, since I have keywords I can use for search.

Finally, I used ChatGPT to help me quickly do a task that would have been time-consuming to code or do manually. I had a bunch of CSS, and I wanted a sorted list of all the unique CSS properties I'd used. I pasted the entire file into ChatGPT and told it to give me exactly that, which it did. This was probably the most satisfying use I'd had all week. It was something I knew how to do manually, and could also write a program to do, but I wanted it done fast without doing either of those. It "just worked" and got it 100% correct. It made me realize that this kind of language/text programming problem is something I should be feeding to ChatGPT more often. I do a lot of it while teaching, coding, and in my daily life.

This week I found that I had to force myself to use ChatGPT, it wasn't my default. I found the realities of using it both more and less magical than expected. As predicted, I can't trust it. However, if I enter the conversation somewhat adversarially, I can often extract something useful. I'm not talking to AI. We're not chatting. It's much more combative, defensive, and frustrating than talking to a person. I have to verify everything I'm given, which means I have to be in the mood to do that. I can't be tired and let the response wash over me. But I also recognize the value of what I was able to do with it this week. I solved real problems, from technical to academic to personal. It's interesting and I need to try it more.

]]>