Introduction

Yesterday we went through and iterated over requirements for a tool that will help write these blog posts. Let’s see how it plays out feeding these requirements into the llm and asking it to implement it!

First Try

Prompt:

Read through requirements.md and lets work out how to make a dotnet project in the llm-history-to-post folder

Response:

llm-history-to-post/requirements.md
Add file to the chat? (Y)es/(N)o/(D)on’t ask again [Yes]: y

Based on the requirements, we need to create a .NET console application that will parse LLM chat history and generate blog post templates. Let’s break down the steps to create this project:

  1. First, let’s create a new .NET console application
  2. Set up the project structure
  3. Add necessary NuGet packages for CLI interaction (like Spectre.Console for interactive prompts)

Let’s start by creating the project:

# Create a new directory for the project (if it doesn't exist)
mkdir -p llm-history-to-post

# Navigate to the project directory
cd llm-history-to-post

# Create a new .NET console application
dotnet new console

# Add necessary NuGet packages
dotnet add package Spectre.Console  # For interactive CLI prompts

Now, let’s define the basic structure of our application. We’ll need:

  1. A parser for the chat history file
  2. A model to represent prompts, responses, and days
  3. CLI interaction components
  4. Blog post generation logic

Let’s start by creating these components. First, let’s modify the Program.cs file to set up the basic structure of our application.

Would you like me to provide the initial code for these components?

Tokens: 1.6k sent, I’ll create the necessary files for the LLM History to Post application based on the requirements. Let me provide the initial code structure:

llm-history-to-post/Program.cs

<<<<<<< SEARCH
=======
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Spectre.Console;

namespace LlmHistoryToPost
{
    class Program
    {
        static async Task Main(string[] args)
        {
            try
            {
                string historyFilePath = args.Length > 0 
                    ? args[0] 
                    : Path.Combine(Directory.GetCurrentDirectory(), ".aider.chat.history.md");

                if (!File.Exists(historyFilePath))
                {
                    AnsiConsole.MarkupLine($"[red]Error:[/] History file not found at {historyFilePath}");
                    return;
                }

                var parser = new ChatHistoryParser();
                var chatHistory = await parser.ParseAsync(historyFilePath);
                
                // Group by day
                var dayGroups = chatHistory.GroupByDay();
                
                if (!dayGroups.Any())
                {
                    AnsiConsole.MarkupLine("[yellow]No chat history days found in the file.[/]");
                    return;
                }

                // Day selection
                var selectedDay = SelectDay(dayGroups);
                if (selectedDay == null)
                {
                    AnsiConsole.MarkupLine("[yellow]No day selected. Exiting.[/]");
                    return;
                }

                // Prompt selection
                var selectedPrompts = SelectPrompts(selectedDay);
                if (!selectedPrompts.Any())
                {
                    AnsiConsole.MarkupLine("[yellow]No prompts selected. Exiting.[/]");
                    return;
                }

                // Collect verdicts
                CollectVerdicts(selectedPrompts);

                // Generate blog post
                var blogPostGenerator = new BlogPostGenerator();
                var outputPath = blogPostGenerator.Generate(selectedDay.Date, selectedPrompts);

                AnsiConsole.MarkupLine($"[green]Blog post generated successfully at:[/] {outputPath}");
            }
            catch (Exception ex)
            {
                AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
            }
        }

        private static DayGroup SelectDay(List<DayGroup> dayGroups)
        {
            if (dayGroups.Count == 1)
            {
                AnsiConsole.MarkupLine($"[green]Only one day found. Automatically selecting:[/] {dayGroups[0].Date:yyyy-MM-dd}");
                return dayGroups[0];
            }

            var selectedDay = AnsiConsole.Prompt(
                new SelectionPrompt<DayGroup>()
                    .Title("Select a day to process:")
                    .PageSize(10)
                    .UseConverter(day => $"{day.Date:yyyy-MM-dd} ({day.Conversations.Count} conversations)")
                    .AddChoices(dayGroups));

            return selectedDay;
        }

        private static List<Conversation> SelectPrompts(DayGroup dayGroup)
        {
            var choices = dayGroup.Conversations
                .Select((conv, index) => new { Index = index, Conversation = conv })
                .ToList();

            var selectedIndices = AnsiConsole.Prompt(
                new MultiSelectionPrompt<int>()
                    .Title($"Select prompts from {dayGroup.Date:yyyy-MM-dd}:")
                    .PageSize(15)
                    .InstructionsText("[grey](Press <space> to select, <enter> to confirm)[/]")
                    .UseConverter(index => 
                    {
                        var prompt = choices[index].Conversation.Prompt;
                        return $"{index + 1}. {(prompt.Length > 100 ? prompt.Substring(0, 97) + "..." : prompt)}";
                    })
                    .AddChoices(choices.Select(c => c.Index)));

            return selectedIndices.Select(index => dayGroup.Conversations[index]).ToList();
        }

