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:
First, let’s create a new .NET console application
Set up the project structure
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 directorycd llm-history-to-post
# Create a new .NET console applicationdotnet new console
# Add necessary NuGet packagesdotnet add package Spectre.Console # For interactive CLI prompts
Now, let’s define the basic structure of our application. We’ll need:
A parser for the chat history file
A model to represent prompts, responses, and days
CLI interaction components
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
{
classProgram {
staticasync 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 dayvar dayGroups = chatHistory.GroupByDay();
if (!dayGroups.Any())
{
AnsiConsole.MarkupLine("[yellow]No chat history days found in the file.[/]");
return;
}
// Day selectionvar selectedDay = SelectDay(dayGroups);
if (selectedDay == null)
{
AnsiConsole.MarkupLine("[yellow]No day selected. Exiting.[/]");
return;
}
// Prompt selectionvar selectedPrompts = SelectPrompts(selectedDay);
if (!selectedPrompts.Any())
{
AnsiConsole.MarkupLine("[yellow]No prompts selected. Exiting.[/]");
return;
}
// Collect verdicts CollectVerdicts(selectedPrompts);
// Generate blog postvar 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}");
}
}
privatestatic 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;
}
privatestatic 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();
}
privatestaticvoid 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
{
publicclassConversation {
publicstring Prompt { get; set; }
publicstring Response { get; set; }
public DateTime Timestamp { get; set; }
public Verdict Verdict { get; set; }
}
publicclassVerdict {
publicbool IsSuccess { get; set; }
publicstring Comment { get; set; }
publicoverridestring ToString()
{
return$"Verdict: {(IsSuccess ? "✅" : "❌")} {Comment}";
}
}
publicclassDayGroup {
public DateTime Date { get; set; }
public List<Conversation> Conversations { get; set; } = new List<Conversation>();
}
publicclassChatHistory {
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
{
publicclassChatHistoryParser {
privatestaticreadonly Regex TimestampRegex = new Regex(@"# aider chat started at (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})");
privatestaticreadonly Regex UserPromptRegex = new Regex(@"^####\s+(.*)$", RegexOptions.Multiline);
publicasync Task<ChatHistory> ParseAsync(string filePath)
{
var content = await File.ReadAllTextAsync(filePath);
var chatHistory = new ChatHistory();
// Split the content by timestamp sectionsvar sections = TimestampRegex.Split(content);
DateTime currentTimestamp = DateTime.MinValue;
for (int i = 1; i < sections.Length; i += 2)
{
// Parse the timestampif (DateTime.TryParse(sections[i], out DateTime timestamp))
{
currentTimestamp = timestamp;
}
string sectionContent = sections[i + 1];
// Split by user promptsvar 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
{
publicclassBlogPostGenerator {
publicstring Generate(DateTime date, List<Conversation> conversations)
{
// Create the output directory if it doesn't existstring 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 determinestring 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();
// Conversationsforeach (var conversation in conversations)
{
sb.AppendLine("## Prompt and Response");
sb.AppendLine();
sb.AppendLine("> Prompt:");
sb.AppendLine(">");
// Format the prompt with proper indentation for blockquoteforeach (var line in conversation.Prompt.Split('\n'))
{
sb.AppendLine($"> {line}");
}
sb.AppendLine(">");
sb.AppendLine("> Response:");
sb.AppendLine(">");
// Format the response with proper indentation for blockquoteforeach (var line in conversation.Response.Split('\n'))
{
sb.AppendLine($"> {line}");
}
sb.AppendLine();
// Add verdictif (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
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!