Skip to content

Commit

Permalink
.Net: Add OpenAPI operations filtering samples (#9834)
Browse files Browse the repository at this point in the history
1. Add samples demonstrating the ways OpenAPI operations can be
filtered.
2. Use OpenAIPromptExecutionSettings instead of
AzureOpenAIPromptExecutionSettings with OpenAI connector.
  • Loading branch information
SergeyMenshykh authored Nov 28, 2024
1 parent b9263bb commit 0f61101
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 7 deletions.
192 changes: 192 additions & 0 deletions dotnet/samples/Concepts/Plugins/OpenApiPlugin_Filtering.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Copyright (c) Microsoft. All rights reserved.

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Plugins.OpenApi;

namespace Plugins;

/// <summary>
/// These samples show different ways OpenAPI operations can be filtered out from the OpenAPI document before creating a plugin out of it.
/// </summary>
public sealed class OpenApiPlugin_Filtering : BaseTest
{
private readonly Kernel _kernel;
private readonly ITestOutputHelper _output;

public OpenApiPlugin_Filtering(ITestOutputHelper output) : base(output)
{
IKernelBuilder builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey);

this._kernel = builder.Build();

this._output = output;
}

/// <summary>
/// This sample demonstrates how to filter out specified operations from an OpenAPI plugin based on an exclusion list.
/// In this scenario, only the `listRepairs` operation from the RepairService OpenAPI plugin is allowed to be invoked,
/// while operations such as `createRepair`, `updateRepair`, and `deleteRepair` are excluded.
/// Note: The filtering occurs at the pre-parsing stage, which is more efficient from a resource utilization perspective.
/// </summary>
[Fact]
public async Task ExcludeOperationsBasedOnExclusionListAsync()
{
// The RepairService OpenAPI plugin being imported below includes the following operations: `listRepairs`, `createRepair`, `updateRepair`, and `deleteRepair`.
// However, to meet our business requirements, we need to restrict state-modifying operations such as creating, updating, and deleting repairs, allowing only non-state-modifying operations like listing repairs.
// To enforce this restriction, we will exclude the `createRepair`, `updateRepair`, and `deleteRepair` operations from the OpenAPI document prior to importing the plugin.
OpenApiFunctionExecutionParameters executionParameters = new()
{
OperationsToExclude = ["createRepair", "updateRepair", "deleteRepair"]
};

// Import the RepairService OpenAPI plugin and filter out all operations except `listRepairs` one.
await this._kernel.ImportPluginFromOpenApiAsync(
pluginName: "RepairService",
filePath: "Resources/Plugins/RepairServicePlugin/repair-service.json",
executionParameters: executionParameters);

// Tell the AI model not to call any function and show the list of functions it can call instead.
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.None() };
FunctionResult result = await this._kernel.InvokePromptAsync(promptTemplate: "Show me the list of the functions you can call", arguments: new KernelArguments(settings));

this._output.WriteLine(result);

// The AI model output:
// I can call the following functions in the current context:
// 1. `functions.RepairService - listRepairs`: Returns a list of repairs with their details and images. It takes an optional parameter `assignedTo` to filter the repairs based on the assigned individual.
// I can also utilize the `multi_tool_use.parallel` function to execute multiple tools in parallel if required.
}

/// <summary>
/// This sample demonstrates how to include specified operations from an OpenAPI plugin based on an inclusion list.
/// In this scenario, only the `createRepair` and `updateRepair` operations from the RepairService OpenAPI plugin are allowed to be invoked,
/// while operations such as `listRepairs` and `deleteRepair` are excluded.
/// Note: The filtering occurs at the pre-parsing stage, which is more efficient from a resource utilization perspective.
/// </summary>
[Fact]
public async Task ImportOperationsBasedOnInclusionListAsync()
{
OpenApiDocumentParser parser = new();
using StreamReader reader = System.IO.File.OpenText("Resources/Plugins/RepairServicePlugin/repair-service.json");

// The RepairService OpenAPI plugin, parsed and imported below, has the following operations: `listRepairs`, `createRepair`, `updateRepair`, and `deleteRepair`.
// However, for our business scenario, we only want to permit the AI model to invoke the `createRepair` and `updateRepair` operations, excluding all others.
// To accomplish this, we will define an inclusion list that specifies the allowed operations and filters out the rest.
List<string> operationsToInclude = ["createRepair", "updateRepair"];

// The selection predicate is initialized to evaluate each operation in the OpenAPI document and include only those specified in the inclusion list.
OpenApiDocumentParserOptions parserOptions = new()
{
OperationSelectionPredicate = (OperationSelectionPredicateContext context) => operationsToInclude.Contains(context.Id!)
};

// Parse the OpenAPI document.
RestApiSpecification specification = await parser.ParseAsync(stream: reader.BaseStream, options: parserOptions);

// Import the OpenAPI document specification.
this._kernel.ImportPluginFromOpenApi("RepairService", specification);

// Tell the AI model not to call any function and show the list of functions it can call instead.
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.None() };
FunctionResult result = await this._kernel.InvokePromptAsync(promptTemplate: "Show me the list of the functions you can call", arguments: new KernelArguments(settings));

