Skip to content

Commit

Permalink
Merge branch 'microsoft:main' into update-modelid
Browse files Browse the repository at this point in the history
  • Loading branch information
SandraAhlgrimm committed Jan 24, 2024
2 parents f76a4b0 + f959256 commit f5baf9e
Show file tree
Hide file tree
Showing 161 changed files with 3,232 additions and 1,384 deletions.
150 changes: 150 additions & 0 deletions docs/decisions/0031-kernel-filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
---
# These are optional elements. Feel free to remove any of them.
status: accepted
contact: dmytrostruk
date: 2023-01-23
deciders: sergeymenshykh, markwallace, rbarreto, stephentoub, dmytrostruk
---

# Kernel Filters

## Context and Problem Statement

Current way of intercepting some event during function execution works as expected using Kernel Events and event handlers. Example:

```csharp
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:

1. Event handlers does not support dependency injection. It's hard to get access to specific service, which is registered in application, unless the handler is defined in the same scope where specific service is available. This approach provides some limitations in what place in solution the handler could be defined. (e.g. If developer wants to use `ILoggerFactory` in handler, the handler should be defined in place where `ILoggerFactory` instance is available).
2. It's not clear in what specific period of application runtime the handler should be attached to kernel. Also, it's not clear if developer needs to detach it at some point.
3. Mechanism of events and event handlers in .NET may not be familiar to .NET developers who didn't work with events previously.

<!-- This is an optional element. Feel free to remove. -->

## Decision Drivers

1. Dependency injection for handlers should be supported to easily access registered services within application.
2. There should not be any limitations where handlers are defined within solution, whether it's Startup.cs or separate file.
3. There should be clear way of registering and removing handlers at specific point of application runtime.
4. The mechanism of receiving and processing events in Kernel should be easy and common in .NET ecosystem.
5. New approach should support the same functionality that is available in Kernel Events - cancel function execution, change kernel arguments, change rendered prompt before sending it to AI etc.

## Decision Outcome

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`

```csharp
public interface IFunctionFilter
{
void OnFunctionInvoking(FunctionInvokingContext context);

void OnFunctionInvoked(FunctionInvokedContext context);
}
```

For prompt-related events: `IPromptFilter`

```csharp
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:

```csharp
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):

```csharp
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:

```csharp
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:

```csharp
kernel.FunctionFilters.Insert(0, new InitialFilter());
kernel.FunctionFilters.RemoveAt(1);
```
4 changes: 2 additions & 2 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@
<PackageVersion Include="xretry" Version="1.9.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<!-- Plugins -->
<PackageVersion Include="DocumentFormat.OpenXml" Version="3.0.0" />
<PackageVersion Include="DocumentFormat.OpenXml" Version="3.0.1" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.0" />
<PackageVersion Include="DuckDB.NET.Data.Full" Version="0.9.2" />
<PackageVersion Include="DuckDB.NET.Data" Version="0.9.2" />
<PackageVersion Include="MongoDB.Driver" Version="2.23.1" />
<PackageVersion Include="Microsoft.Graph" Version="[4.51.0, 5)" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="[3.32.3, )" />
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="[2.28.0, )" />
<PackageVersion Include="Microsoft.OpenApi" Version="1.6.11" />
<PackageVersion Include="Microsoft.OpenApi" Version="1.6.12" />
<PackageVersion Include="Microsoft.OpenApi.Readers" Version="1.6.11" />
<PackageVersion Include="Google.Apis.CustomSearchAPI.v1" Version="[1.60.0.3001, )" />
<PackageVersion Include="Grpc.Net.Client" Version="2.60.0" />
Expand Down
3 changes: 3 additions & 0 deletions dotnet/SK-dotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "System", "System", "{3CDE10B2-AE8F-4FC4-8D55-92D4AD32E144}"
ProjectSection(SolutionItems) = preProject
src\InternalUtilities\src\System\EnvExtensions.cs = src\InternalUtilities\src\System\EnvExtensions.cs
src\InternalUtilities\src\System\InternalTypeConverter.cs = src\InternalUtilities\src\System\InternalTypeConverter.cs
src\InternalUtilities\src\System\TypeConverterFactory.cs = src\InternalUtilities\src\System\TypeConverterFactory.cs
src\InternalUtilities\src\System\NonNullCollection.cs = src\InternalUtilities\src\System\NonNullCollection.cs
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Type", "Type", "{E85EA4D0-BB7E-4DFD-882F-A76EB8C0B8FF}"
Expand Down
2 changes: 1 addition & 1 deletion dotnet/docs/EXPERIMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ You can use the following diagnostic IDs to ignore warnings or errors for a part
- SKEXP0001: Embedding services
- SKEXP0002: Image services
- SKEXP0003: Memory connectors
- SKEXP0004: Kernel Events
- SKEXP0004: Kernel Filters

## OpenAI and Azure OpenAI services

Expand Down
4 changes: 2 additions & 2 deletions dotnet/nuget/nuget-package.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<!-- Central version prefix - applies to all nuget packages. -->
<VersionPrefix>1.1.0</VersionPrefix>
<VersionPrefix>1.2.0</VersionPrefix>

<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix)</PackageVersion>
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)</PackageVersion>
Expand All @@ -10,7 +10,7 @@
<IsPackable>true</IsPackable>

<!-- Package validation. Baseline Version should be lower than current version. -->
<PackageValidationBaselineVersion>1.1.0</PackageValidationBaselineVersion>
<PackageValidationBaselineVersion>1.2.0</PackageValidationBaselineVersion>
<!-- Validate assembly attributes only for Publish builds -->
<NoWarn Condition="'$(Configuration)' != 'Publish'">$(NoWarn);CP0003</NoWarn>
<!-- Do not validate reference assemblies -->
Expand Down
5 changes: 5 additions & 0 deletions dotnet/samples/KernelSyntaxExamples/BaseTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using RepoUtils;
using Xunit.Abstractions;

Expand All @@ -10,9 +11,13 @@ public abstract class BaseTest
{
protected ITestOutputHelper Output { get; }

protected ILoggerFactory LoggerFactory { get; }

protected BaseTest(ITestOutputHelper output)
{
this.Output = output;
this.LoggerFactory = new XunitLogger<object>(output);

LoadUserSecrets();
}

Expand Down
2 changes: 2 additions & 0 deletions dotnet/samples/KernelSyntaxExamples/Example57_KernelHooks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

namespace Examples;

#pragma warning disable CS0618 // Events are deprecated

public class Example57_KernelHooks : BaseTest
{
/// <summary>
Expand Down
Loading

0 comments on commit f5baf9e

Please sign in to comment.