LLM Incantations and Spellbooks

LLM Incantations and Spellbooks

How to manage your prompt library

Published on Wednesday, February 5, 2025

The Problem

We live in interesting times where software development is going through a paradigm shift - Human developers are no longer the only ones writing code!

To summon a code from the magical Nvidia box in the astral cloud, we need proper incantations:

Write me a person class.

When I pray to Claude/Sonnet, I get a Python class:

class Person:
    def __init__(self, first_name, last_name, age, email=None):
        self.first_name = first_name
        self.last_name = last_name
        self.age = age
        self.email = email
...

This is a much better incantation:

Write me a person class with Name, Surname, DateOfBirth. Add methods to change Surname.

Use C#. Use Factory Method. Methods should return new object if successful Result with CSharpFunctionalExtensions.

and this returns:

using System;
using CSharpFunctionalExtensions;

namespace Domain
{
    public class Person
    {
        public string Name { get; }
        public string Surname { get; }
        public DateTime DateOfBirth { get; }

        private Person(string name, string surname, DateTime dateOfBirth)
        {
            Name = name;
            Surname = surname;
            DateOfBirth = dateOfBirth;
        }

        public static Result<Person> Create(string name, string surname, DateTime dateOfBirth)
        {
            if (string.IsNullOrWhiteSpace(name))
                return Result.Failure<Person>("Name cannot be empty");

            if (string.IsNullOrWhiteSpace(surname))
                return Result.Failure<Person>("Surname cannot be empty");

            if (dateOfBirth > DateTime.UtcNow)
                return Result.Failure<Person>("Date of birth cannot be in the future");

            if (dateOfBirth.Year < 1900)
                return Result.Failure<Person>("Date of birth cannot be before 1900");

            return Result.Success(new Person(name, surname, dateOfBirth));
        }

        public Result<Person> ChangeSurname(string newSurname)
        {
            if (string.IsNullOrWhiteSpace(newSurname))
                return Result.Failure<Person>("New surname cannot be empty");

            if (newSurname == Surname)
                return Result.Failure<Person>("New surname must be different from current surname");

            return Result.Success(new Person(Name, newSurname, DateOfBirth));
        }

        public override string ToString()
        {
            return $"{Name} {Surname} (Born: {DateOfBirth:d})";
        }
    }
}

After many tweaks, I have a decent spell:

Write me a person class with Name, Surname, DateOfBirth. Add methods to change Surname.


Use C#. Use file block-scoped namespaces. Use Factory Method. Methods should return new object if successful Result with CSharpFunctionalExtensions. For every method as input use class with FluentValidation, not primitive types. 
Method input class should be subclass of the main class. Everything should be sealed. Do not use inheritance for requests, classes should be standalone.
Do not add comments in code.

It's not perfect. I could argue about validation, but it's a decent start.


namespace Domain;

using FluentValidation;
using CSharpFunctionalExtensions;

public sealed class Person
{
    public string Name { get; private set; }
    public string Surname { get; private set; }
    public DateTime DateOfBirth { get; private set; }

    private Person(string name, string surname, DateTime dateOfBirth)
    {
        Name = name;
        Surname = surname;
        DateOfBirth = dateOfBirth;
    }

    public sealed class CreatePersonRequest
    {
        public string Name { get; set; }
        public string Surname { get; set; }
        public DateTime DateOfBirth { get; set; }
    }

    public sealed class CreatePersonRequestValidator : AbstractValidator<CreatePersonRequest>
    {
        public CreatePersonRequestValidator()
        {
            RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
            RuleFor(x => x.Surname).NotEmpty().MaximumLength(100);
            RuleFor(x => x.DateOfBirth).NotEmpty().LessThan(DateTime.Now);
        }
    }

    public static Result<Person> Create(CreatePersonRequest request)
    {
        var validator = new CreatePersonRequestValidator();
        var validationResult = validator.Validate(request);

        if (!validationResult.IsValid)
            return Result.Failure<Person>(string.Join(", ", validationResult.Errors));

        return Result.Success(new Person(request.Name, request.Surname, request.DateOfBirth));
    }

    public sealed class ChangeSurnameRequest
    {
        public string NewSurname { get; set; }
    }

    public sealed class ChangeSurnameRequestValidator : AbstractValidator<ChangeSurnameRequest>
    {
        public ChangeSurnameRequestValidator()
        {
            RuleFor(x => x.NewSurname).NotEmpty().MaximumLength(100);
        }
    }

    public Result<Person> ChangeSurname(ChangeSurnameRequest request)
    {
        var validator = new ChangeSurnameRequestValidator();
        var validationResult = validator.Validate(request);

        if (!validationResult.IsValid)
            return Result.Failure<Person>(string.Join(", ", validationResult.Errors));

        return Result.Success(new Person(Name, request.NewSurname, DateOfBirth));
    }
}

I can work on the code now. I will refactor the parts I don't like and make them more standardized with the projects I work on. I will even ask LLM to write unit tests, etc.

But what happens with the spell? Is it forever lost? Was the purpose just a one-time chant?

Storing base spells and reusing them when necessary would be much easier than writing them from scratch, and it would produce more consistent results. But, if we decide to store the base spells, where should we store them, and how should we organize them?

The problem is even worse when you need to instruct multiple LLMs simultaneously. Good instructions for one are not necessarily good instructions for the other. For example, I'm deeply offended when Claude/Sonnet for my c# Blazor transforms it to React. I must be specific in my prompt to keep it in the c# realm. For ChatGPT, I never have to do that (even if I clean the stored data). This problem is even more pronounced with picture generators.

The Solutions?

Storing and sharing prompts is an as old idea as LLMs themself. There are browser plugins, even websites like prompthere.com or promptbase.com, where users share or sell prompts.

I don't find these approaches practical for development. I don't want to share them, at least not outside the team. Also, most prompts are project-specific.

My requirements are:

  • Markdown Format - It must support markdown, de-factor LLM, and human-readable format.
  • History - It would be nice to have an option to see the changes, but the focus should be on the current version.
  • Searchable - It needs to have at least some search capabilities. Tagging and other similar capabilities are not necessary.
  • Security - It probably should not be public, but it is not some secret.

Basic solution:

Put everything into a single markdown file in git and publish it on the website with a static website generator.

Conclusions

Maybe one day, I will instruct AI to write an AI prompt for me inside of the IDE, but in the meantime, we have to find something less elegant but still practical.

My current solution is a plain spellbook in the markdown format stored on git and published as a static website. I have something similar with Chocolatey, an essential list of commands I use when installing something.

Practical solutions can be simple but still functional.