        private static void CollectVerdicts(List<Conversation> conversations)
        {
            foreach (var conversation in conversations)
            {
                AnsiConsole.Clear();
                AnsiConsole.MarkupLine("[blue]===== PROMPT =====[/]");
                AnsiConsole.WriteLine(conversation.Prompt);
                
                AnsiConsole.MarkupLine("\n[green]===== RESPONSE =====[/]");
                AnsiConsole.WriteLine(conversation.Response);

                var isSuccess = AnsiConsole.Confirm("Was this a success?", true);
                
                var comment = AnsiConsole.Ask<string>("Enter a comment for the verdict:", "");
                
                conversation.Verdict = new Verdict
                {
                    IsSuccess = isSuccess,
                    Comment = comment
                };
            }
        }
    }
}
>>>>>>> REPLACE

llm-history-to-post/Models.cs

<<<<<<< SEARCH
=======
using System;
using System.Collections.Generic;
using System.Linq;

namespace LlmHistoryToPost
{
    public class Conversation
    {
        public string Prompt { get; set; }
        public string Response { get; set; }
        public DateTime Timestamp { get; set; }
        public Verdict Verdict { get; set; }
    }

    public class Verdict
    {
        public bool IsSuccess { get; set; }
        public string Comment { get; set; }

        public override string ToString()
        {
            return $"Verdict: {(IsSuccess ? "" : "")} {Comment}";
        }
    }

    public class DayGroup
    {
        public DateTime Date { get; set; }
        public List<Conversation> Conversations { get; set; } = new List<Conversation>();
    }

    public class ChatHistory
    {
        public List<Conversation> Conversations { get; set; } = new List<Conversation>();

        public List<DayGroup> GroupByDay()
        {
            return Conversations
                .GroupBy(c => c.Timestamp.Date)
                .Select(g => new DayGroup
                {
                    Date = g.Key,
                    Conversations = g.ToList()
                })
                .OrderBy(d => d.Date)
                .ToList();
        }
    }
}
>>>>>>> REPLACE

llm-history-to-post/ChatHistoryParser.cs

<<<<<<< SEARCH
=======
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace LlmHistoryToPost
{
    public class ChatHistoryParser
    {
        private static readonly Regex TimestampRegex = new Regex(@"# aider chat started at (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})");
        private static readonly Regex UserPromptRegex = new Regex(@"^####\s+(.*)$", RegexOptions.Multiline);

        public async Task<ChatHistory> ParseAsync(string filePath)
        {
            var content = await File.ReadAllTextAsync(filePath);
            var chatHistory = new ChatHistory();
            
            // Split the content by timestamp sections
            var sections = TimestampRegex.Split(content);
            
            DateTime currentTimestamp = DateTime.MinValue;
            
            for (int i = 1; i < sections.Length; i += 2)
            {
                // Parse the timestamp
                if (DateTime.TryParse(sections[i], out DateTime timestamp))
                {
                    currentTimestamp = timestamp;
                }
                
                string sectionContent = sections[i + 1];
                
                // Split by user prompts
                var promptMatches = UserPromptRegex.Matches(sectionContent);
                
                if (promptMatches.Count > 0)
                {
                    for (int j = 0; j < promptMatches.Count; j++)
                    {
                        var match = promptMatches[j];
                        int startIndex = match.Index;
                        int endIndex = (j < promptMatches.Count - 1) ? promptMatches[j + 1].Index : sectionContent.Length;
                        
                        string prompt = match.Groups[1].Value.Trim();
                        string response = sectionContent.Substring(
                            startIndex + match.Length, 
                            endIndex - (startIndex + match.Length)
                        ).Trim();
                        
                        chatHistory.Conversations.Add(new Conversation
                        {
                            Prompt = prompt,
                            Response = response,
                            Timestamp = currentTimestamp
                        });
                    }
                }
            }
            
            return chatHistory;
        }
    }
}
>>>>>>> REPLACE

llm-history-to-post/BlogPostGenerator.cs

