Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 235 additions & 0 deletions dotnet/samples/Concepts/Filtering/ExternalGovernanceCheckpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;

namespace Filtering;

/// <summary>
/// Shows how to place an external governance checkpoint in front of automatic function invocation.
/// </summary>
public class ExternalGovernanceCheckpoint(ITestOutputHelper output) : BaseTest(output)
{
[Theory]
[InlineData("allow", "Executed wire transfer", "executed")]
[InlineData("require_approval", "Paused for approval", "paused")]
public async Task ExternalCheckpointCanAllowOrPauseFunctionInvocationAsync(string requestedVerdict, string expectedResult, string expectedStatus)
{
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton<IExternalCheckpointClient>(new ExampleCheckpointClient(requestedVerdict));
builder.Services.AddSingleton<IAutoFunctionInvocationFilter, ExternalGovernanceFilter>();

var kernel = builder.Build();
var function = KernelFunctionFactory.CreateFromMethod(
(decimal amount, string recipient) => "Executed wire transfer",
"WireTransfer");

kernel.ImportPluginFromFunctions("Payments", [function]);
KernelFunction importedFunction = kernel.Plugins.GetFunction("Payments", "WireTransfer");

var context = CreateAutoFunctionInvocationContext(
kernel,
importedFunction,
new KernelArguments
{
["amount"] = 1250m,
["recipient"] = "Fabrikam"
});

var filter = kernel.Services.GetRequiredService<IAutoFunctionInvocationFilter>();
await filter.OnAutoFunctionInvocationAsync(context, async invocationContext =>
{
invocationContext.Result = await invocationContext.Function.InvokeAsync(kernel, invocationContext.Arguments);
});

Console.WriteLine(context.Result);
Assert.Equal(expectedResult, context.Result.GetValue<string>());
Assert.Equal(expectedStatus, context.Result.Metadata?["governance_status"]);

// Output for allow:
// Executed wire transfer
//
// Output for require_approval:
// Paused for approval
}

[Fact]
public async Task ExternalCheckpointCanDenyFunctionInvocationAsync()
{
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton<IExternalCheckpointClient>(new ExampleCheckpointClient("deny"));
builder.Services.AddSingleton<IAutoFunctionInvocationFilter, ExternalGovernanceFilter>();

var kernel = builder.Build();
var function = KernelFunctionFactory.CreateFromMethod(() => "Deleted customer record", "DeleteCustomerRecord");

kernel.ImportPluginFromFunctions("CustomerAdmin", [function]);
KernelFunction importedFunction = kernel.Plugins.GetFunction("CustomerAdmin", "DeleteCustomerRecord");

var context = CreateAutoFunctionInvocationContext(kernel, importedFunction, new KernelArguments());
var filter = kernel.Services.GetRequiredService<IAutoFunctionInvocationFilter>();

var exception = await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
filter.OnAutoFunctionInvocationAsync(context, _ => throw new InvalidOperationException("The function should not execute.")));

Assert.Contains("denied", exception.Message, StringComparison.OrdinalIgnoreCase);
}

private static AutoFunctionInvocationContext CreateAutoFunctionInvocationContext(
Kernel kernel,
KernelFunction function,
KernelArguments arguments)
{
var chatHistory = new ChatHistory("Transfer $1,250 to Fabrikam.");
var functionCall = new FunctionCallContent(
functionName: function.Name,
pluginName: function.PluginName,
id: "call_123",
arguments: arguments);

var chatMessageContent = new ChatMessageContent(AuthorRole.Assistant, [functionCall]);
chatHistory.Add(chatMessageContent);

return new AutoFunctionInvocationContext(
kernel,
function,
new FunctionResult(function),
chatHistory,
chatMessageContent)
{
Arguments = arguments,
RequestSequenceIndex = 0,
FunctionSequenceIndex = 0,
ToolCallId = functionCall.Id
};
}

