Skip to content

Repository files navigation

Zaiets.PromptBuilder

A fluent .NET 8 library for composing prompts to send to LLM APIs. Build system instructions, render templates with named variables, manage conversation history, and check token budgets — all with a clean, chainable API.

Installation

dotnet add package Zaiets.PromptBuilder

Quick Start

using Zaiets.PromptBuilder;

var result = new PromptBuilder()
    .WithSystemPrompt("You are a concise technical writer.")
    .WithTemplate("Explain {{concept}} in one paragraph, targeted at a {{audience}}.")
    .WithVariable("concept", "async/await in C#")
    .WithVariable("audience", "junior developer")
    .Build();

// Send result.Messages to your preferred LLM client
Console.WriteLine(result.FormattedPrompt);
Console.WriteLine($"Estimated tokens: {result.EstimatedTokenCount}");

Features

Feature Description
Fluent API Chainable methods — no boilerplate
Template engine {{variable}} placeholders with strict-mode validation
Chat history Add, trim to token limit, keep last N turns
Token counting cl100k_base-compatible approximation (±10 % for English)
DI support services.AddPromptBuilder() for ASP.NET Core
Immutable messages ChatMessage is a value-like sealed class

API Reference

PromptBuilder

The main entry point. All methods return this for chaining.

var builder = new PromptBuilder();                     // default tokenizer
var builder = new PromptBuilder(myCustomTokenizer);    // custom tokenizer

System prompt

builder.WithSystemPrompt("You are a helpful assistant.");

Replaces any previously set system message.

Templates

builder
    .WithTemplate("Summarise the following in {{language}}: {{text}}")
    .WithVariable("language", "French")
    .WithVariable("text", someArticle)
    .WithStrictTemplates();   // throws TemplateRenderException if a variable is missing

Explicit messages

builder
    .AddUserMessage("What is a monad?")
    .AddAssistantMessage("A monad is a design pattern…")   // few-shot example
    .AddUserMessage("Can you give a C# example?");

Chat history

var history = new ChatHistory()
    .AddSystemMessage("You are a code reviewer.")
    .AddUserMessage("Here is my PR diff: …")
    .AddAssistantMessage("Looks good overall, but consider …")
    .TrimToTokenLimit(3000);

builder.WithChatHistory(history);

Building

PromptResult result = builder.Build();

result.Messages              // IReadOnlyList<ChatMessage> — pass to your LLM client
result.FormattedPrompt       // single string for logging / completion endpoints
result.EstimatedTokenCount   // int — total estimated tokens
result.SystemPrompt          // string? — extracted system message
result.TurnCount             // non-system message count
result.FitsWithin(4096)      // bool
result.TokensRemaining(4096) // int — remaining budget

ChatHistory

var history = new ChatHistory();

history
    .AddSystemMessage("You are helpful.")
    .AddUserMessage("Hello!")
    .AddAssistantMessage("Hi there, how can I help?");

// Trim to a token budget (preserves system messages)
history.TrimToTokenLimit(2048);

// Keep only the last 5 turns
var recent = history.LastNTurns(5);

// Fit into a window without modifying the original
var fitted = history.FitWithin(1500);

// Convenience extension: add a user+assistant pair in one call
history.AddTurn("What is 2 + 2?", "4.");

PromptTemplate

Use standalone when you need template rendering without the full builder.

var tpl = new PromptTemplate("Dear {{name}}, your order {{orderId}} has shipped.");

// Render with a dictionary
string text = tpl.Render(new Dictionary<string, string>
{
    ["name"]    = "Alice",
    ["orderId"] = "ORD-9982"
});

// Check for missing variables before rendering
IReadOnlyList<string> missing = tpl.GetMissingVariables(myVars);

// Inspect declared placeholders
IReadOnlySet<string> declared = tpl.DeclaredVariables;
// → { "name", "orderid" }

ITokenizer / SimpleTokenizer

ITokenizer tokenizer = new SimpleTokenizer();

int tokens = tokenizer.CountTokens("Hello, world!");          // plain string
int tokens = tokenizer.CountTokens(someMessage);              // ChatMessage
int tokens = tokenizer.CountTokens(history.Messages);         // message sequence

Implement ITokenizer to plug in a BPE tokenizer (e.g. Microsoft.ML.Tokenizers).


Dependency Injection (ASP.NET Core)

// Program.cs
builder.Services.AddPromptBuilder();

// Inject PromptBuilder into any service
public class SummaryService(PromptBuilder promptBuilder)
{
    public PromptResult BuildSummaryPrompt(string article) =>
        promptBuilder
            .WithSystemPrompt("You are a summarisation expert.")
            .WithTemplate("Summarise this article in 3 bullet points:\n\n{{article}}")
            .WithVariable("article", article)
            .Build();
}

Custom tokenizer:

builder.Services.AddPromptBuilder<MyBpeTokenizer>();

Conditional sections

builder
    .WithSystemPrompt("You are a code assistant.")
    .When(includeFewShot, b => b
        .AddUserMessage("Reverse a string in C#.")
        .AddAssistantMessage("return new string(input.Reverse().ToArray());"))
    .AddUserMessage(userQuestion)
    .Build();

Token counting accuracy

SimpleTokenizer uses a word-piece length heuristic calibrated against the OpenAI cl100k_base vocabulary:

Text type Typical error
English prose ±5 %
Code (C#, Python, JS) ±10 %
Mixed / non-Latin ±15 %

For production systems that need exact counts, implement ITokenizer using Microsoft.ML.Tokenizers or the tiktoken-dotnet library.


License

MIT © 2025 Vladyslav Zaiets

About

Fluent prompt builder for LLM APIs - templates, variables, chat history, token counting | NuGet: dotnet add package Zaiets.PromptBuilder

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages