https://github.com/CharlieDigital/SKPromptGenerator
Generates strongly typed classes for Semantic Kernel using prompt strings.
https://github.com/CharlieDigital/SKPromptGenerator
ai llm semantic-kernel source-generator
Last synced: 5 months ago
JSON representation
Generates strongly typed classes for Semantic Kernel using prompt strings.
- Host: GitHub
- URL: https://github.com/CharlieDigital/SKPromptGenerator
- Owner: CharlieDigital
- License: mit
- Created: 2024-06-23T01:12:30.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2024-09-25T19:32:59.000Z (over 1 year ago)
- Last Synced: 2025-08-01T00:55:13.070Z (5 months ago)
- Topics: ai, llm, semantic-kernel, source-generator
- Language: C#
- Homepage: https://www.nuget.org/packages/SKPromptGenerator
- Size: 3.81 MB
- Stars: 12
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- RSCG_Examples - https://github.com/CharlieDigital/SKPromptGenerator
README
# Semantic Kernel (SK) Prompt Generator
SKPromptGenerator is a C# source generator which automatically creates strongly-typed classes for your string prompt templates.
It is intended to be used with [Semantic Kernel](https://github.com/microsoft/semantic-kernel).
https://github.com/user-attachments/assets/a3727ef4-6880-4939-be40-5c5c08948a3e
> 💡 NOTE: As of version 4, the token format is switched from `{name}` to `{{$name}}` to match Semantic Kernel.
## Motivation
When working with prompts, you'll end up doing a lot of string templating and repetitive code.
Wouldn't it be nice if you could just have a strongly-typed class for each prompt automatically created using the prompt?
This library does exactly that.
```csharp
public static class Prompts
{
// Define a prompt
[PromptTemplate]
public const string Capitol = """
What is the capitol of {{$state}} {{$country}}?
Respond directly in a single line
""";
}
// Execute the prompt passing in a Semantic Kernel instance.
var capitol = await new CapitolPrompt(
state: "NJ",
country: "USA"
).ExecuteAsync(kernel);
```
The tokens in the prompt string become named parameters on the class constructor 🎉
## Limitations
1. Your prompt must be a `const string` because the generator needs to be able to read the string in the source.
2. Your prompts must live in some class in a namespace. If you get the error `error CS1001: Identifier expected`, then you are probably missing a namespace around your prompt.
3. You must add a dependency to `Microsoft.SemanticKernel` since the `ExecuteAsync` method requires the `Kernel` instance.
4. Currently only targets .NET 8; considering `netstandard2.0`.
## Installing
This generator is built for .NET 8.
To install:
```shell
dotnet add package SKPromptGenerator
```
For the latest releases, see: https://www.nuget.org/packages/SKPromptGenerator
## Using
This repository includes a sample project under the `/app` directory.
To use, create a new console app:
```shell
mkdir sk-prompt-gen-test
cd sk-prompt-gen-test
dotnet new console
dotnet add package SKPromptGenerator
dotnet add package Microsoft.SemanticKernel
```
In the project, create a class like so (you can call your class whatever you want):
```csharp
namespace SomeNamespace;
public static class Prompts
{
[PromptTemplate]
public const string Capitol = """
What is the capitol of {{$state}} {{$country}}?
Respond directly in a single line
When writing the state, always write it as the full name
Write your output in the format: The capitol of is: .
For example: The capitol of California is: Sacramento.
""";
}
```
> 💡 Note the usage of a namespace for the class. The prompt need not be in a standalone class. It can also be placed in an existing `Controller` (for example)
In the code above, we've created a prompt with two tokens: `{{$state}}` and `{{$country}}`.
The `[PromptTemplate]` attribute instructs the generator to create a class like so:
```csharp
using System;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using SKPromptGenerator;
namespace SomeNamespace;
public partial class CapitolPrompt(
string state, string country
) : PromptTemplateBase
{
public override string Text => $$"""
What is the capitol of {{state}} {{country}}?
Respond directly in a single line
When writing the state, always write it as the full name
Write your output in the format: The capitol of is: .
For example: The capitol of California is: Sacramento.
""";
public override OpenAIPromptExecutionSettings Settings => new OpenAIPromptExecutionSettings
{
MaxTokens = 500,
Temperature = 0.5d,
TopP = 0d,
};
}
```
Note the two class parameters `state` and `country` which are extracted from the prompt template are now string literal tokens.
Now we can use the prompt like so:
```csharp
var capitol = await new CapitolPrompt("NJ", "USA").ExecuteAsync(kernel);
Console.WriteLine($"{capitol}");
// The capitol of New Jersey is: Trenton.
capitol = await new CapitolPrompt("NY", "USA").ExecuteAsync(kernel);
Console.WriteLine($"{capitol}");
// The capitol of New York is: Albany.
```
If your prompt returns JSON, we can also deserialize it into an object:
```csharp
// Our model that we are serializing to
public record CapitolResponse(string Country, string State, string Capitol);
// Use ExecuteWithJsonAsync if we also want the raw JSON
var (sacramento, json) = await new CapitolJsonPrompt(
"CA",
"US"
).ExecuteWithJsonAsync(kernel);
```
NOTE: The underlying code will strip Markdown fences so if you are expecting your result to contain markdown, it will be stripped.
### Typed Parameters
If you want to use typed parameters, you can append the type to the parameter token:
```csharp
[PromptTemplate]
public const string Cities = """
Write a list of {{$count:int}} cities in {{$region}}, {{$country}}
Write each city on a separate line
Start you response with: Sure, here are {{$count:int}} cities in {{$region}}, {{$country}}
""";
```
This will generate the signature:
```csharp
public partial class CitiesPrompt(
int count, string region, string country
) : PromptTemplateBase
```
Which can then be invoked with a typed, integer parameter for count:
```csharp
var njCities = await new CitiesPrompt(4, "NJ", "USA").ExecuteAsync(kernel);
```
This will output:
```
Sure, here are 4 cities in NJ, USA:
1. Newark
2. Jersey City
3. Paterson
4. Elizabeth
```
Note that if you are using your own types, those types should be added using a global `using` statement or specify the full type name since the generated class does not know about your namespaces.
(See the example in the `/app` directory for usage)
### Including History
The `ExecuteAsync` method takes a `historyBuilder` parameter which will receives a `ChatHistory` instance
The unit tests show how this can be used:
```csharp
[Fact]
public async void History_Builder_Test()
{
var response = await new HistoryTmplPrompt("Spencer").ExecuteAsync(
new Kernel(),
// 👇 Here we can build the chat history up before adding the new user prompt
historyBuilder: (h) =>
{
h.Add(new ChatMessageContent(AuthorRole.User, "User question"));
h.Add(new ChatMessageContent(AuthorRole.System, "System response"));
}
);
// Fake test where we are just going to return the history instead
Assert.Equal("User question\nSystem response", response);
}
```
You can do the retrieval of the actual history *before* the block and then do the history building in the block.
## Custom Base Class
If you want to customize how the prompt is executed, you can specify a custom base class when assigning the attribute.
Your base class must inherit from `PromptTemplateBase`:
```csharp
public abstract class CustomBase : PromptTemplateBase
{
public override async Task ExecuteAsync(
Kernel kernel,
string? serviceId = null,
CancellationToken cancellation = default
)
{
return await Task.FromResult("response");
}
}
```
And then you can specify this custom base class as a generic type:
```csharp
[PromptTemplate]
public const string CapitolCustom = """
What is the capitol of {{$state}} {{$country}}?
Respond directly in a single line
When writing the state, always write it as the full name
Write your output in the format: The capitol of is: .
For example: The capitol of California is: Sacramento.
""";
```
This allows you to take full control of the execution of the prompt (e.g. add logging, telemetry, etc.).
> 💡 Note: you should use a global `using` statement for the namespace of your custom base class.
## Prompt Execution Settings
The `PromptTemplate` attribute also allows specification of the prompt execution settings.
The three parameters are:
|Parameter|Details|Default|
|--|--|--|
|`MaxTokens`|The maximum number of tokens in the response|`500`|
|`Temperature`|The temperature|`0.5`|
|`TopP`|The TopP|`0`|
For example:
```csharp
public static class Prompts
{
[PromptTemplate(10, 0.1)]
public const string SampleTmpl1 = """
What is the capitol of {{$state}} {{$country}}
Respond directly on a single line.
""";
}
```
(See the `PromptTmpl` class for details)
## Using the Sample App
To use the sample app, you'll need to set up user secrets:
```shell
dotnet user-secrets init
dotnet user-secrets set "AzureOpenAIKey" "YOUR_AZURE_OPEN_AI_KEY"
dotnet user-secrets set "AzureOpenAIEndpoint" "YOUR_AZURE_OPEN_AI_ENDPOINT"
```
If you are using OpenAI, feel free to fork this project and simply change the service type and configuration values.