this._output.WriteLine(result);

// The AI model output:
// Here are the functions I can call for you:
// 1. **RepairService - createRepair **:
// -Adds a new repair to the list with details about the repair.
// 2. **RepairService - updateRepair **:
// -Updates an existing repair in the list with new details.
// If you need to perform any repair - related actions such as creating or updating repair records, feel free to ask!
}

/// <summary>
/// This sample demonstrates how to selectively include certain operations from an OpenAPI plugin based on HTTP method used.
/// In this scenario, only `GET` operations from the RepairService OpenAPI plugin are allowed for invocation,
/// while `POST`, `PUT`, and `DELETE` operations are excluded.
/// Note: The filtering occurs at the pre-parsing stage, which is more efficient from a resource utilization perspective.
/// </summary>
[Fact]
public async Task ImportOperationsBasedOnMethodAsync()
{
OpenApiDocumentParser parser = new();
using StreamReader reader = System.IO.File.OpenText("Resources/Plugins/RepairServicePlugin/repair-service.json");

// The parsed RepairService OpenAPI plugin includes operations such as `listRepairs`, `createRepair`, `updateRepair`, and `deleteRepair`.
// However, for our business requirements, we only permit non-state-modifying operations like listing repairs, excluding all others.
// To achieve this, we set up the selection predicate to evaluate each operation in the OpenAPI document, including only those with the `GET` method.
// Note: The selection predicate can assess operations based on operation ID, method, path, and description.
OpenApiDocumentParserOptions parserOptions = new()
{
OperationSelectionPredicate = (OperationSelectionPredicateContext context) => context.Method == "Get"
};

// Parse the OpenAPI document.
RestApiSpecification specification = await parser.ParseAsync(stream: reader.BaseStream, options: parserOptions);

// Import the OpenAPI document specification.
this._kernel.ImportPluginFromOpenApi("RepairService", specification);

// Tell the AI model not to call any function and show the list of functions it can call instead.
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.None() };
FunctionResult result = await this._kernel.InvokePromptAsync(promptTemplate: "Show me the list of the functions you can call", arguments: new KernelArguments(settings));

this._output.WriteLine(result);

// The AI model output:
// I can call the following function:
// 1. `RepairService - listRepairs`: This function returns a list of repairs with their details and images.
// It can accept an optional parameter `assignedTo` to filter the repairs assigned to a specific person.
}

/// <summary>
/// This example illustrates how to selectively exclude specific operations from an OpenAPI plugin based on the HTTP method used and the presence of a payload.
/// In this context, GET operations that are defined with a payload, which contradicts the HTTP semantic of being idempotent, are not imported.
/// Note: The filtering happens at the post-parsing stage, which is less efficient in terms of resource utilization.
/// </summary>
[Fact]
public async Task FilterOperationsAtPostParsingStageAsync()
{
OpenApiDocumentParser parser = new();
using StreamReader reader = System.IO.File.OpenText("Resources/Plugins/RepairServicePlugin/repair-service.json");

// Parse the OpenAPI document.
RestApiSpecification specification = await parser.ParseAsync(stream: reader.BaseStream);

// The parsed RepairService OpenAPI plugin includes operations like `listRepairs`, `createRepair`, `updateRepair`, and `deleteRepair`.
// However, based on our business requirements, we need to identify all GET operations that are defined as non-idempotent (i.e., have a payload),
// log a warning for each of them, and exclude these operations from the import.
// To do this, we will locate all GET operations that contain a payload.
// Note that the RepairService OpenAPI plugin does not have any GET operations with payloads, so no operations will be found in this case.
// However, the code below demonstrates how to identify and exclude such operations if they were present.
IEnumerable<RestApiOperation> operationsToExclude = specification.Operations.Where(o => o.Method == HttpMethod.Get && o.Payload is not null);

// Exclude operations that are declared as non-idempotent due to having a payload.
foreach (RestApiOperation operation in operationsToExclude)
{
this.Output.WriteLine($"Warning: The `{operation.Id}` operation with `{operation.Method}` has payload which contradicts to being idempotent. This operation will not be imported.");
specification.Operations.Remove(operation);
}

// Import the OpenAPI document specification.
this._kernel.ImportPluginFromOpenApi("RepairService", specification);

// Tell the AI model not to call any function and show the list of functions it can call instead.
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.None() };
FunctionResult result = await this._kernel.InvokePromptAsync(promptTemplate: "Show me the list of the functions you can call", arguments: new KernelArguments(settings));

this._output.WriteLine(result);

// The AI model output:
// I can call the following functions:
// 1. **RepairService - listRepairs **: Returns a list of repairs with their details and images.
// 2. **RepairService - createRepair **: Adds a new repair to the list with the given details and image URL.
// 3. **RepairService - updateRepair **: Updates an existing repair with new details and image URL.
// 4. **RepairService - deleteRepair **: Deletes an existing repair from the list using its ID.
}
}
14 changes: 7 additions & 7 deletions dotnet/samples/Concepts/Plugins/OpenApiPlugin_PayloadHandling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System.Text;
using System.Text.Json;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Plugins.OpenApi;

namespace Plugins;
Expand Down Expand Up @@ -140,7 +140,7 @@ public async Task InvokeOpenApiFunctionWithPayloadProvidedByCallerAsync()
await this._kernel.InvokeAsync(createMeetingFunction, arguments);

// Example of how to have the createEvent function invoked by the AI
AzureOpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
await this._kernel.InvokePromptAsync("Schedule one hour IT Meeting for October 1st, 2023, at 10:00 AM UTC.", new KernelArguments(settings));
}

Expand Down Expand Up @@ -201,7 +201,7 @@ public async Task InvokeOpenApiFunctionWithArgumentsForPayloadLeafPropertiesAsyn
await this._kernel.InvokeAsync(createMeetingFunction, arguments);

// Example of how to have the createEvent function invoked by the AI
AzureOpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
await this._kernel.InvokePromptAsync("Schedule one hour IT Meeting for October 1st, 2023, at 10:00 AM UTC.", new KernelArguments(settings));
}

Expand Down Expand Up @@ -282,7 +282,7 @@ public async Task InvokeOpenApiFunctionWithArgumentsForPayloadLeafPropertiesWith
await this._kernel.InvokeAsync(createMeetingFunction, arguments);

// Example of how to have the createEvent function invoked by the AI
AzureOpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
await this._kernel.InvokePromptAsync("Schedule one hour IT Meeting for October 1st, 2023, at 10:00 AM UTC.", new KernelArguments(settings));
}

Expand All @@ -302,7 +302,7 @@ public async Task InvokeOpenApiFunctionWithArgumentsForPayloadOneOfAsync()
});

// Example of how to have the updatePater function invoked by the AI
AzureOpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
Console.WriteLine("\nExpected payload: Dog { breed=Husky, bark=false }");
await this._kernel.InvokePromptAsync("My new dog is a Husky, he is very quiet, please create my pet information.", new KernelArguments(settings));
Console.WriteLine("\nExpected payload: Dog { breed=Dingo, bark=true }");
Expand Down Expand Up @@ -331,7 +331,7 @@ public async Task InvokeOpenApiFunctionWithArgumentsForPayloadAllOfAsync()
});

// Example of how to have the updatePater function invoked by the AI
AzureOpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
Console.WriteLine("\nExpected payload: { pet_type=dog, breed=Husky, bark=false }");
Console.WriteLine(await this._kernel.InvokePromptAsync("My new dog is a Husky, he is very quiet, please update my pet information.", new KernelArguments(settings)));
Console.WriteLine("\nExpected payload: { pet_type=dog, breed=Dingo, bark=true }");
Expand Down Expand Up @@ -361,7 +361,7 @@ public async Task InvokeOpenApiFunctionWithArgumentsForPayloadAnyOfAsync()
});

// Example of how to have the updatePater function invoked by the AI
AzureOpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
Console.WriteLine("\nExpected payload: { pet_type=Dog, nickname=Fido }");
Console.WriteLine(await this._kernel.InvokePromptAsync("My new dog is named Fido he is 2 years old, please create my pet information.", new KernelArguments(settings)));
Console.WriteLine("\nExpected payload: { pet_type=Dog, nickname=Spot age=1 hunts=true }");
Expand Down
4 changes: 4 additions & 0 deletions dotnet/samples/Concepts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ dotnet test -l "console;verbosity=detailed" --filter "FullyQualifiedName=ChatCom
- [CreatePluginFromOpenApiSpec_Klarna](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/CreatePluginFromOpenApiSpec_Klarna.cs)
- [CreatePluginFromOpenApiSpec_RepairService](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/CreatePluginFromOpenApiSpec_RepairService.cs)
- [OpenApiPlugin_PayloadHandling](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/OpenApiPlugin_PayloadHandling.cs)
- [OpenApiPlugin_CustomHttpContentReader](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/OpenApiPlugin_CustomHttpContentReader.cs)
- [OpenApiPlugin_Customization](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/OpenApiPlugin_Customization.cs)
- [OpenApiPlugin_Filtering](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/OpenApiPlugin_Filtering.cs)
- [OpenApiPlugin_Telemetry](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/OpenApiPlugin_Telemetry.cs)
- [CustomMutablePlugin](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/CustomMutablePlugin.cs)
- [DescribeAllPluginsAndFunctions](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/DescribeAllPluginsAndFunctions.cs)
- [GroundednessChecks](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Plugins/GroundednessChecks.cs)
Expand Down

0 comments on commit 0f61101

Please sign in to comment.