<<<<<<< SEARCH
=======
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace LlmHistoryToPost
{
    public class BlogPostGenerator
    {
        public string Generate(DateTime date, List<Conversation> conversations)
        {
            // Create the output directory if it doesn't exist
            string year = date.Year.ToString();
            string month = date.Month.ToString("00");
            string day = date.Day.ToString("00");
            
            string directoryPath = Path.Combine("content", "posts", year, month);
            Directory.CreateDirectory(directoryPath);
            
            // Determine the day number (this is a simplification - in a real app you might want to track this better)
            int dayNumber = 1; // Default to 1 if we can't determine
            
            string fileName = $"{year}-{month}-{day}-hadai-day-{dayNumber}.md";
            string outputPath = Path.Combine(directoryPath, fileName);
            
            var sb = new StringBuilder();
            
            // Add YAML frontmatter
            sb.AppendLine("---");
            sb.AppendLine($"title: \"HADAI Day {dayNumber}\"");
            sb.AppendLine($"date: {year}-{month}-{day}");
            sb.AppendLine("categories:");
            sb.AppendLine("  - HADAI");
            sb.AppendLine("tags:");
            sb.AppendLine("  - AI");
            sb.AppendLine("  - LLM");
            sb.AppendLine("---");
            sb.AppendLine();
            
            // Introduction
            sb.AppendLine("## Introduction");
            sb.AppendLine();
            sb.AppendLine("Today I worked with an AI assistant on various tasks. Here's a summary of our interactions.");
            sb.AppendLine();
            
            // Conversations
            foreach (var conversation in conversations)
            {
                sb.AppendLine("## Prompt and Response");
                sb.AppendLine();
                sb.AppendLine("> Prompt:");
                sb.AppendLine(">");
                
                // Format the prompt with proper indentation for blockquote
                foreach (var line in conversation.Prompt.Split('\n'))
                {
                    sb.AppendLine($"> {line}");
                }
                
                sb.AppendLine(">");
                sb.AppendLine("> Response:");
                sb.AppendLine(">");
                
                // Format the response with proper indentation for blockquote
                foreach (var line in conversation.Response.Split('\n'))
                {
                    sb.AppendLine($"> {line}");
                }
                
                sb.AppendLine();
                
                // Add verdict
                if (conversation.Verdict != null)
                {
                    sb.AppendLine(conversation.Verdict.ToString());
                    sb.AppendLine();
                }
            }
            
            // Conclusion
            sb.AppendLine("## Conclusion");
            sb.AppendLine();
            sb.AppendLine("This concludes today's interactions with the AI assistant.");
            
            // Write to file
            File.WriteAllText(outputPath, sb.ToString());
            
            return outputPath;
        }
    }
}
>>>>>>> REPLACE

llm-history-to-post/llm-history-to-post.csproj

<<<<<<< SEARCH
=======
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <RootNamespace>LlmHistoryToPost</RootNamespace>
    <ImplicitUsings>disable</ImplicitUsings>
    <Nullable>disable</Nullable>
    <AssemblyName>llm-history-to-post</AssemblyName>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Spectre.Console" Version="0.47.0" />
  </ItemGroup>

</Project>
>>>>>>> REPLACE

Tokens: 2.9k sent, 2.4k received. Cost: $0.04 message, $0.05 session.
llm-history-to-post/Program.cs
Create new file? (Y)es/(N)o [Yes]: y
llm-history-to-post/Models.cs
Create new file? (Y)es/(N)o [Yes]: y
llm-history-to-post/ChatHistoryParser.cs
Create new file? (Y)es/(N)o [Yes]: y
llm-history-to-post/BlogPostGenerator.cs
Create new file? (Y)es/(N)o [Yes]: y
llm-history-to-post/llm-history-to-post.csproj
Create new file? (Y)es/(N)o [Yes]: y
Applied edit to llm-history-to-post/Models.cs
Applied edit to llm-history-to-post/Program.cs
Applied edit to llm-history-to-post/ChatHistoryParser.cs
Applied edit to llm-history-to-post/llm-history-to-post.csproj
Applied edit to llm-history-to-post/BlogPostGenerator.cs
Commit d4f6390 feat: Create initial .NET console app for parsing LLM chat history and generating blog posts
You can use /undo to undo and discard each aider commit. 257 received. Cost: $0.0086 message, $0.0086 session.

Verdict: ✅ Honestly this was pretty good! There are some things I didn’t like after I looked into further.

  • It set the target framework for the project to net6.0 when I was expecting net9.0. To be fair I never told it which framework I’d like.
  • Both ImplicitUsings and Nullables were disabled.
  • The generated files were using spaces instead of tabs for leading whitespace.
  • var was not used at all from what I could see, I prefer almost exclusively using it since Rider has such strong support to showing what types objects are.

All of these are mostly “whatever” but the thing that I saw that was the worst was

  • It didn’t wrap a single line statement with brackets after a conditional.

This technically compiles, but I feel is bad form and has almost no place in a professional setting. We can spare the extra characters to reduce the likelihood of introducing an error down the road.

Tries Two, Three, and Four…

The next couple tries were several iterations of adding statements to my aider conventions files (just a collection of markdown files with extra rules aider should use), asking the prompt to try again, and finding issues again.

I don’t think the interim results are all too interesting, so I’ll skip to the rules I ended up adding to my conventions.

general.md

* Prefer tabs instead of spaces for leading whitespace
* Prefer creating pure functions and aim for functional design
* Prefer dependency injection when possible

csharp.md

* use var whenever possible when defining local variables
* use filescoped namespaces
* make use of early returns when possible instead of nesting
* never do single line if statments. Always surrond if blocks with braces

I made a little bit more progress today, but to not spoil too much, the tool doesn’t entirely work, so I ran out of time writing the blog. We’ll finish up tomorrow!