From d6605d71a471f8675db4a58fb651dfafae377da8 Mon Sep 17 00:00:00 2001
From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
Date: Thu, 14 Nov 2024 16:31:37 +0000
Subject: [PATCH] .Net: Mechanism for transforming OpenAPI documents - part2
(#9689)
### Motivation and Context
This is the second PR to add a mechanism to transform OpenAPI documents
before creating a kernel plugin from them.
### Description
This PR:
1. Makes the `OpenApiDocumentParser` public, allowing for the parsing of
OpenAPI documents and accessing the instance of the
`RestApiSpecification` class representing the parsed document, which can
be modified, by the consumer, if needed, before creating a plugin from
it. Currently, it's only possible to modify argument name property of
parameters and server variables.
2. Adds a few kernel extension overload methods to create and import
OpenAPI document represented by the `RestApiSpecification` model class.
This is the final element of the transformation mechanism that receives
the specification model class instance returned by the parser and
transformed by the consumer and creates an SK plugin from it.
3. Adds the `OpenApiDocumentParserOptions` class to represent existing
parser options and allows adding new ones with no breaking changes.
4. Replaces the `operationsToExclude` exclusion list, which is limited
to filtering out operations by operation id, with the
`OperationSelectionPredicate` callback that can filter out operations
based on id, method, path, and description.
5. Removes the **internal** `IOpenApiDocumentParser` unneeded interface
that was not used all that time.
6. Other: XML comments + OpenAPI specification transformation sample
Contributes to the issue:
https://github.com/microsoft/semantic-kernel/issues/4666
The first PR: https://github.com/microsoft/semantic-kernel/pull/9668
---
dotnet/samples/Concepts/Concepts.csproj | 5 +
...eatePluginFromOpenApiSpec_RepairService.cs | 2 +-
.../Plugins/OpenApiPlugin_Customization.cs | 137 ++++++++++++++++++
.../Plugins/ProductsPlugin/openapi.json | 62 ++++++++
.../OpenApiFunctionExecutionParameters.cs | 7 +-
.../Extensions/OpenApiKernelExtensions.cs | 117 ++++++++++-----
.../Model/RestApiSpecification.cs | 6 +-
.../OpenApi/IOpenApiDocumentParser.cs | 30 ----
.../OpenApi/OpenApiDocumentParser.cs | 33 +++--
.../OpenApi/OpenApiDocumentParserOptions.cs | 26 ++++
.../OperationSelectionPredicateContext.cs | 80 ++++++++++
.../OpenApiKernelPluginFactory.cs | 101 ++++++++++---
.../OpenApiKernelExtensionsTests.cs | 84 +++++++++++
.../OpenApi/OpenApiDocumentParserV20Tests.cs | 21 +++
.../OpenApi/OpenApiDocumentParserV30Tests.cs | 2 +-
.../OpenApiKernelPluginFactoryTests.cs | 41 ++++++
...OperationSelectionPredicateContextTests.cs | 53 +++++++
.../Plugins/OpenApi/RepairServiceTests.cs | 11 +-
18 files changed, 703 insertions(+), 115 deletions(-)
create mode 100644 dotnet/samples/Concepts/Plugins/OpenApiPlugin_Customization.cs
create mode 100644 dotnet/samples/Concepts/Resources/Plugins/ProductsPlugin/openapi.json
delete mode 100644 dotnet/src/Functions/Functions.OpenApi/OpenApi/IOpenApiDocumentParser.cs
create mode 100644 dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParserOptions.cs
create mode 100644 dotnet/src/Functions/Functions.OpenApi/OpenApi/OperationSelectionPredicateContext.cs
create mode 100644 dotnet/src/Functions/Functions.UnitTests/OpenApi/OperationSelectionPredicateContextTests.cs
diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj
index 942a3eca849b..941b3fa68c6e 100644
--- a/dotnet/samples/Concepts/Concepts.csproj
+++ b/dotnet/samples/Concepts/Concepts.csproj
@@ -128,4 +128,9 @@
+
+
+ Always
+
+
diff --git a/dotnet/samples/Concepts/Plugins/CreatePluginFromOpenApiSpec_RepairService.cs b/dotnet/samples/Concepts/Plugins/CreatePluginFromOpenApiSpec_RepairService.cs
index 040b9045303d..dcdb47823653 100644
--- a/dotnet/samples/Concepts/Plugins/CreatePluginFromOpenApiSpec_RepairService.cs
+++ b/dotnet/samples/Concepts/Plugins/CreatePluginFromOpenApiSpec_RepairService.cs
@@ -36,7 +36,7 @@ public async Task ShowCreatingRepairServicePluginAsync()
Console.WriteLine(result.ToString());
// List All Repairs
- result = await plugin["listRepairs"].InvokeAsync(kernel, arguments);
+ result = await plugin["listRepairs"].InvokeAsync(kernel);
var repairs = JsonSerializer.Deserialize(result.ToString());
Assert.True(repairs?.Length > 0);
var id = repairs[repairs.Length - 1].Id;
diff --git a/dotnet/samples/Concepts/Plugins/OpenApiPlugin_Customization.cs b/dotnet/samples/Concepts/Plugins/OpenApiPlugin_Customization.cs
new file mode 100644
index 000000000000..19b14f58f7b9
--- /dev/null
+++ b/dotnet/samples/Concepts/Plugins/OpenApiPlugin_Customization.cs
@@ -0,0 +1,137 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Plugins.OpenApi;
+
+namespace Plugins;
+
+///
+/// These samples show different ways OpenAPI document can be transformed to change its various aspects before creating a plugin out of it.
+/// The transformations can be useful if the original OpenAPI document can't be consumed as is.
+///
+public sealed class OpenApiPlugin_Customization : BaseTest
+{
+ private readonly Kernel _kernel;
+ private readonly ITestOutputHelper _output;
+ private readonly HttpClient _httpClient;
+
+ public OpenApiPlugin_Customization(ITestOutputHelper output) : base(output)
+ {
+ IKernelBuilder builder = Kernel.CreateBuilder();
+
+ this._kernel = builder.Build();
+
+ this._output = output;
+
+ void RequestDataHandler(string requestData)
+ {
+ this._output.WriteLine("Request payload");
+ this._output.WriteLine(requestData);
+ }
+
+ // Create HTTP client with a stub handler to log the request data
+ this._httpClient = new(new StubHttpHandler(RequestDataHandler));
+ }
+
+ ///
+ /// This sample demonstrates how to assign argument names to parameters and variables that have the same name.
+ /// For example, in this sample, there are multiple parameters named 'id' in the 'getProductFromCart' operation.
+ /// * Region of the API in the server variable.
+ /// * User ID in the path.
+ /// * Subscription ID in the query string.
+ /// * Session ID in the header.
+ ///
+ [Fact]
+ public async Task HandleOpenApiDocumentHavingTwoParametersWithSameNameButRelatedToDifferentEntitiesAsync()
+ {
+ OpenApiDocumentParser parser = new();
+
+ using StreamReader sr = File.OpenText("Resources/Plugins/ProductsPlugin/openapi.json");
+
+ // Register the custom HTTP client with the stub handler
+ OpenApiFunctionExecutionParameters executionParameters = new() { HttpClient = this._httpClient };
+
+ // Parse the OpenAPI document
+ RestApiSpecification specification = await parser.ParseAsync(sr.BaseStream);
+
+ // Get the 'getProductFromCart' operation
+ RestApiOperation getProductFromCartOperation = specification.Operations.Single(o => o.Id == "getProductFromCart");
+
+ // Set the 'region' argument name to the 'id' server variable that represents the region of the API
+ RestApiServerVariable idServerVariable = getProductFromCartOperation.Servers[0].Variables["id"];
+ idServerVariable.ArgumentName = "region";
+
+ // Set the 'userId' argument name to the 'id' path parameter that represents the user ID
+ RestApiParameter idPathParameter = getProductFromCartOperation.Parameters.Single(p => p.Location == RestApiParameterLocation.Path && p.Name == "id");
+ idPathParameter.ArgumentName = "userId";
+
+ // Set the 'subscriptionId' argument name to the 'id' query string parameter that represents the subscription ID
+ RestApiParameter idQueryStringParameter = getProductFromCartOperation.Parameters.Single(p => p.Location == RestApiParameterLocation.Query && p.Name == "id");
+ idQueryStringParameter.ArgumentName = "subscriptionId";
+
+ // Set the 'sessionId' argument name to the 'id' header parameter that represents the session ID
+ RestApiParameter sessionIdHeaderParameter = getProductFromCartOperation.Parameters.Single(p => p.Location == RestApiParameterLocation.Header && p.Name == "id");
+ sessionIdHeaderParameter.ArgumentName = "sessionId";
+
+ // Import the transformed OpenAPI plugin specification
+ KernelPlugin plugin = this._kernel.ImportPluginFromOpenApi("Products_Plugin", specification, new OpenApiFunctionExecutionParameters(this._httpClient));
+
+ // Create arguments for the 'addProductToCart' operation using the new argument names defined earlier.
+ // Internally these will be mapped to the correct entity when invoking the Open API endpoint.
+ KernelArguments arguments = new()
+ {
+ ["region"] = "en",
+ ["subscriptionId"] = "subscription-12345",
+ ["userId"] = "user-12345",
+ ["sessionId"] = "session-12345",
+ };
+
+ // Invoke the 'addProductToCart' function
+ await this._kernel.InvokeAsync(plugin["getProductFromCart"], arguments);
+
+ // The REST API request details
+ // {
+ // "RequestUri": "https://api.example.com:443/eu/users/user-12345/cart?id=subscription-12345",
+ // "Method": "Get",
+ // "Headers": {
+ // "id": ["session-12345"]
+ // }
+ // }
+ }
+
+ private sealed class StubHttpHandler : DelegatingHandler
+ {
+ private readonly Action _requestHandler;
+ private readonly JsonSerializerOptions _options;
+
+ public StubHttpHandler(Action requestHandler) : base()
+ {
+ this._requestHandler = requestHandler;
+ this._options = new JsonSerializerOptions { WriteIndented = true };
+ }
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ var requestData = new Dictionary
+ {
+ { "RequestUri", request.RequestUri! },
+ { "Method", request.Method },
+ { "Headers", request.Headers.ToDictionary(h => h.Key, h => h.Value) },
+ };
+
+ this._requestHandler(JsonSerializer.Serialize(requestData, this._options));
+
+ return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
+ {
+ Content = new StringContent("Success", System.Text.Encoding.UTF8, "application/json")
+ };
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ this._httpClient.Dispose();
+ }
+}
diff --git a/dotnet/samples/Concepts/Resources/Plugins/ProductsPlugin/openapi.json b/dotnet/samples/Concepts/Resources/Plugins/ProductsPlugin/openapi.json
new file mode 100644
index 000000000000..be083e223a0c
--- /dev/null
+++ b/dotnet/samples/Concepts/Resources/Plugins/ProductsPlugin/openapi.json
@@ -0,0 +1,62 @@
+{
+ "openapi": "3.0.1",
+ "info": {
+ "title": "User Product API",
+ "version": "1.0.0",
+ "description": "API for managing products associated with users"
+ },
+ "servers": [
+ {
+ "url": "https://api.example.com/{id}",
+ "variables": {
+ "id": {
+ "default": "eu",
+ "description": "Server variable representing the region of the API (e.g., 'us' for United States, 'eu' for Europe)"
+ }
+ }
+ }
+ ],
+ "paths": {
+ "/users/{id}/cart": {
+ "get": {
+ "operationId": "getProductFromCart",
+ "summary": "Retrieve a user's cart",
+ "description": "Retrieve the contents of the cart for the user ID provided in the query parameter.",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "query",
+ "required": true,
+ "description": "The ID of the subscription to retrieve products from",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "description": "The ID of the user whose cart is being retrieved (query parameter)",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "id",
+ "in": "header",
+ "required": true,
+ "description": "The ID representing the session (header parameter)",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successfully retrieved the user's cart"
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnet/src/Functions/Functions.OpenApi/Extensions/OpenApiFunctionExecutionParameters.cs b/dotnet/src/Functions/Functions.OpenApi/Extensions/OpenApiFunctionExecutionParameters.cs
index f5a6ee3519bb..e6b653309271 100644
--- a/dotnet/src/Functions/Functions.OpenApi/Extensions/OpenApiFunctionExecutionParameters.cs
+++ b/dotnet/src/Functions/Functions.OpenApi/Extensions/OpenApiFunctionExecutionParameters.cs
@@ -41,16 +41,19 @@ public class OpenApiFunctionExecutionParameters
///
/// Determines whether the REST API operation payload is constructed dynamically based on payload metadata.
- /// If false, the payload must be provided via the 'payload' argument.
+ /// It's enabled by default and allows to support operations with simple payload structure - no properties with the same name at different levels.
+ /// To support more complex payloads, it should be disabled and the payload should be provided via the 'payload' argument.
+ /// See the 'Providing Payload for OpenAPI Functions' ADR for more details: https://github.com/microsoft/semantic-kernel/blob/main/docs/decisions/0062-open-api-payload.md
///
public bool EnableDynamicPayload { get; set; }
///
- /// Determines whether payload parameter names are augmented with namespaces.
+ /// Determines whether payload parameter names are augmented with namespaces. It's only applicable when EnableDynamicPayload property is set to true.
/// Namespaces prevent naming conflicts by adding the parent parameter name as a prefix, separated by dots.
/// For instance, without namespaces, the 'email' parameter for both the 'sender' and 'receiver' parent parameters
/// would be resolved from the same 'email' argument, which is incorrect. However, by employing namespaces,
/// the parameters 'sender.email' and 'sender.receiver' will be correctly resolved from arguments with the same names.
+ /// See the 'Providing Payload for OpenAPI Functions' ADR for more details: https://github.com/microsoft/semantic-kernel/blob/main/docs/decisions/0062-open-api-payload.md
///
public bool EnablePayloadNamespacing { get; set; }
diff --git a/dotnet/src/Functions/Functions.OpenApi/Extensions/OpenApiKernelExtensions.cs b/dotnet/src/Functions/Functions.OpenApi/Extensions/OpenApiKernelExtensions.cs
index 3424d2b6cadb..a162fdda5c7b 100644
--- a/dotnet/src/Functions/Functions.OpenApi/Extensions/OpenApiKernelExtensions.cs
+++ b/dotnet/src/Functions/Functions.OpenApi/Extensions/OpenApiKernelExtensions.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Http;
using System.Threading;
@@ -14,21 +15,19 @@
namespace Microsoft.SemanticKernel;
///
-/// Provides extension methods for importing plugins exposed as OpenAPI specifications.
+/// Extension methods for to create and import plugins from OpenAPI specifications.
///
public static class OpenApiKernelExtensions
{
- // TODO: Revise XML comments
-
///
- /// Creates a plugin from an OpenAPI specification and adds it to the kernel's plugins collection.
+ /// Creates from an OpenAPI specification and adds it to .
///
/// The containing services, plugins, and other state for use throughout the operation.
- /// Plugin name.
- /// The file path to the AI Plugin
- /// Plugin execution parameters.
+ /// The plugin name.
+ /// The file path to the OpenAPI specification.
+ /// The OpenAPI specification parsing and function execution parameters.
/// The cancellation token.
- /// A collection of invocable functions
+ /// A instance that contains functions corresponding to the operations defined in the OpenAPI specification.
public static async Task ImportPluginFromOpenApiAsync(
this Kernel kernel,
string pluginName,
@@ -42,14 +41,14 @@ public static async Task ImportPluginFromOpenApiAsync(
}
///
- /// Creates a plugin from an OpenAPI specification and adds it to the kernel's plugins collection.
+ /// Creates from an OpenAPI specification and adds it to .
///
/// The containing services, plugins, and other state for use throughout the operation.
- /// Plugin name.
- /// A local or remote URI referencing the AI Plugin
- /// Plugin execution parameters.
+ /// The plugin name.
+ /// A URI referencing the OpenAPI specification.
+ /// The OpenAPI specification parsing and function execution parameters.
/// The cancellation token.
- /// A collection of invocable functions
+ /// A instance that contains functions corresponding to the operations defined in the OpenAPI specification.
public static async Task ImportPluginFromOpenApiAsync(
this Kernel kernel,
string pluginName,
@@ -63,14 +62,14 @@ public static async Task ImportPluginFromOpenApiAsync(
}
///
- /// Creates a plugin from an OpenAPI specification and adds it to the kernel's plugins collection.
+ /// Creates from an OpenAPI specification and adds it to .
///
/// The containing services, plugins, and other state for use throughout the operation.
- /// Plugin name.
- /// A stream representing the AI Plugin
- /// Plugin execution parameters.
+ /// The plugin name.
+ /// A stream representing the OpenAPI specification.
+ /// The OpenAPI specification parsing and function execution parameters.
/// The cancellation token.
- /// A collection of invocable functions
+ /// A instance that contains functions corresponding to the operations defined in the OpenAPI specification.
public static async Task ImportPluginFromOpenApiAsync(
this Kernel kernel,
string pluginName,
@@ -84,14 +83,34 @@ public static async Task ImportPluginFromOpenApiAsync(
}
///
- /// Creates a plugin from an OpenAPI specification.
+ /// Creates from an OpenAPI specification and adds it to .
///
/// The containing services, plugins, and other state for use throughout the operation.
- /// Plugin name.
- /// The file path to the AI Plugin
- /// Plugin execution parameters.
+ /// The plugin name.
+ /// The specification model.
+ /// The OpenAPI specification parsing and function execution parameters.
+ /// A instance that contains functions corresponding to the operations defined in the OpenAPI specification.
+ [Experimental("SKEXP0040")]
+ public static KernelPlugin ImportPluginFromOpenApi(
+ this Kernel kernel,
+ string pluginName,
+ RestApiSpecification specification,
+ OpenApiFunctionExecutionParameters? executionParameters = null)
+ {
+ KernelPlugin plugin = kernel.CreatePluginFromOpenApi(pluginName, specification, executionParameters);
+ kernel.Plugins.Add(plugin);
+ return plugin;
+ }
+
+ ///
+ /// Creates from an OpenAPI specification.
+ ///
+ /// The containing services, plugins, and other state for use throughout the operation.
+ /// The plugin name.
+ /// The file path to the OpenAPI specification.
+ /// The OpenAPI specification parsing and function execution parameters.
/// The cancellation token.
- /// A collection of invocable functions
+ /// A instance that contains functions corresponding to the operations defined in the OpenAPI specification.
public static async Task CreatePluginFromOpenApiAsync(
this Kernel kernel,
string pluginName,
@@ -121,14 +140,14 @@ public static async Task CreatePluginFromOpenApiAsync(
}
///
- /// Creates a plugin from an OpenAPI specification.
+ /// Creates from an OpenAPI specification.
///
/// The containing services, plugins, and other state for use throughout the operation.
- /// Plugin name.
- /// A local or remote URI referencing the AI Plugin
- /// Plugin execution parameters.
+ /// The plugin name.
+ /// A URI referencing the OpenAPI specification.
+ /// The OpenAPI specification parsing and function execution parameters.
/// The cancellation token.
- /// A collection of invocable functions
+ /// A instance that contains functions corresponding to the operations defined in the OpenAPI specification.
public static async Task CreatePluginFromOpenApiAsync(
this Kernel kernel,
string pluginName,
@@ -162,14 +181,14 @@ public static async Task CreatePluginFromOpenApiAsync(
}
///
- /// Creates a plugin from an OpenAPI specification.
+ /// Creates from an OpenAPI specification.
///
/// The containing services, plugins, and other state for use throughout the operation.
- /// Plugin name.
- /// A stream representing the AI Plugin
- /// Plugin execution parameters.
+ /// The plugin name.
+ /// A stream representing the OpenAPI specification.
+ /// The OpenAPI specification parsing and function execution parameters.
/// The cancellation token.
- /// A collection of invocable functions
+ /// A instance that contains functions corresponding to the operations defined in the OpenAPI specification.
public static async Task CreatePluginFromOpenApiAsync(
this Kernel kernel,
string pluginName,
@@ -195,6 +214,38 @@ public static async Task CreatePluginFromOpenApiAsync(
cancellationToken: cancellationToken).ConfigureAwait(false);
}
+ ///
+ /// Creates from an OpenAPI specification.
+ ///
+ /// The containing services, plugins, and other state for use throughout the operation.
+ /// The plugin name.
+ /// The specification model.
+ /// The OpenAPI specification parsing and function execution parameters.
+ /// The cancellation token.
+ /// A instance that contains functions corresponding to the operations defined in the OpenAPI specification.
+ [Experimental("SKEXP0040")]
+ public static KernelPlugin CreatePluginFromOpenApi(
+ this Kernel kernel,
+ string pluginName,
+ RestApiSpecification specification,
+ OpenApiFunctionExecutionParameters? executionParameters = null,
+ CancellationToken cancellationToken = default)
+ {
+ Verify.NotNull(kernel);
+ Verify.ValidPluginName(pluginName, kernel.Plugins);
+
+#pragma warning disable CA2000 // Dispose objects before losing scope. No need to dispose the Http client here. It can either be an internal client using NonDisposableHttpClientHandler or an external client managed by the calling code, which should handle its disposal.
+ var httpClient = HttpClientProvider.GetHttpClient(executionParameters?.HttpClient ?? kernel.Services.GetService());
+#pragma warning restore CA2000
+
+ return OpenApiKernelPluginFactory.CreateOpenApiPlugin(
+ pluginName: pluginName,
+ executionParameters: executionParameters,
+ httpClient: httpClient,
+ specification: specification,
+ loggerFactory: kernel.LoggerFactory);
+ }
+
#region private
private static async Task CreateOpenApiPluginAsync(
diff --git a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiSpecification.cs b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiSpecification.cs
index b86661aa7512..ae6f6b9ff901 100644
--- a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiSpecification.cs
+++ b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiSpecification.cs
@@ -1,13 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
namespace Microsoft.SemanticKernel.Plugins.OpenApi;
///
/// REST API specification.
///
-internal sealed class RestApiSpecification
+[Experimental("SKEXP0040")]
+public sealed class RestApiSpecification
{
///
/// The REST API information.
@@ -30,7 +32,7 @@ internal sealed class RestApiSpecification
/// REST API information.
/// REST API security requirements.
/// REST API operations.
- public RestApiSpecification(RestApiInfo info, List? securityRequirements, IList operations)
+ internal RestApiSpecification(RestApiInfo info, List? securityRequirements, IList operations)
{
this.Info = info;
this.SecurityRequirements = securityRequirements;
diff --git a/dotnet/src/Functions/Functions.OpenApi/OpenApi/IOpenApiDocumentParser.cs b/dotnet/src/Functions/Functions.OpenApi/OpenApi/IOpenApiDocumentParser.cs
deleted file mode 100644
index 104e50d8895f..000000000000
--- a/dotnet/src/Functions/Functions.OpenApi/OpenApi/IOpenApiDocumentParser.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.SemanticKernel.Plugins.OpenApi;
-
-///
-/// Interface for OpenAPI document parser classes.
-///
-internal interface IOpenApiDocumentParser
-{
- ///
- /// Parses OpenAPI document.
- ///
- /// Stream containing OpenAPI document to parse.
- /// Flag indicating whether to ignore non-compliant errors.
- /// If set to true, the parser will not throw exceptions for non-compliant documents.
- /// Please note that enabling this option may result in incomplete or inaccurate parsing results.
- /// Optional list of operations not to import, e.g. in case they are not supported
- /// The cancellation token.
- /// Specification of the REST API.
- Task ParseAsync(
- Stream stream,
- bool ignoreNonCompliantErrors = false,
- IList? operationsToExclude = null,
- CancellationToken cancellationToken = default);
-}
diff --git a/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs b/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs
index 0f0da229afa5..a1f9d81a2782 100644
--- a/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs
+++ b/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs
@@ -26,14 +26,16 @@ namespace Microsoft.SemanticKernel.Plugins.OpenApi;
///
/// Parser for OpenAPI documents.
///
-internal sealed class OpenApiDocumentParser(ILoggerFactory? loggerFactory = null) : IOpenApiDocumentParser
+public sealed class OpenApiDocumentParser(ILoggerFactory? loggerFactory = null)
{
- ///
- public async Task ParseAsync(
- Stream stream,
- bool ignoreNonCompliantErrors = false,
- IList? operationsToExclude = null,
- CancellationToken cancellationToken = default)
+ ///
+ /// Parses OpenAPI document.
+ ///
+ /// Stream containing OpenAPI document to parse.
+ /// Options for parsing OpenAPI document.
+ /// The cancellation token.
+ /// Specification of the REST API.
+ public async Task ParseAsync(Stream stream, OpenApiDocumentParserOptions? options = null, CancellationToken cancellationToken = default)
{
var jsonObject = await this.DowngradeDocumentVersionToSupportedOneAsync(stream, cancellationToken).ConfigureAwait(false);
@@ -41,12 +43,12 @@ public async Task ParseAsync(
var result = await this._openApiReader.ReadAsync(memoryStream, cancellationToken).ConfigureAwait(false);
- this.AssertReadingSuccessful(result, ignoreNonCompliantErrors);
+ this.AssertReadingSuccessful(result, options?.IgnoreNonCompliantErrors ?? false);
return new(
ExtractRestApiInfo(result.OpenApiDocument),
CreateRestApiOperationSecurityRequirements(result.OpenApiDocument.SecurityRequirements),
- ExtractRestApiOperations(result.OpenApiDocument, operationsToExclude, this._logger));
+ ExtractRestApiOperations(result.OpenApiDocument, options, this._logger));
}
#region private
@@ -155,16 +157,16 @@ internal static RestApiInfo ExtractRestApiInfo(OpenApiDocument document)
/// Parses an OpenAPI document and extracts REST API operations.
///
/// The OpenAPI document.
- /// Optional list of operations not to import, e.g. in case they are not supported
+ /// Options for parsing OpenAPI document.
/// Used to perform logging.
/// List of Rest operations.
- private static List ExtractRestApiOperations(OpenApiDocument document, IList? operationsToExclude, ILogger logger)
+ private static List ExtractRestApiOperations(OpenApiDocument document, OpenApiDocumentParserOptions? options, ILogger logger)
{
var result = new List();
foreach (var pathPair in document.Paths)
{
- var operations = CreateRestApiOperations(document, pathPair.Key, pathPair.Value, operationsToExclude, logger);
+ var operations = CreateRestApiOperations(document, pathPair.Key, pathPair.Value, options, logger);
result.AddRange(operations);
}
@@ -177,10 +179,10 @@ private static List ExtractRestApiOperations(OpenApiDocument d
/// The OpenAPI document.
/// Rest resource path.
/// Rest resource metadata.
- /// Optional list of operations not to import, e.g. in case they are not supported
+ /// Options for parsing OpenAPI document.
/// Used to perform logging.
/// Rest operation.
- internal static List CreateRestApiOperations(OpenApiDocument document, string path, OpenApiPathItem pathItem, IList? operationsToExclude, ILogger logger)
+ internal static List CreateRestApiOperations(OpenApiDocument document, string path, OpenApiPathItem pathItem, OpenApiDocumentParserOptions? options, ILogger logger)
{
var operations = new List();
@@ -190,7 +192,8 @@ internal static List CreateRestApiOperations(OpenApiDocument d
var operationItem = operationPair.Value;
- if (operationsToExclude is not null && operationsToExclude.Contains(operationItem.OperationId, StringComparer.OrdinalIgnoreCase))
+ // Skip the operation parsing and don't add it to the result operations list if it's explicitly excluded by the predicate.
+ if (!options?.OperationSelectionPredicate?.Invoke(new OperationSelectionPredicateContext(operationItem.OperationId, path, method, operationItem.Description)) ?? false)
{
continue;
}
diff --git a/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParserOptions.cs b/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParserOptions.cs
new file mode 100644
index 000000000000..a59b61257e4b
--- /dev/null
+++ b/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParserOptions.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+
+namespace Microsoft.SemanticKernel.Plugins.OpenApi;
+
+///
+/// Options for OpenAPI document parser.
+///
+public sealed class OpenApiDocumentParserOptions
+{
+ ///
+ /// Flag indicating whether to ignore non-compliant errors in the OpenAPI document during parsing.
+ /// If set to true, the parser will not throw exceptions for non-compliant documents.
+ /// Please note that enabling this option may result in incomplete or inaccurate parsing results.
+ ///
+ public bool IgnoreNonCompliantErrors { set; get; } = false;
+
+ ///
+ /// Operation selection predicate to apply to all OpenAPI document operations.
+ /// If set, the predicate will be applied to each operation in the document.
+ /// If the predicate returns true, the operation will be parsed; otherwise, it will be skipped.
+ /// This can be used to filter out operations that should not be imported for various reasons.
+ ///
+ public Func? OperationSelectionPredicate { get; set; }
+}
diff --git a/dotnet/src/Functions/Functions.OpenApi/OpenApi/OperationSelectionPredicateContext.cs b/dotnet/src/Functions/Functions.OpenApi/OpenApi/OperationSelectionPredicateContext.cs
new file mode 100644
index 000000000000..0632a5186de1
--- /dev/null
+++ b/dotnet/src/Functions/Functions.OpenApi/OpenApi/OperationSelectionPredicateContext.cs
@@ -0,0 +1,80 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+
+namespace Microsoft.SemanticKernel.Plugins.OpenApi;
+
+///
+/// Represents the context for an operation selection predicate.
+///
+public readonly struct OperationSelectionPredicateContext : IEquatable
+{
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The identifier for the operation.
+ /// The path of the operation.
+ /// The HTTP method (GET, POST, etc.) of the operation.
+ /// The description of the operation.
+ internal OperationSelectionPredicateContext(string? Id, string Path, string Method, string? Description)
+ {
+ this.Id = Id;
+ this.Path = Path;
+ this.Method = Method;
+ this.Description = Description;
+ }
+
+ ///
+ /// The identifier for the operation.
+ ///
+ public string? Id { get; }
+
+ ///
+ /// The path of the operation.
+ ///
+ public string Path { get; }
+
+ ///
+ /// The HTTP method (GET, POST, etc.) of the operation.
+ ///
+ public string Method { get; }
+
+ ///
+ /// The description of the operation.
+ ///
+ public string? Description { get; }
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ return obj is OperationSelectionPredicateContext other && this.Equals(other);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ // Using a tuple to create a hash code based on the properties
+ return HashCode.Combine(this.Id, this.Path, this.Method, this.Description);
+ }
+
+ ///
+ public static bool operator ==(OperationSelectionPredicateContext left, OperationSelectionPredicateContext right)
+ {
+ return left.Equals(right);
+ }
+
+ ///
+ public static bool operator !=(OperationSelectionPredicateContext left, OperationSelectionPredicateContext right)
+ {
+ return !(left == right);
+ }
+
+ ///
+ public bool Equals(OperationSelectionPredicateContext other)
+ {
+ return this.Id == other.Id &&
+ this.Path == other.Path &&
+ this.Method == other.Method &&
+ this.Description == other.Description;
+ }
+}
diff --git a/dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs b/dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs
index 34508b4d74d1..623d6292e54d 100644
--- a/dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs
+++ b/dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -18,18 +19,18 @@
namespace Microsoft.SemanticKernel.Plugins.OpenApi;
///
-/// Provides static factory methods for creating OpenAPI KernelPlugin implementations.
+/// Provides static factory methods for creating KernelPlugins from OpenAPI specifications.
///
public static partial class OpenApiKernelPluginFactory
{
///
- /// Creates a plugin from an OpenAPI specification.
+ /// Creates from an OpenAPI specification.
///
- /// Plugin name.
- /// The file path to the OpenAPI Plugin.
- /// Plugin execution parameters.
+ /// The plugin name.
+ /// The file path to the OpenAPI specification.
+ /// The OpenAPI specification parsing and function execution parameters.
/// The cancellation token.
- /// A KernelPlugin instance whose functions correspond to the OpenAPI operations.
+ /// A instance that contains functions corresponding to the operations defined in the OpenAPI specification.
public static async Task CreateFromOpenApiAsync(
string pluginName,
string filePath,
@@ -56,13 +57,13 @@ public static async Task CreateFromOpenApiAsync(
}
///
- /// Creates a plugin from an OpenAPI specification.
+ /// Creates from an OpenAPI specification.
///
- /// Plugin name.
- /// A local or remote URI referencing the OpenAPI Plugin.
- /// Plugin execution parameters.
+ /// The plugin name.
+ /// A URI referencing the OpenAPI specification.
+ /// The OpenAPI specification parsing and function execution parameters.
/// The cancellation token.
- /// A KernelPlugin instance whose functions correspond to the OpenAPI operations.
+ /// A instance that contains functions corresponding to the operations defined in the OpenAPI specification.
public static async Task CreateFromOpenApiAsync(
string pluginName,
Uri uri,
@@ -93,13 +94,13 @@ public static async Task CreateFromOpenApiAsync(
}
///
- /// Creates a plugin from an OpenAPI specification.
+ /// Creates from an OpenAPI specification.
///
- /// Plugin name.
- /// A stream representing the OpenAPI Plugin.
- /// Plugin execution parameters.
+ /// The plugin name.
+ /// A stream representing the OpenAPI specification.
+ /// The OpenAPI specification parsing and function execution parameters.
/// The cancellation token.
- /// A KernelPlugin instance whose functions correspond to the OpenAPI operations.
+ /// A instance that contains functions corresponding to the operations defined in the OpenAPI specification.
public static async Task CreateFromOpenApiAsync(
string pluginName,
Stream stream,
@@ -122,6 +123,32 @@ public static async Task CreateFromOpenApiAsync(
cancellationToken: cancellationToken).ConfigureAwait(false);
}
+ ///
+ /// Creates from an OpenAPI specification.
+ ///
+ /// The plugin name.
+ /// The specification model.
+ /// The OpenAPI specification parsing and function execution parameters.
+ /// A instance that contains functions corresponding to the operations defined in the OpenAPI specification.
+ [Experimental("SKEXP0040")]
+ public static KernelPlugin CreateFromOpenApi(
+ string pluginName,
+ RestApiSpecification specification,
+ OpenApiFunctionExecutionParameters? executionParameters = null)
+ {
+ Verify.ValidPluginName(pluginName);
+
+#pragma warning disable CA2000 // Dispose objects before losing scope. No need to dispose the Http client here. It can either be an internal client using NonDisposableHttpClientHandler or an external client managed by the calling code, which should handle its disposal.
+ var httpClient = HttpClientProvider.GetHttpClient(executionParameters?.HttpClient);
+#pragma warning restore CA2000
+
+ return CreateOpenApiPlugin(
+ pluginName: pluginName,
+ executionParameters: executionParameters,
+ httpClient: httpClient,
+ specification: specification);
+ }
+
///
/// Creates a plugin from an OpenAPI specification.
///
@@ -141,10 +168,38 @@ internal static async Task CreateOpenApiPluginAsync(
var parser = new OpenApiDocumentParser(loggerFactory);
var restApi = await parser.ParseAsync(
- documentStream,
- executionParameters?.IgnoreNonCompliantErrors ?? false,
- executionParameters?.OperationsToExclude,
- cancellationToken).ConfigureAwait(false);
+ stream: documentStream,
+ options: new OpenApiDocumentParserOptions
+ {
+ IgnoreNonCompliantErrors = executionParameters?.IgnoreNonCompliantErrors ?? false,
+ OperationSelectionPredicate = (context) =>
+ {
+ return !executionParameters?.OperationsToExclude.Contains(context.Id ?? string.Empty) ?? true;
+ }
+ },
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ return CreateOpenApiPlugin(
+ pluginName: pluginName,
+ executionParameters: executionParameters,
+ httpClient: httpClient,
+ specification: restApi,
+ documentUri: documentUri,
+ loggerFactory: loggerFactory);
+ }
+
+ ///
+ /// Creates a plugin from an OpenAPI specification.
+ ///
+ internal static KernelPlugin CreateOpenApiPlugin(
+ string pluginName,
+ OpenApiFunctionExecutionParameters? executionParameters,
+ HttpClient httpClient,
+ RestApiSpecification specification,
+ Uri? documentUri = null,
+ ILoggerFactory? loggerFactory = null)
+ {
+ loggerFactory ??= NullLoggerFactory.Instance;
var runner = new RestApiOperationRunner(
httpClient,
@@ -156,12 +211,12 @@ internal static async Task CreateOpenApiPluginAsync(
var functions = new List();
ILogger logger = loggerFactory.CreateLogger(typeof(OpenApiKernelExtensions)) ?? NullLogger.Instance;
- foreach (var operation in restApi.Operations)
+ foreach (var operation in specification.Operations)
{
try
{
logger.LogTrace("Registering Rest function {0}.{1}", pluginName, operation.Id);
- functions.Add(CreateRestApiFunction(pluginName, runner, restApi.Info, restApi.SecurityRequirements, operation, executionParameters, documentUri, loggerFactory));
+ functions.Add(CreateRestApiFunction(pluginName, runner, specification.Info, specification.SecurityRequirements, operation, executionParameters, documentUri, loggerFactory));
operation.Freeze();
}
catch (Exception ex) when (!ex.IsCriticalException())
@@ -172,7 +227,7 @@ internal static async Task CreateOpenApiPluginAsync(
}
}
- return KernelPluginFactory.CreateFromFunctions(pluginName, restApi.Info.Description, functions);
+ return KernelPluginFactory.CreateFromFunctions(pluginName, specification.Info.Description, functions);
}
///
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/Extensions/OpenApiKernelExtensionsTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/Extensions/OpenApiKernelExtensionsTests.cs
index f990519c15de..e7db29b47817 100644
--- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/Extensions/OpenApiKernelExtensionsTests.cs
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/Extensions/OpenApiKernelExtensionsTests.cs
@@ -311,6 +311,90 @@ public async Task ItShouldReplicateMetadataToOperationAsync(string documentFileN
Assert.Contains("x-object-extension", nonNullOperationExtensions.Keys);
}
+ [Fact]
+ public void ItCreatesPluginFromOpenApiSpecificationModel()
+ {
+ // Arrange
+ var info = new RestApiInfo() { Description = "api-description", Title = "api-title", Version = "7.0" };
+
+ var securityRequirements = new List
+ {
+ new(new Dictionary> { { new RestApiSecurityScheme(), new List() } })
+ };
+
+ var operations = new List
+ {
+ new (
+ id: "operation1",
+ servers: [],
+ path: "path",
+ method: HttpMethod.Get,
+ description: "operation-description",
+ parameters: [],
+ responses: new Dictionary(),
+ securityRequirements: [],
+ payload: null)
+ };
+
+ var specification = new RestApiSpecification(info, securityRequirements, operations);
+
+ // Act
+ var plugin = this._kernel.CreatePluginFromOpenApi("fakePlugin", specification, this._executionParameters);
+
+ // Assert
+ Assert.Single(plugin);
+ Assert.Equal("api-description", plugin.Description);
+ Assert.Equal("fakePlugin", plugin.Name);
+
+ var function = plugin["operation1"];
+ Assert.Equal("operation1", function.Name);
+ Assert.Equal("operation-description", function.Description);
+ Assert.Same(operations[0], function.Metadata.AdditionalProperties["operation"]);
+ }
+
+ [Fact]
+ public void ItImportPluginFromOpenApiSpecificationModel()
+ {
+ // Arrange
+ var info = new RestApiInfo() { Description = "api-description", Title = "api-title", Version = "7.0" };
+
+ var securityRequirements = new List
+ {
+ new(new Dictionary> { { new RestApiSecurityScheme(), new List() } })
+ };
+
+ var operations = new List
+ {
+ new (
+ id: "operation1",
+ servers: [],
+ path: "path",
+ method: HttpMethod.Get,
+ description: "operation-description",
+ parameters: [],
+ responses: new Dictionary(),
+ securityRequirements: [],
+ payload: null)
+ };
+
+ var specification = new RestApiSpecification(info, securityRequirements, operations);
+
+ // Act
+ this._kernel.ImportPluginFromOpenApi("fakePlugin", specification, this._executionParameters);
+
+ // Assert
+ var plugin = Assert.Single(this._kernel.Plugins);
+
+ Assert.Single(plugin);
+ Assert.Equal("api-description", plugin.Description);
+ Assert.Equal("fakePlugin", plugin.Name);
+
+ var function = plugin["operation1"];
+ Assert.Equal("operation1", function.Name);
+ Assert.Equal("operation-description", function.Description);
+ Assert.Same(operations[0], function.Metadata.AdditionalProperties["operation"]);
+ }
+
public void Dispose()
{
this._openApiDocument.Dispose();
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs
index 099ab02042f9..625420e2f956 100644
--- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs
@@ -414,6 +414,27 @@ public async Task ItCanParsePropertiesOfObjectDataTypeAsync()
Assert.Null(property.Format);
}
+ [Fact]
+ public async Task ItCanFilterOutSpecifiedOperationsAsync()
+ {
+ // Arrange
+ var operationsToExclude = new[] { "Excuses", "TestDefaultValues", "OpenApiExtensions", "TestParameterDataTypes" };
+
+ var options = new OpenApiDocumentParserOptions
+ {
+ OperationSelectionPredicate = (context) => !operationsToExclude.Contains(context.Id)
+ };
+
+ // Act
+ var restApiSpec = await this._sut.ParseAsync(this._openApiDocument, options);
+
+ // Assert
+ Assert.Equal(2, restApiSpec.Operations.Count);
+
+ Assert.Contains(restApiSpec.Operations, o => o.Id == "SetSecret");
+ Assert.Contains(restApiSpec.Operations, o => o.Id == "GetSecret");
+ }
+
private static RestApiParameter GetParameterMetadata(IList operations, string operationId,
RestApiParameterLocation location, string name)
{
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs
index 1a98185b68ab..6a00410e24e6 100644
--- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs
@@ -280,7 +280,7 @@ public async Task ItShouldWorkWithNonCompliantDocumentIfAllowedAsync()
var nonComplaintOpenApiDocument = ResourcePluginsProvider.LoadFromResource("nonCompliant_documentV3_0.json");
// Act
- await this._sut.ParseAsync(nonComplaintOpenApiDocument, ignoreNonCompliantErrors: true);
+ await this._sut.ParseAsync(nonComplaintOpenApiDocument, new OpenApiDocumentParserOptions() { IgnoreNonCompliantErrors = true });
// Assert
// The absence of any thrown exceptions serves as evidence of the functionality's success.
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs
index 4e3db39212fa..e68c5537c0ae 100644
--- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs
@@ -476,6 +476,47 @@ public async Task ItAddSecurityMetadataToOperationAsync(string documentFileName,
}
}
+ [Fact]
+ public void ItCreatesPluginFromOpenApiSpecificationModel()
+ {
+ // Arrange
+ var info = new RestApiInfo() { Description = "api-description", Title = "api-title", Version = "7.0" };
+
+ var securityRequirements = new List
+ {
+ new(new Dictionary> { { new RestApiSecurityScheme(), new List() } })
+ };
+
+ var operations = new List
+ {
+ new (
+ id: "operation1",
+ servers: [],
+ path: "path",
+ method: HttpMethod.Get,
+ description: "operation-description",
+ parameters: [],
+ responses: new Dictionary(),
+ securityRequirements: [],
+ payload: null)
+ };
+
+ var specification = new RestApiSpecification(info, securityRequirements, operations);
+
+ // Act
+ var plugin = OpenApiKernelPluginFactory.CreateFromOpenApi("fakePlugin", specification, this._executionParameters);
+
+ // Assert
+ Assert.Single(plugin);
+ Assert.Equal("api-description", plugin.Description);
+ Assert.Equal("fakePlugin", plugin.Name);
+
+ var function = plugin["operation1"];
+ Assert.Equal("operation1", function.Name);
+ Assert.Equal("operation-description", function.Description);
+ Assert.Same(operations[0], function.Metadata.AdditionalProperties["operation"]);
+ }
+
///
/// Generate theory data for ItAddSecurityMetadataToOperationAsync
///
diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OperationSelectionPredicateContextTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OperationSelectionPredicateContextTests.cs
new file mode 100644
index 000000000000..2d7794acb65f
--- /dev/null
+++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OperationSelectionPredicateContextTests.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.SemanticKernel.Plugins.OpenApi;
+using Xunit;
+
+namespace SemanticKernel.Functions.UnitTests.OpenApi;
+
+public class OperationSelectionPredicateContextTests
+{
+ [Fact]
+ public void ItShouldCheckTwoContextsAreEqual()
+ {
+ // Arrange
+ var context1 = new OperationSelectionPredicateContext("id", "path", "method", "description");
+ var context2 = new OperationSelectionPredicateContext("id", "path", "method", "description");
+
+ // Act & Assert
+ Assert.True(context1 == context2);
+ }
+
+ [Fact]
+ public void ItShouldCheckTwoContextsAreNotEqual()
+ {
+ // Arrange
+ var context1 = new OperationSelectionPredicateContext("id", "path", "method", "description");
+ var context2 = new OperationSelectionPredicateContext("id1", "path1", "method1", "description1");
+
+ // Act & Assert
+ Assert.False(context1 == context2);
+ }
+
+ [Fact]
+ public void ItShouldCheckContextsIsEqualToItself()
+ {
+ // Arrange
+ var context = new OperationSelectionPredicateContext("id", "path", "method", "description");
+
+ // Act & Assert
+#pragma warning disable CS1718 // Comparison made to same variable
+ Assert.True(context == context);
+#pragma warning restore CS1718 // Comparison made to same variable
+ }
+
+ [Fact]
+ public void ItShouldCheckContextIsNotEqualToNull()
+ {
+ // Arrange
+ var context = new OperationSelectionPredicateContext("id", "path", "method", "description");
+
+ // Act & Assert
+ Assert.False(context.Equals(null));
+ }
+}
diff --git a/dotnet/src/IntegrationTests/Plugins/OpenApi/RepairServiceTests.cs b/dotnet/src/IntegrationTests/Plugins/OpenApi/RepairServiceTests.cs
index 53e5f5f48828..ce3cde78a55e 100644
--- a/dotnet/src/IntegrationTests/Plugins/OpenApi/RepairServiceTests.cs
+++ b/dotnet/src/IntegrationTests/Plugins/OpenApi/RepairServiceTests.cs
@@ -38,7 +38,7 @@ public async Task ValidateInvokingRepairServicePluginAsync()
Assert.Equal("New repair created", result.ToString());
// List All Repairs
- result = await plugin["listRepairs"].InvokeAsync(kernel, arguments);
+ result = await plugin["listRepairs"].InvokeAsync(kernel);
Assert.NotNull(result);
var repairs = JsonSerializer.Deserialize(result.ToString());
@@ -95,7 +95,7 @@ public async Task ValidateCreatingRepairServicePluginAsync()
Assert.Equal("New repair created", result.ToString());
// List All Repairs
- result = await plugin["listRepairs"].InvokeAsync(kernel, arguments);
+ result = await plugin["listRepairs"].InvokeAsync(kernel);
Assert.NotNull(result);
var repairs = JsonSerializer.Deserialize(result.ToString());
@@ -178,15 +178,10 @@ public async Task KernelFunctionCanceledExceptionIncludeRequestInfoAsync()
stream,
new OpenApiFunctionExecutionParameters(httpClient) { IgnoreNonCompliantErrors = true, EnableDynamicPayload = false });
- var arguments = new KernelArguments
- {
- ["payload"] = """{ "title": "Engine oil change", "description": "Need to drain the old engine oil and replace it with fresh oil.", "assignedTo": "", "date": "", "image": "" }"""
- };
-
var id = 99999;
// Update Repair
- arguments = new KernelArguments
+ var arguments = new KernelArguments
{
["payload"] = $"{{ \"id\": {id}, \"assignedTo\": \"Karin Blair\", \"date\": \"2024-04-16\", \"image\": \"https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg\" }}"
};