private sealed class ExternalGovernanceFilter(IExternalCheckpointClient checkpointClient) : IAutoFunctionInvocationFilter
{
public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next)
{
ActionEnvelope envelope = ActionEnvelope.FromContext(context);
string checkpointReference = ActionEnvelopeDigest.ComputeReference(envelope);

CheckpointVerdict verdict = await checkpointClient.EvaluateAsync(envelope, checkpointReference, context.CancellationToken);

switch (verdict.Decision)
{
case "allow":
await next(context);
context.Result = WithGovernanceMetadata(context.Result, checkpointReference, "executed");
return;

case "require_approval":
context.Result = WithGovernanceMetadata(
context.Result,
checkpointReference,
"paused",
"Paused for approval");
context.Terminate = true;
return;

case "deny":
throw new UnauthorizedAccessException(
$"Function call '{envelope.PluginName ?? "<none>"}.{envelope.FunctionName}' was denied by checkpoint {checkpointReference}.");

default:
throw new InvalidOperationException($"Unknown checkpoint verdict '{verdict.Decision}'.");
}
}

private static FunctionResult WithGovernanceMetadata(
FunctionResult result,
string checkpointReference,
string status,
string? value = null)
{
Dictionary<string, object?> metadata = result.Metadata is not null ? new(result.Metadata) : [];
metadata["governance_checkpoint"] = checkpointReference;
metadata["governance_status"] = status;

return new FunctionResult(result, value)
{
Metadata = metadata
};
}
}

private sealed record ActionEnvelope(
string? PluginName,
string FunctionName,
IReadOnlyDictionary<string, object?> Arguments,
int RequestSequenceIndex,
int FunctionSequenceIndex,
string? ToolCallId)
{
public static ActionEnvelope FromContext(AutoFunctionInvocationContext context)
{
SortedDictionary<string, object?> arguments = new(StringComparer.Ordinal);

if (context.Arguments is not null)
{
foreach (var argument in context.Arguments.OrderBy(static item => item.Key, StringComparer.Ordinal))
{
arguments[argument.Key] = argument.Value;
}
}

return new(
context.Function.PluginName,
context.Function.Name,
arguments,
context.RequestSequenceIndex,
context.FunctionSequenceIndex,
context.ToolCallId);
}
}

private static class ActionEnvelopeDigest
{
private static readonly JsonSerializerOptions s_serializerOptions = new(JsonSerializerDefaults.Web);

public static string ComputeReference(ActionEnvelope envelope)
{
StableActionEnvelope stableEnvelope = new(
envelope.PluginName,
envelope.FunctionName,
envelope.Arguments);

byte[] envelopeBytes = JsonSerializer.SerializeToUtf8Bytes(stableEnvelope, s_serializerOptions);
byte[] digest = SHA256.HashData(envelopeBytes);

return $"sha256:{Convert.ToHexString(digest).ToLowerInvariant()}";
}

private sealed record StableActionEnvelope(
string? PluginName,
string FunctionName,
IReadOnlyDictionary<string, object?> Arguments);
}

private interface IExternalCheckpointClient
{
Task<CheckpointVerdict> EvaluateAsync(ActionEnvelope envelope, string checkpointReference, CancellationToken cancellationToken);
}

private sealed class ExampleCheckpointClient(string decision) : IExternalCheckpointClient
{
public Task<CheckpointVerdict> EvaluateAsync(
ActionEnvelope envelope,
string checkpointReference,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

Console.WriteLine($"Checkpoint {checkpointReference}: {decision} {envelope.PluginName ?? "<none>"}.{envelope.FunctionName}");

return Task.FromResult(new CheckpointVerdict(decision));
}
}

private sealed record CheckpointVerdict(string Decision);
}
1 change: 1 addition & 0 deletions dotnet/samples/Concepts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ dotnet test -l "console;verbosity=detailed" --filter "FullyQualifiedName=ChatCom

- [AutoFunctionInvocationFiltering](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Filtering/AutoFunctionInvocationFiltering.cs)
- [FunctionInvocationFiltering](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Filtering/FunctionInvocationFiltering.cs)
- [ExternalGovernanceCheckpoint](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Filtering/ExternalGovernanceCheckpoint.cs)
- [MaxTokensWithFilters](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Filtering/MaxTokensWithFilters.cs)
- [PIIDetection](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Filtering/PIIDetection.cs)
- [PromptRenderFiltering](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Filtering/PromptRenderFiltering.cs)
Expand Down
Loading