featured image

The new prompt filter in Semantic Kernel

Working with the new prompt filter IPromptRenderFilter in semantic kernel

Yash Worlikar Yash Worlikar Wed May 15 2024 4 min read

With the latest semantic kernel version, there was a rehaul to the filters API along with multiple new filters introduced. Today Let’s look at the IPromptRenderFilter

Note this feature is still marked as experimental and is subject to change in future updates as of Microsoft.SemanticKernel version 1.11.1

But first, what’s the benefit of using the IPromptRenderFilter filter?

This filter can be used to add logic before or after a prompt gets rendered. With this we can easily add custom logic like checking for malicious use or censoring sensitive information before sending it to our AI service.

public sealed class PromptRenderFilter : IPromptRenderFilter
{
	public async Task OnPromptRenderAsync(PromptRenderContext context, Func<PromptRenderContext, Task> next)
	{
		//Logic Before the prompt is rendered 
		await next(context);
		//Logic After the prompt is rendered 
	}
}

Rather than keeping methods for OnRendering and OnRendered separate this interface only uses one method OnPromptRenderAsync. With the help of await next(context) we can add custom logic before or after rendering a prompt.

Note that only the prompt was rendered after the next(context) call and has not yet been sent to the AI service.

To get a better understanding of how the filter works let’s use a simple coin toss example.

First, create a kernel plugin named CoinFlip that returns either Heads or Tails randomly.

/// <summary>
/// Class that represents a coin toss game.
/// </summary>
[Description("Represents a coin toss game")]
public class CoinFlip
{
   private readonly Random _random = new();

   [KernelFunction, Description("Toss the coin")]
   public string TossCoin()
   {
	   // Generate a random number: 0 or 1
	   int tossResult = _random.Next(2);

	   // If the result is 0, return "Heads". Otherwise, return "Tails".
	   return tossResult == 0 ? "Heads" : "Tails";
   }
}

Now we will be implementing the prompt filter. This filter will generate a response based on the rendered prompt template and return a response based on the generated prompt. (Assigning a new FunctionResult stops further execution and returns the results to the parent kernel invocation call)

public sealed class CoinFlipFilter : IPromptRenderFilter
{
	public async Task OnPromptRenderAsync(PromptRenderContext context, Func<PromptRenderContext, Task> next)
	{
		//Before the prompt is rendered 

		await next(context);
		//After the prompt is rendered 
		string prompt = context.RenderedPrompt;
		Console.WriteLine(prompt);
		if (prompt == "Heads")
		{
			context.Result = new FunctionResult(context.Function, "You win");
		}
		else
		{
			context.Result = new FunctionResult(context.Function, "You lose");
		}
	}
}

Next, create a kernel with the CoinFilp plugin. For this example, there’s no need to enter the Model name and key as the calls will be blocked by our filter.

IKernelBuilder builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion("<Model-Name>", "<API-Key>");
builder.Plugins.AddFromType<CoinFlip>();
Kernel kernel = builder.Build();

Finally, for setup, we will be adding the Prompt filter to our kernel

kernel.PromptRenderFilters.Add(new CoinFlipFilter());

Now that we have our setup ready let’s try out our filter

//Name of our plugin method
string tossPrompt = "{{CoinFlip.TossCoin}}";
var response = await kernel.InvokePromptAsync(tossPrompt);
Console.WriteLine($"Assistant Response: {response}");

Based on the value we should be seeing a similar output in the kernel. Instead of returning a generated response, we are instead returning a custom response through our filter.

Heads
Assistant Response: You win

Here’s the complete program

internal class Program
{
	static async Task Main(string[] args)
	{
		IKernelBuilder builder = Kernel.CreateBuilder();

		builder.AddOpenAIChatCompletion("<Model-Name>", "<API-Key>");
		builder.Plugins.AddFromType<CoinFlip>();
		Kernel kernel = builder.Build();

		kernel.PromptRenderFilters.Add(new CoinFlipFilter());

		string tossPrompt = "{{CoinFlip.TossCoin}}";

		var response = await kernel.InvokePromptAsync(tossPrompt);
		Console.WriteLine($"Assistant Response: {response}");
	}
}

/// <summary>
/// Class that represents a coin toss game.
/// </summary>
[Description("Represents a coin toss game")]
public class CoinFlip
{
	private readonly Random _random = new();

	[KernelFunction, Description("Toss the coin")]
	public string TossCoin()
	{
		// Generate a random number: 0 or 1
		int tossResult = _random.Next(2);

		// If the result is 0, return "Heads". Otherwise, return "Tails".
		return tossResult == 0 ? "Heads" : "Tails";
	}
}

public sealed class CoinFlipFilter : IPromptRenderFilter
{
	public async Task OnPromptRenderAsync(PromptRenderContext context, Func<PromptRenderContext, Task> next)
	{
		//Before the prompt is rendered 
		var a  = context.Kernel.Plugins.First();
		await next(context);
		//After the prompt is rendered 
		string prompt = context.RenderedPrompt;
		Console.WriteLine(prompt);
		if (prompt == "Heads")
		{
			context.Result = new FunctionResult(context.Function, "You win");
		}
		else
		{
			context.Result = new FunctionResult(context.Function, "You lose");
		}
	}
}

Although experimental, filters open up a lot of possibilities for developers to ensure the security and integrity of the data being processed. The IPromptRenderFilter offers better control to our existing workflows allowing us to modify it in a way that was not easily possible.

Prev
Packaged Food Health Checker with Semantic kernel
Next
Yaml prompts with semantic kernel