docs/decisions/0033-kernel-filters.md
Current way of intercepting some event during function execution works as expected using Kernel Events and event handlers. Example:
ILogger logger = loggerFactory.CreateLogger("MyLogger");
var kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey)
.Build();
void MyInvokingHandler(object? sender, FunctionInvokingEventArgs e)
{
logger.LogInformation("Invoking: {FunctionName}", e.Function.Name)
}
void MyInvokedHandler(object? sender, FunctionInvokedEventArgs e)
{
if (e.Result.Metadata is not null && e.Result.Metadata.ContainsKey("Usage"))
{
logger.LogInformation("Token usage: {TokenUsage}", e.Result.Metadata?["Usage"]?.AsJson());
}
}
kernel.FunctionInvoking += MyInvokingHandler;
kernel.FunctionInvoked += MyInvokedHandler;
var result = await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.")
There are a couple of problems with this approach:
ILoggerFactory in handler, the handler should be defined in place where ILoggerFactory instance is available).Introduce Kernel Filters - the approach of receiving the events in Kernel in similar way as action filters in ASP.NET.
Two new abstractions will be used across Semantic Kernel and developers will have to implement these abstractions in a way that will cover their needs.
For function-related events: IFunctionFilter
public interface IFunctionFilter
{
void OnFunctionInvoking(FunctionInvokingContext context);
void OnFunctionInvoked(FunctionInvokedContext context);
}
For prompt-related events: IPromptFilter
public interface IPromptFilter
{
void OnPromptRendering(PromptRenderingContext context);
void OnPromptRendered(PromptRenderedContext context);
}
New approach will allow developers to define filters in separate classes and easily inject required services to process kernel event correctly:
MyFunctionFilter.cs - filter with the same logic as event handler presented above:
public sealed class MyFunctionFilter : IFunctionFilter
{
private readonly ILogger _logger;
public MyFunctionFilter(ILoggerFactory loggerFactory)
{
this._logger = loggerFactory.CreateLogger("MyLogger");
}
public void OnFunctionInvoking(FunctionInvokingContext context)
{
this._logger.LogInformation("Invoking {FunctionName}", context.Function.Name);
}
public void OnFunctionInvoked(FunctionInvokedContext context)
{
var metadata = context.Result.Metadata;
if (metadata is not null && metadata.ContainsKey("Usage"))
{
this._logger.LogInformation("Token usage: {TokenUsage}", metadata["Usage"]?.AsJson());
}
}
}
As soon as new filter is defined, it's easy to configure it to be used in Kernel using dependency injection (pre-construction) or add filter after Kernel initialization (post-construction):
IKernelBuilder kernelBuilder = Kernel.CreateBuilder();
kernelBuilder.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey);
// Adding filter with DI (pre-construction)
kernelBuilder.Services.AddSingleton<IFunctionFilter, MyFunctionFilter>();
Kernel kernel = kernelBuilder.Build();
// Adding filter after Kernel initialization (post-construction)
// kernel.FunctionFilters.Add(new MyAwesomeFilter());
var result = await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.");
It's also possible to configure multiple filters which will be triggered in order of registration:
kernelBuilder.Services.AddSingleton<IFunctionFilter, Filter1>();
kernelBuilder.Services.AddSingleton<IFunctionFilter, Filter2>();
kernelBuilder.Services.AddSingleton<IFunctionFilter, Filter3>();
And it's possible to change the order of filter execution in runtime or remove specific filter if needed:
kernel.FunctionFilters.Insert(0, new InitialFilter());
kernel.FunctionFilters.RemoveAt(1);