diff --git a/dotnet/samples/KernelSyntaxExamples/Example21_ChatGPTPlugins.cs b/dotnet/samples/KernelSyntaxExamples/Example21_ChatGPTPlugins.cs index e0441d62e7b8..c7e087d8399e 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example21_ChatGPTPlugins.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example21_ChatGPTPlugins.cs @@ -4,8 +4,8 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Functions.OpenAPI.Extensions; using Microsoft.SemanticKernel.Functions.OpenAPI.Model; +using Microsoft.SemanticKernel.Functions.OpenAPI.OpenAI; using Microsoft.SemanticKernel.Orchestration; using RepoUtils; @@ -24,8 +24,8 @@ private static async Task RunChatGptPluginAsync() //This HTTP client is optional. SK will fallback to a default internal one if omitted. using HttpClient httpClient = new(); - //Import a ChatGPT plugin via URI - var plugin = await kernel.ImportPluginFunctionsAsync("", new Uri(""), new OpenApiFunctionExecutionParameters(httpClient)); + //Import an Open AI plugin via URI + var plugin = await kernel.ImportOpenAIPluginFunctionsAsync("", new Uri(""), new OpenAIFunctionExecutionParameters(httpClient)); //Add arguments for required parameters, arguments for optional ones can be skipped. var contextVariables = new ContextVariables(); @@ -43,7 +43,7 @@ private static async Task RunChatGptPluginAsync() //var kernel = new KernelBuilder().WithLoggerFactory(ConsoleLogger.LoggerFactory).Build(); - //var plugin = await kernel.ImportPluginFunctionsAsync("Klarna", new Uri("https://www.klarna.com/.well-known/ai-plugin.json")); + //var plugin = await kernel.ImportOpenAIPluginFunctionsAsync("Klarna", new Uri("https://www.klarna.com/.well-known/ai-plugin.json")); //var contextVariables = new ContextVariables(); //contextVariables.Set("q", "Laptop"); // A precise query that matches one very small category or product that needs to be searched for to find the products the user is looking for. If the user explicitly stated what they want, use that as a query. The query is as specific as possible to the product name or category mentioned by the user in its singular form, and don't contain any clarifiers like latest, newest, cheapest, budget, premium, expensive or similar. The query is always taken from the latest topic, if there is a new topic a new query is started. diff --git a/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiPlugin_AzureKeyVault.cs b/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiPlugin_AzureKeyVault.cs index b92be76a5239..365807538609 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiPlugin_AzureKeyVault.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiPlugin_AzureKeyVault.cs @@ -45,8 +45,8 @@ public static async Task GetSecretFromAzureKeyVaultWithRetryAsync(InteractiveMsa var stream = type.Assembly.GetManifestResourceStream(type, resourceName); - // Import AI Plugin - var plugin = await kernel.ImportPluginFunctionsAsync( + // Import an Open AI Plugin via Stream + var plugin = await kernel.ImportOpenApiPluginFunctionsAsync( PluginResourceNames.AzureKeyVault, stream!, new OpenApiFunctionExecutionParameters { AuthCallback = authenticationProvider.AuthenticateRequestAsync }); @@ -75,7 +75,7 @@ public static async Task AddSecretToAzureKeyVaultAsync(InteractiveMsalAuthentica var stream = type.Assembly.GetManifestResourceStream(type, resourceName); // Import AI Plugin - var plugin = await kernel.ImportPluginFunctionsAsync( + var plugin = await kernel.ImportOpenApiPluginFunctionsAsync( PluginResourceNames.AzureKeyVault, stream!, new OpenApiFunctionExecutionParameters diff --git a/dotnet/samples/KernelSyntaxExamples/Example24_OpenApiPlugin_Jira.cs b/dotnet/samples/KernelSyntaxExamples/Example24_OpenApiPlugin_Jira.cs index 41c4188e2146..8148b14e3459 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example24_OpenApiPlugin_Jira.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example24_OpenApiPlugin_Jira.cs @@ -43,12 +43,12 @@ public static async Task RunAsync() if (useLocalFile) { var apiPluginFile = "./../../../Plugins/JiraPlugin/openapi.json"; - jiraFunctions = await kernel.ImportPluginFunctionsAsync("jiraPlugin", apiPluginFile, new OpenApiFunctionExecutionParameters(authCallback: tokenProvider.AuthenticateRequestAsync)); + jiraFunctions = await kernel.ImportOpenApiPluginFunctionsAsync("jiraPlugin", apiPluginFile, new OpenApiFunctionExecutionParameters(authCallback: tokenProvider.AuthenticateRequestAsync)); } else { var apiPluginRawFileURL = new Uri("https://raw.githubusercontent.com/microsoft/PowerPlatformConnectors/dev/certified-connectors/JIRA/apiDefinition.swagger.json"); - jiraFunctions = await kernel.ImportPluginFunctionsAsync("jiraPlugin", apiPluginRawFileURL, new OpenApiFunctionExecutionParameters(httpClient, tokenProvider.AuthenticateRequestAsync)); + jiraFunctions = await kernel.ImportOpenApiPluginFunctionsAsync("jiraPlugin", apiPluginRawFileURL, new OpenApiFunctionExecutionParameters(httpClient, tokenProvider.AuthenticateRequestAsync)); } // GetIssue Function diff --git a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs index a71dada10454..e151550e7d9c 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example59_OpenAIFunctionCalling.cs @@ -8,8 +8,8 @@ using Microsoft.SemanticKernel.AI.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AI.OpenAI; using Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk; -using Microsoft.SemanticKernel.Functions.OpenAPI.Extensions; using Microsoft.SemanticKernel.Functions.OpenAPI.Model; +using Microsoft.SemanticKernel.Functions.OpenAPI.OpenAI; using Microsoft.SemanticKernel.Orchestration; using Microsoft.SemanticKernel.Plugins.Core; using RepoUtils; @@ -56,7 +56,7 @@ private static async Task InitializeKernelAsync() // Load functions to kernel kernel.ImportFunctions(new TimePlugin(), "TimePlugin"); - await kernel.ImportPluginFunctionsAsync("KlarnaShoppingPlugin", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"), new OpenApiFunctionExecutionParameters()); + await kernel.ImportOpenAIPluginFunctionsAsync("KlarnaShoppingPlugin", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"), new OpenAIFunctionExecutionParameters()); return kernel; } diff --git a/dotnet/src/Functions/Functions.OpenAPI/Authentication/CustomAuthenticationProvider.cs b/dotnet/src/Functions/Functions.OpenAPI/Authentication/CustomAuthenticationProvider.cs index 3769b4877ba8..9348c1a25a75 100644 --- a/dotnet/src/Functions/Functions.OpenAPI/Authentication/CustomAuthenticationProvider.cs +++ b/dotnet/src/Functions/Functions.OpenAPI/Authentication/CustomAuthenticationProvider.cs @@ -29,7 +29,6 @@ public CustomAuthenticationProvider(Func> header, Func /// Applies the header and value to the provided HTTP request message. /// /// The HTTP request message. - /// public async Task AuthenticateRequestAsync(HttpRequestMessage request) { var header = await this._header().ConfigureAwait(false); diff --git a/dotnet/src/Functions/Functions.OpenAPI/Authentication/README.md b/dotnet/src/Functions/Functions.OpenAPI/Authentication/README.md index 7df11ca60cdf..56f4620fcea4 100644 --- a/dotnet/src/Functions/Functions.OpenAPI/Authentication/README.md +++ b/dotnet/src/Functions/Functions.OpenAPI/Authentication/README.md @@ -17,9 +17,11 @@ This pattern was designed to be flexible enough to support a wide variety of aut ## Reference Authentication Providers ### [`BasicAuthenticationProvider`](./BasicAuthenticationProvider.cs) + This class implements the HTTP "basic" authentication scheme. The constructor accepts a `Func` which defines how to retrieve the user's credentials. When the `AuthenticateRequestAsync` method is called, it retrieves the credentials, encodes them as a UTF-8 encoded Base64 string, and adds them to the `HttpRequestMessage`'s authorization header. The following code demonstrates how to use this provider: + ```csharp var basicAuthProvider = new BasicAuthenticationProvider(() => { @@ -28,19 +30,21 @@ var basicAuthProvider = new BasicAuthenticationProvider(() => Env.Var("MY_EMAIL_ADDRESS") + ":" + Env.Var("JIRA_API_KEY") ); }); -var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.Jira, new OpenApiPluginExecutionParameters { AuthCallback = basicAuthProvider.AuthenticateRequestAsync } ); +var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.Jira, new OpenApiFunctionExecutionParameters { AuthCallback = basicAuthProvider.AuthenticateRequestAsync } ); ``` ### [`BearerAuthenticationProvider`](./BearerAuthenticationProvider.cs) -This class implements the HTTP "bearer" authentication scheme. The constructor accepts a `Func` which defines how to retrieve the bearer token. When the `AuthenticateRequestAsync` method is called, it retrieves the token and adds it to the `HttpRequestMessage`'s authorization header. + +This class implements the HTTP "bearer" authentication scheme. The constructor accepts a `Func` which defines how to retrieve the bearer token. When the `AuthenticateRequestAsync` method is called, it retrieves the token and adds it to the `HttpRequestMessage`'s authorization header. The following code demonstrates how to use this provider: + ```csharp var bearerAuthProvider = new BearerAuthenticationProvider(() => { return Task.FromResult(Env.Var("AZURE_KEYVAULT_TOKEN")); }); -var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.AzureKeyVault, new OpenApiPluginExecutionParameters { AuthCallback = bearerAuthProvider.AuthenticateRequestAsync } ) +var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.AzureKeyVault, new OpenApiFunctionExecutionParameters { AuthCallback = bearerAuthProvider.AuthenticateRequestAsync } ) ``` ### [`InteractiveMsalAuthenticationProvider`](./InteractiveMsalAuthenticationProvider.cs) @@ -50,10 +54,11 @@ This class uses the [Microsoft Authentication Library (MSAL)](https://learn.micr Once the token is acquired, it is added to the HTTP authentication header via the `AuthenticateRequestAsync` method, which is inherited from `BearerAuthenticationProvider`. To construct this provider, the caller must specify: -- *Client ID* – identifier of the calling application. This is acquired by [registering your application with the Microsoft Identity platform](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app). -- *Tenant ID* – identifier of the target service tenant, or “common” -- *Scopes* – permissions being requested -- *Redirect URI* – for redirecting the user back to the application. (When running locally, this is typically http://localhost.) + +- _Client ID_ – identifier of the calling application. This is acquired by [registering your application with the Microsoft Identity platform](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app). +- _Tenant ID_ – identifier of the target service tenant, or “common” +- _Scopes_ – permissions being requested +- _Redirect URI_ – for redirecting the user back to the application. (When running locally, this is typically http://localhost.) ```csharp var msalAuthProvider = new InteractiveMsalAuthenticationProvider( @@ -62,5 +67,5 @@ var msalAuthProvider = new InteractiveMsalAuthenticationProvider( new string[] { ".default" }, // scopes new Uri("http://localhost") // redirectUri ); -var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.AzureKeyVault, new OpenApiPluginExecutionParameters { AuthCallback = msalAuthProvider.AuthenticateRequestAsync } ) -``` \ No newline at end of file +var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.AzureKeyVault, new OpenApiFunctionExecutionParameters { AuthCallback = msalAuthProvider.AuthenticateRequestAsync } ) +``` diff --git a/dotnet/src/Functions/Functions.OpenAPI/DocumentLoader.cs b/dotnet/src/Functions/Functions.OpenAPI/DocumentLoader.cs new file mode 100644 index 000000000000..c45074162aa0 --- /dev/null +++ b/dotnet/src/Functions/Functions.OpenAPI/DocumentLoader.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Functions.OpenAPI.Authentication; + +namespace Microsoft.SemanticKernel.Functions.OpenAPI; + +internal static class DocumentLoader +{ + internal static async Task LoadDocumentFromUriAsync( + Uri uri, + ILogger logger, + HttpClient httpClient, + AuthenticateRequestAsyncCallback? authCallback, + string? userAgent, + CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage(HttpMethod.Get, uri.ToString()); + request.Headers.UserAgent.Add(ProductInfoHeaderValue.Parse(userAgent ?? Telemetry.HttpUserAgent)); + + if (authCallback is not null) + { + await authCallback(request).ConfigureAwait(false); + } + + logger.LogTrace("Importing document from {0}", uri); + + using var response = await httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); + return await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false); + } + + internal static async Task LoadDocumentFromFilePathAsync( + string filePath, + ILogger logger, + CancellationToken cancellationToken) + { + var pluginJson = string.Empty; + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"Invalid URI. The specified path '{filePath}' does not exist."); + } + + logger.LogTrace("Importing document from {0}", filePath); + + using (var sr = File.OpenText(filePath)) + { + return await sr.ReadToEndAsync().ConfigureAwait(false); // must await here to avoid stream reader being disposed before the string is read + } + } + + internal static async Task LoadDocumentFromStreamAsync(Stream stream) + { + using StreamReader reader = new(stream); + return await reader.ReadToEndAsync().ConfigureAwait(false); + } +} diff --git a/dotnet/src/Functions/Functions.OpenAPI/Extensions/KernelAIPluginExtensions.cs b/dotnet/src/Functions/Functions.OpenAPI/Extensions/KernelOpenApiPluginExtensions.cs similarity index 66% rename from dotnet/src/Functions/Functions.OpenAPI/Extensions/KernelAIPluginExtensions.cs rename to dotnet/src/Functions/Functions.OpenAPI/Extensions/KernelOpenApiPluginExtensions.cs index ff0ba2a22f55..77e3c038a4a2 100644 --- a/dotnet/src/Functions/Functions.OpenAPI/Extensions/KernelAIPluginExtensions.cs +++ b/dotnet/src/Functions/Functions.OpenAPI/Extensions/KernelOpenApiPluginExtensions.cs @@ -2,13 +2,10 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; -using System.Text.Json.Nodes; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -22,26 +19,12 @@ namespace Microsoft.SemanticKernel.Functions.OpenAPI.Extensions; /// -/// Provides extension methods for importing AI plugins exposed as OpenAPI v3 endpoints or through OpenAI's ChatGPT format. +/// Provides extension methods for importing plugins exposed as OpenAPI v3 endpoints. /// -public static class KernelAIPluginExtensions +public static class KernelOpenApiPluginExtensions { - [Obsolete("Methods and classes which includes Skill in the name have been renamed to use Plugin. Use Kernel.ImportPluginFunctionsAsync instead. This will be removed in a future release.")] - [EditorBrowsable(EditorBrowsableState.Never)] -#pragma warning disable CS1591 - public static async Task> ImportAIPluginAsync( - this IKernel kernel, - string pluginName, - string filePath, - OpenApiFunctionExecutionParameters? executionParameters = null, - CancellationToken cancellationToken = default) - { - return await kernel.ImportPluginFunctionsAsync(pluginName, filePath, executionParameters, cancellationToken).ConfigureAwait(false); - } -#pragma warning restore CS1591 - /// - /// Imports an AI plugin that is exposed as an OpenAPI v3 endpoint or through OpenAI's ChatGPT format. + /// Imports a plugin that is exposed as an OpenAPI v3 endpoint. /// /// Semantic Kernel instance. /// Plugin name. @@ -49,7 +32,7 @@ public static async Task> ImportAIPluginAsync( /// Plugin execution parameters. /// The cancellation token. /// A collection of invocable functions - public static async Task> ImportPluginFunctionsAsync( + public static async Task> ImportOpenApiPluginFunctionsAsync( this IKernel kernel, string pluginName, string filePath, @@ -63,38 +46,22 @@ public static async Task> ImportPluginFunctions var httpClient = HttpClientProvider.GetHttpClient(kernel.HttpHandlerFactory, executionParameters?.HttpClient, kernel.LoggerFactory); #pragma warning restore CA2000 - var pluginContents = await LoadDocumentFromFilePathAsync( - kernel, + var openApiSpec = await DocumentLoader.LoadDocumentFromFilePathAsync( filePath, - executionParameters, - httpClient, + kernel.LoggerFactory.CreateLogger(typeof(KernelOpenApiPluginExtensions)), cancellationToken).ConfigureAwait(false); - return await CompleteImportAsync( + return await RegisterOpenApiPluginAsync( kernel, - pluginContents, pluginName, - httpClient, executionParameters, + httpClient, + openApiSpec, cancellationToken: cancellationToken).ConfigureAwait(false); } - [Obsolete("Methods and classes which includes Skill in the name have been renamed to use Plugin. Use Kernel.ImportPluginFunctionsAsync instead. This will be removed in a future release.")] - [EditorBrowsable(EditorBrowsableState.Never)] -#pragma warning disable CS1591 - public static async Task> ImportAIPluginAsync( - this IKernel kernel, - string pluginName, - Uri uri, - OpenApiFunctionExecutionParameters? executionParameters = null, - CancellationToken cancellationToken = default) - { - return await kernel.ImportPluginFunctionsAsync(pluginName, uri, executionParameters, cancellationToken).ConfigureAwait(false); - } -#pragma warning restore CS1591 - /// - /// Imports an AI plugin that is exposed as an OpenAPI v3 endpoint or through OpenAI's ChatGPT format. + /// Imports a plugin that is exposed as an OpenAPI v3 endpoint. /// /// Semantic Kernel instance. /// Plugin name. @@ -102,7 +69,7 @@ public static async Task> ImportAIPluginAsync( /// Plugin execution parameters. /// The cancellation token. /// A collection of invocable functions - public static async Task> ImportPluginFunctionsAsync( + public static async Task> ImportOpenApiPluginFunctionsAsync( this IKernel kernel, string pluginName, Uri uri, @@ -116,25 +83,26 @@ public static async Task> ImportPluginFunctions var httpClient = HttpClientProvider.GetHttpClient(kernel.HttpHandlerFactory, executionParameters?.HttpClient, kernel.LoggerFactory); #pragma warning restore CA2000 - var pluginContents = await LoadDocumentFromUriAsync( - kernel, + var openApiSpec = await DocumentLoader.LoadDocumentFromUriAsync( uri, - executionParameters, + kernel.LoggerFactory.CreateLogger(typeof(KernelOpenApiPluginExtensions)), httpClient, + executionParameters?.AuthCallback, + executionParameters?.UserAgent, cancellationToken).ConfigureAwait(false); - return await CompleteImportAsync( + return await RegisterOpenApiPluginAsync( kernel, - pluginContents, pluginName, - httpClient, executionParameters, + httpClient, + openApiSpec, uri, - cancellationToken).ConfigureAwait(false); + cancellationToken: cancellationToken).ConfigureAwait(false); } /// - /// Imports an AI plugin that is exposed as an OpenAPI v3 endpoint or through OpenAI's ChatGPT format. + /// Imports a plugin that is exposed as an OpenAPI v3 endpoint. /// /// Semantic Kernel instance. /// Plugin name. @@ -142,7 +110,7 @@ public static async Task> ImportPluginFunctions /// Plugin execution parameters. /// The cancellation token. /// A collection of invocable functions - public static async Task> ImportPluginFunctionsAsync( + public static async Task> ImportOpenApiPluginFunctionsAsync( this IKernel kernel, string pluginName, Stream stream, @@ -156,50 +124,20 @@ public static async Task> ImportPluginFunctions var httpClient = HttpClientProvider.GetHttpClient(kernel.HttpHandlerFactory, executionParameters?.HttpClient, kernel.LoggerFactory); #pragma warning restore CA2000 - var pluginContents = await LoadDocumentFromStreamAsync(kernel, stream).ConfigureAwait(false); + var openApiSpec = await DocumentLoader.LoadDocumentFromStreamAsync(stream).ConfigureAwait(false); - return await CompleteImportAsync( + return await RegisterOpenApiPluginAsync( kernel, - pluginContents, pluginName, - httpClient, executionParameters, + httpClient, + openApiSpec, cancellationToken: cancellationToken).ConfigureAwait(false); } #region private - private static async Task> CompleteImportAsync( - IKernel kernel, - string pluginContents, - string pluginName, - HttpClient httpClient, - OpenApiFunctionExecutionParameters? executionParameters, - Uri? documentUri = null, - CancellationToken cancellationToken = default) - { - if (TryParseAIPluginForUrl(pluginContents, out var openApiUrl)) - { - return await kernel - .ImportPluginFunctionsAsync( - pluginName, - new Uri(openApiUrl), - executionParameters, - cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - return await LoadPluginAsync( - kernel, - pluginName, - executionParameters, - httpClient, - pluginContents, - documentUri, - cancellationToken).ConfigureAwait(false); - } - - private static async Task> LoadPluginAsync( + private static async Task> RegisterOpenApiPluginAsync( IKernel kernel, string pluginName, OpenApiFunctionExecutionParameters? executionParameters, @@ -223,7 +161,7 @@ private static async Task> LoadPluginAsync( var plugin = new Dictionary(); - ILogger logger = kernel.LoggerFactory.CreateLogger(typeof(KernelAIPluginExtensions)); + ILogger logger = kernel.LoggerFactory.CreateLogger(typeof(KernelOpenApiPluginExtensions)); foreach (var operation in operations) { try @@ -244,84 +182,6 @@ private static async Task> LoadPluginAsync( } } - private static async Task LoadDocumentFromUriAsync( - IKernel kernel, - Uri uri, - OpenApiFunctionExecutionParameters? executionParameters, - HttpClient httpClient, - CancellationToken cancellationToken) - { - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri.ToString()); - - requestMessage.Headers.UserAgent.Add(ProductInfoHeaderValue.Parse(executionParameters?.UserAgent ?? Telemetry.HttpUserAgent)); - - using var response = await httpClient.SendWithSuccessCheckAsync(requestMessage, cancellationToken).ConfigureAwait(false); - - return await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false); - } - - private static async Task LoadDocumentFromFilePathAsync( - IKernel kernel, - string filePath, - OpenApiFunctionExecutionParameters? executionParameters, - HttpClient httpClient, - CancellationToken cancellationToken) - { - var pluginJson = string.Empty; - - if (!File.Exists(filePath)) - { - throw new FileNotFoundException($"Invalid URI. The specified path '{filePath}' does not exist."); - } - - kernel.LoggerFactory.CreateLogger(typeof(KernelAIPluginExtensions)).LogTrace("Importing AI Plugin from {0}", filePath); - - using (var sr = File.OpenText(filePath)) - { - return await sr.ReadToEndAsync().ConfigureAwait(false); //must await here to avoid stream reader being disposed before the string is read - } - } - - private static async Task LoadDocumentFromStreamAsync( - IKernel kernel, - Stream stream) - { - using StreamReader reader = new(stream); - return await reader.ReadToEndAsync().ConfigureAwait(false); - } - - private static bool TryParseAIPluginForUrl(string gptPluginJson, out string? openApiUrl) - { - try - { - JsonNode? gptPlugin = JsonNode.Parse(gptPluginJson); - - string? apiType = gptPlugin?["api"]?["type"]?.ToString(); - - if (string.IsNullOrWhiteSpace(apiType) || apiType != "openapi") - { - openApiUrl = null; - - return false; - } - - openApiUrl = gptPlugin?["api"]?["url"]?.ToString(); - - if (string.IsNullOrWhiteSpace(openApiUrl)) - { - return false; - } - - return true; - } - catch (System.Text.Json.JsonException) - { - openApiUrl = null; - - return false; - } - } - /// /// Registers SKFunction for a REST API operation. /// @@ -349,7 +209,7 @@ private static ISKFunction RegisterRestApiFunction( documentUri ); - var logger = kernel.LoggerFactory is not null ? kernel.LoggerFactory.CreateLogger(typeof(KernelAIPluginExtensions)) : NullLogger.Instance; + var logger = kernel.LoggerFactory is not null ? kernel.LoggerFactory.CreateLogger(typeof(KernelOpenApiPluginExtensions)) : NullLogger.Instance; async Task ExecuteAsync(SKContext context) { diff --git a/dotnet/src/Functions/Functions.OpenAPI/Extensions/RestApiOperationExtensions.cs b/dotnet/src/Functions/Functions.OpenAPI/Extensions/RestApiOperationExtensions.cs index b69dc88d8b00..a04b95c0f558 100644 --- a/dotnet/src/Functions/Functions.OpenAPI/Extensions/RestApiOperationExtensions.cs +++ b/dotnet/src/Functions/Functions.OpenAPI/Extensions/RestApiOperationExtensions.cs @@ -51,7 +51,7 @@ public static IReadOnlyList GetParameters( var parameters = new List(operation.Parameters) { // Register the "server-url" parameter if override is provided - new RestApiOperationParameter( + new( name: RestApiOperation.ServerUrlArgumentName, type: "string", isRequired: false, diff --git a/dotnet/src/Functions/Functions.OpenAPI/OpenAI/KernelOpenAIPluginExtensions.cs b/dotnet/src/Functions/Functions.OpenAPI/OpenAI/KernelOpenAIPluginExtensions.cs new file mode 100644 index 000000000000..82cca09a3cbf --- /dev/null +++ b/dotnet/src/Functions/Functions.OpenAPI/OpenAI/KernelOpenAIPluginExtensions.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Functions.OpenAPI.Extensions; + +namespace Microsoft.SemanticKernel.Functions.OpenAPI.OpenAI; + +/// +/// Provides extension methods for importing plugins exposed through OpenAI's ChatGPT format. +/// +public static class KernelOpenAIPluginExtensions +{ + /// + /// Imports a plugin that is exposed through OpenAI's ChatGPT format. + /// + /// Semantic Kernel instance. + /// Plugin name. + /// The file path to the AI Plugin + /// Plugin execution parameters. + /// The cancellation token. + /// A collection of invocable functions + public static async Task> ImportOpenAIPluginFunctionsAsync( + this IKernel kernel, + string pluginName, + string filePath, + OpenAIFunctionExecutionParameters? executionParameters = null, + CancellationToken cancellationToken = default) + { + Verify.NotNull(kernel); + Verify.ValidPluginName(pluginName); + + var openAIManifest = await DocumentLoader.LoadDocumentFromFilePathAsync( + filePath, + kernel.LoggerFactory.CreateLogger(typeof(KernelOpenAIPluginExtensions)), + cancellationToken).ConfigureAwait(false); + + return await ImportAsync( + kernel, + openAIManifest, + pluginName, + executionParameters, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Imports a plugin that is exposed through OpenAI's ChatGPT format. + /// + /// Semantic Kernel instance. + /// Plugin name. + /// A local or remote URI referencing the AI Plugin + /// Plugin execution parameters. + /// The cancellation token. + /// A collection of invocable functions + public static async Task> ImportOpenAIPluginFunctionsAsync( + this IKernel kernel, + string pluginName, + Uri uri, + OpenAIFunctionExecutionParameters? executionParameters = null, + CancellationToken cancellationToken = default) + { + Verify.NotNull(kernel); + 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(kernel.HttpHandlerFactory, executionParameters?.HttpClient, kernel.LoggerFactory); +#pragma warning restore CA2000 + + var openAIManifest = await DocumentLoader.LoadDocumentFromUriAsync( + uri, + kernel.LoggerFactory.CreateLogger(typeof(KernelOpenAIPluginExtensions)), + httpClient, + null, // auth is not needed when loading the manifest + executionParameters?.UserAgent, + cancellationToken).ConfigureAwait(false); + + return await ImportAsync( + kernel, + openAIManifest, + pluginName, + executionParameters, + cancellationToken).ConfigureAwait(false); + } + + /// + /// Imports a plugin that is exposed through OpenAI's ChatGPT format. + /// + /// Semantic Kernel instance. + /// Plugin name. + /// A stream representing the AI Plugin + /// Plugin execution parameters. + /// The cancellation token. + /// A collection of invocable functions + public static async Task> ImportOpenAIPluginFunctionsAsync( + this IKernel kernel, + string pluginName, + Stream stream, + OpenAIFunctionExecutionParameters? executionParameters = null, + CancellationToken cancellationToken = default) + { + Verify.NotNull(kernel); + Verify.ValidPluginName(pluginName); + + var openAIManifest = await DocumentLoader.LoadDocumentFromStreamAsync(stream).ConfigureAwait(false); + + return await ImportAsync( + kernel, + openAIManifest, + pluginName, + executionParameters, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + #region private + + private static async Task> ImportAsync( + IKernel kernel, + string openAIManifest, + string pluginName, + OpenAIFunctionExecutionParameters? executionParameters = null, + CancellationToken cancellationToken = default) + { + JsonNode pluginJson; + OpenAIAuthenticationConfig openAIAuthConfig; + try + { + pluginJson = JsonNode.Parse(openAIManifest)!; + openAIAuthConfig = pluginJson["auth"].Deserialize()!; + } + catch (JsonException ex) + { + throw new SKException("Parsing of Open AI manifest failed.", ex); + } + + if (executionParameters?.AuthCallback is not null) + { + var callback = executionParameters.AuthCallback; + ((OpenApiFunctionExecutionParameters)executionParameters).AuthCallback = async (request) => + { + await callback(request, pluginName, openAIAuthConfig).ConfigureAwait(false); + }; + } + + return await kernel.ImportOpenApiPluginFunctionsAsync( + pluginName, + ParseOpenAIManifestForOpenApiSpecUrl(pluginJson), + executionParameters, + cancellationToken).ConfigureAwait(false); + } + + private static Uri ParseOpenAIManifestForOpenApiSpecUrl(JsonNode pluginJson) + { + string? apiType = pluginJson?["api"]?["type"]?.ToString(); + if (string.IsNullOrWhiteSpace(apiType) || apiType != "openapi") + { + throw new SKException($"Unexpected API type '{apiType}' found in Open AI manifest."); + } + + string? apiUrl = pluginJson?["api"]?["url"]?.ToString(); + if (string.IsNullOrWhiteSpace(apiUrl)) + { + throw new SKException("No Open API spec URL found in Open AI manifest."); + } + + try + { + return new Uri(apiUrl); + } + catch (System.UriFormatException ex) + { + throw new SKException("Invalid Open API spec URI found in Open AI manifest.", ex); + } + } + + #endregion +} diff --git a/dotnet/src/Functions/Functions.OpenAPI/OpenAI/OpenAIAuthenticateRequestAsyncCallback.cs b/dotnet/src/Functions/Functions.OpenAPI/OpenAI/OpenAIAuthenticateRequestAsyncCallback.cs new file mode 100644 index 000000000000..be7592b01718 --- /dev/null +++ b/dotnet/src/Functions/Functions.OpenAPI/OpenAI/OpenAIAuthenticateRequestAsyncCallback.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Functions.OpenAPI.OpenAI; + +/// +/// Represents a delegate that defines the method signature for asynchronously authenticating an HTTP request. +/// +/// The to authenticate. +/// The name of the plugin to be authenticated. +/// The used to authenticate. +/// A representing the asynchronous operation. +public delegate Task OpenAIAuthenticateRequestAsyncCallback(HttpRequestMessage request, string pluginName, OpenAIAuthenticationConfig openAIAuthConfig); diff --git a/dotnet/src/Functions/Functions.OpenAPI/OpenAI/OpenAIAuthenticationConfig.cs b/dotnet/src/Functions/Functions.OpenAPI/OpenAI/OpenAIAuthenticationConfig.cs new file mode 100644 index 000000000000..b641a4b5ecf6 --- /dev/null +++ b/dotnet/src/Functions/Functions.OpenAPI/OpenAI/OpenAIAuthenticationConfig.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Functions.OpenAPI.OpenAI; + +/// +/// Represents the authentication section for an OpenAI plugin. +/// +public record OpenAIAuthenticationConfig +{ + /// + /// The type of authentication. + /// + [JsonPropertyName("type")] + public OpenAIAuthenticationType Type { get; set; } = OpenAIAuthenticationType.None; + + /// + /// The type of authorization. + /// + [JsonPropertyName("authorization_type")] + public OpenAIAuthorizationType? AuthorizationType { get; set; } + + /// + /// The client URL. + /// + [JsonPropertyName("client_url")] + public Uri? ClientUrl { get; set; } + + /// + /// The authorization URL. + /// + [JsonPropertyName("authorization_url")] + public Uri? AuthorizationUrl { get; set; } + + /// + /// The authorization content type. + /// + public OpenAIAuthorizationContentType? AuthorizationContentType { get; set; } + + /// + /// The authorization scope. + /// + [JsonPropertyName("scope")] + public string? Scope { get; set; } + + /// + /// The verification tokens. + /// + [JsonPropertyName("verification_tokens")] + public Dictionary? VerificationTokens { get; set; } +} + +/// +/// Represents the type of authentication for an OpenAI plugin. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum OpenAIAuthenticationType +{ + /// + /// No authentication. + /// + [EnumMember(Value = "none")] + None, + + /// + /// User HTTP authentication. + /// + [EnumMember(Value = "user_http")] + UserHttp, + + /// + /// Service HTTP authentication. + /// + [EnumMember(Value = "service_http")] + ServiceHttp, + + /// + /// OAuth authentication. + /// + [EnumMember(Value = "oauth")] + OAuth +} + +/// +/// Represents the type of authorization for an OpenAI plugin. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum OpenAIAuthorizationType +{ + /// + /// Basic authorization. + /// + [EnumMember(Value = "Basic")] + Basic, + + /// + /// Bearer authorization. + /// + [EnumMember(Value = "Bearer")] + Bearer +} + +/// +/// Represents the type of content used for authorization for an OpenAI plugin. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum OpenAIAuthorizationContentType +{ + /// + /// JSON content. + /// + [EnumMember(Value = "application/json")] + JSON, + + /// + /// Form URL encoded content. + /// + [EnumMember(Value = "application/x-www-form-urlencoded")] + FormUrlEncoded +} diff --git a/dotnet/src/Functions/Functions.OpenAPI/OpenAI/OpenAIFunctionExecutionParameters.cs b/dotnet/src/Functions/Functions.OpenAPI/OpenAI/OpenAIFunctionExecutionParameters.cs new file mode 100644 index 000000000000..8ff2c648402f --- /dev/null +++ b/dotnet/src/Functions/Functions.OpenAPI/OpenAI/OpenAIFunctionExecutionParameters.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Functions.OpenAPI.Extensions; + +namespace Microsoft.SemanticKernel.Functions.OpenAPI.OpenAI; + +/// +/// OpenAI function execution parameters +/// +public class OpenAIFunctionExecutionParameters : OpenApiFunctionExecutionParameters +{ + /// + /// Callback for adding Open AI authentication data to HTTP requests. + /// + public new OpenAIAuthenticateRequestAsyncCallback? AuthCallback { get; set; } + + /// + public OpenAIFunctionExecutionParameters( + HttpClient? httpClient = null, + OpenAIAuthenticateRequestAsyncCallback? authCallback = null, + Uri? serverUrlOverride = null, + string userAgent = Telemetry.HttpUserAgent, + bool ignoreNonCompliantErrors = false, + bool enableDynamicOperationPayload = false, + bool enablePayloadNamespacing = false) : base(httpClient, null, serverUrlOverride, userAgent, ignoreNonCompliantErrors, enableDynamicOperationPayload, enablePayloadNamespacing) + { + this.AuthCallback = authCallback; + } +} diff --git a/dotnet/src/Functions/Functions.UnitTests/Functions.UnitTests.csproj b/dotnet/src/Functions/Functions.UnitTests/Functions.UnitTests.csproj index cc3d8351e03c..2d596328f53f 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Functions.UnitTests.csproj +++ b/dotnet/src/Functions/Functions.UnitTests/Functions.UnitTests.csproj @@ -23,6 +23,7 @@ + diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenAPI/Builders/QueryStringBuilderTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/Builders/QueryStringBuilderTests.cs index 366f6f8eee54..766ddcbd36ab 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenAPI/Builders/QueryStringBuilderTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/Builders/QueryStringBuilderTests.cs @@ -142,7 +142,7 @@ public void ItShouldEncodeSpecialSymbolsInQueryStringValues(string specialSymbol // Arrange var metadata = new List { - new RestApiOperationParameter( + new( name: "p1", type: "string", isRequired: false, @@ -173,7 +173,7 @@ public void ItShouldCreateAmpersandSeparatedParameterPerArrayItemForFormStylePar // Arrange var metadata = new List { - new RestApiOperationParameter( + new( name: "p1", type: "array", isRequired: false, @@ -181,7 +181,7 @@ public void ItShouldCreateAmpersandSeparatedParameterPerArrayItemForFormStylePar location: RestApiOperationParameterLocation.Query, style: RestApiOperationParameterStyle.Form, arrayItemType: "string"), - new RestApiOperationParameter( + new( name: "p2", type: "array", isRequired: false, @@ -214,7 +214,7 @@ public void ItShouldCreateParameterWithCommaSeparatedValuePerArrayItemForFormSty // Arrange var metadata = new List { - new RestApiOperationParameter( + new( name: "p1", type: "array", isRequired: false, @@ -222,7 +222,7 @@ public void ItShouldCreateParameterWithCommaSeparatedValuePerArrayItemForFormSty location: RestApiOperationParameterLocation.Query, style: RestApiOperationParameterStyle.Form, arrayItemType: "string"), - new RestApiOperationParameter( + new( name: "p2", type: "array", isRequired: false, @@ -255,7 +255,7 @@ public void ItShouldCreateParameterForPrimitiveValuesForFormStyleParameters() // Arrange var metadata = new List { - new RestApiOperationParameter( + new( name: "p1", type: "string", isRequired: false, @@ -263,7 +263,7 @@ public void ItShouldCreateParameterForPrimitiveValuesForFormStyleParameters() location: RestApiOperationParameterLocation.Query, style: RestApiOperationParameterStyle.Form, arrayItemType: "string"), - new RestApiOperationParameter( + new( name: "p2", type: "boolean", isRequired: false, @@ -295,7 +295,7 @@ public void ItShouldCreateAmpersandSeparatedParameterPerArrayItemForSpaceDelimit // Arrange var metadata = new List { - new RestApiOperationParameter( + new( name: "p1", type: "array", isRequired: false, @@ -303,7 +303,7 @@ public void ItShouldCreateAmpersandSeparatedParameterPerArrayItemForSpaceDelimit location: RestApiOperationParameterLocation.Query, style: RestApiOperationParameterStyle.SpaceDelimited, arrayItemType: "string"), - new RestApiOperationParameter( + new( name: "p2", type: "array", isRequired: false, @@ -336,7 +336,7 @@ public void ItShouldCreateParameterWithSpaceSeparatedValuePerArrayItemForSpaceDe // Arrange var metadata = new List { - new RestApiOperationParameter( + new( name: "p1", type: "array", isRequired: false, @@ -344,7 +344,7 @@ public void ItShouldCreateParameterWithSpaceSeparatedValuePerArrayItemForSpaceDe location: RestApiOperationParameterLocation.Query, style: RestApiOperationParameterStyle.SpaceDelimited, arrayItemType: "string"), - new RestApiOperationParameter( + new( name: "p2", type: "array", isRequired: false, @@ -377,7 +377,7 @@ public void ItShouldCreateAmpersandSeparatedParameterPerArrayItemForPipeDelimite // Arrange var metadata = new List { - new RestApiOperationParameter( + new( name: "p1", type: "array", isRequired: false, @@ -385,7 +385,7 @@ public void ItShouldCreateAmpersandSeparatedParameterPerArrayItemForPipeDelimite location: RestApiOperationParameterLocation.Query, style: RestApiOperationParameterStyle.PipeDelimited, arrayItemType: "string"), - new RestApiOperationParameter( + new( name: "p2", type: "array", isRequired: false, @@ -418,7 +418,7 @@ public void ItShouldCreateParameterWithPipeSeparatedValuePerArrayItemForPipeDeli // Arrange var metadata = new List { - new RestApiOperationParameter( + new( name: "p1", type: "array", isRequired: false, @@ -426,7 +426,7 @@ public void ItShouldCreateParameterWithPipeSeparatedValuePerArrayItemForPipeDeli location: RestApiOperationParameterLocation.Query, style: RestApiOperationParameterStyle.PipeDelimited, arrayItemType: "string"), - new RestApiOperationParameter( + new( name: "p2", type: "array", isRequired: false, @@ -460,25 +460,25 @@ public void ItShouldMixAndMatchParametersOfDifferentTypesAndStyles() var metadata = new List { //'Form' style array parameter with comma separated values - new RestApiOperationParameter(name: "p1", type: "array", isRequired: true, expand: false, location: RestApiOperationParameterLocation.Query, style: RestApiOperationParameterStyle.Form, arrayItemType: "string"), + new(name: "p1", type: "array", isRequired: true, expand: false, location: RestApiOperationParameterLocation.Query, style: RestApiOperationParameterStyle.Form, arrayItemType: "string"), //'Form' style primitive boolean parameter - new RestApiOperationParameter(name: "p2", type: "boolean", isRequired: true, expand: false, location: RestApiOperationParameterLocation.Query, style: RestApiOperationParameterStyle.Form), + new(name: "p2", type: "boolean", isRequired: true, expand: false, location: RestApiOperationParameterLocation.Query, style: RestApiOperationParameterStyle.Form), //'Form' style array parameter with parameter per array item - new RestApiOperationParameter(name : "p3", type : "array", isRequired : true, expand : true, location : RestApiOperationParameterLocation.Query, style : RestApiOperationParameterStyle.Form), + new(name : "p3", type : "array", isRequired : true, expand : true, location : RestApiOperationParameterLocation.Query, style : RestApiOperationParameterStyle.Form), //'SpaceDelimited' style array parameter with space separated values - new RestApiOperationParameter(name : "p4", type : "array", isRequired : true, expand : false, location : RestApiOperationParameterLocation.Query, style : RestApiOperationParameterStyle.SpaceDelimited), + new(name : "p4", type : "array", isRequired : true, expand : false, location : RestApiOperationParameterLocation.Query, style : RestApiOperationParameterStyle.SpaceDelimited), //'SpaceDelimited' style array parameter with parameter per array item - new RestApiOperationParameter(name : "p5", type : "array", isRequired : true, expand : true, location : RestApiOperationParameterLocation.Query, style : RestApiOperationParameterStyle.SpaceDelimited), + new(name : "p5", type : "array", isRequired : true, expand : true, location : RestApiOperationParameterLocation.Query, style : RestApiOperationParameterStyle.SpaceDelimited), //'PipeDelimited' style array parameter with pipe separated values - new RestApiOperationParameter(name : "p6", type : "array", isRequired : true, expand : false, location : RestApiOperationParameterLocation.Query, style : RestApiOperationParameterStyle.PipeDelimited), + new(name : "p6", type : "array", isRequired : true, expand : false, location : RestApiOperationParameterLocation.Query, style : RestApiOperationParameterStyle.PipeDelimited), //'PipeDelimited' style array parameter with parameter per array item - new RestApiOperationParameter(name : "p7", type : "array", isRequired : true, expand : true, location : RestApiOperationParameterLocation.Query, style : RestApiOperationParameterStyle.PipeDelimited), + new(name : "p7", type : "array", isRequired : true, expand : true, location : RestApiOperationParameterLocation.Query, style : RestApiOperationParameterStyle.PipeDelimited), }; var arguments = new Dictionary diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenAPI/Extensions/KernelAIPluginExtensionsTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/Extensions/KernelOpenApiPluginExtensionsTests.cs similarity index 91% rename from dotnet/src/Functions/Functions.UnitTests/OpenAPI/Extensions/KernelAIPluginExtensionsTests.cs rename to dotnet/src/Functions/Functions.UnitTests/OpenAPI/Extensions/KernelOpenApiPluginExtensionsTests.cs index 60a33d456a36..038ec72a3922 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenAPI/Extensions/KernelAIPluginExtensionsTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/Extensions/KernelOpenApiPluginExtensionsTests.cs @@ -18,7 +18,7 @@ namespace SemanticKernel.Functions.UnitTests.OpenAPI.Extensions; -public sealed class KernelAIPluginExtensionsTests : IDisposable +public sealed class KernelOpenApiPluginExtensionsTests : IDisposable { /// /// System under test - an instance of OpenApiDocumentParser class. @@ -36,9 +36,9 @@ public sealed class KernelAIPluginExtensionsTests : IDisposable private readonly IKernel _kernel; /// - /// Creates an instance of a class. + /// Creates an instance of a class. /// - public KernelAIPluginExtensionsTests() + public KernelOpenApiPluginExtensionsTests() { this._kernel = KernelBuilder.Create(); @@ -51,7 +51,7 @@ public KernelAIPluginExtensionsTests() public async Task ItCanIncludeOpenApiOperationParameterTypesIntoFunctionParametersViewAsync() { //Act - var plugin = await this._kernel.ImportPluginFunctionsAsync("fakePlugin", this._openApiDocument); + var plugin = await this._kernel.ImportOpenApiPluginFunctionsAsync("fakePlugin", this._openApiDocument); //Assert var setSecretFunction = plugin["SetSecret"]; @@ -96,7 +96,7 @@ public async Task ItUsesServerUrlOverrideIfProvidedAsync(bool removeServersPrope var variables = this.GetFakeContextVariables(); // Act - var plugin = await this._kernel.ImportPluginFunctionsAsync("fakePlugin", new Uri(DocumentUri), executionParameters); + var plugin = await this._kernel.ImportOpenApiPluginFunctionsAsync("fakePlugin", new Uri(DocumentUri), executionParameters); var setSecretFunction = plugin["SetSecret"]; messageHandlerStub.ResetResponse(); @@ -134,7 +134,7 @@ public async Task ItUsesServerUrlFromOpenApiDocumentAsync(string documentFileNam var variables = this.GetFakeContextVariables(); // Act - var plugin = await this._kernel.ImportPluginFunctionsAsync("fakePlugin", new Uri(DocumentUri), executionParameters); + var plugin = await this._kernel.ImportOpenApiPluginFunctionsAsync("fakePlugin", new Uri(DocumentUri), executionParameters); var setSecretFunction = plugin["SetSecret"]; messageHandlerStub.ResetResponse(); @@ -179,7 +179,7 @@ public async Task ItUsesOpenApiDocumentHostUrlWhenServerUrlIsNotProvidedAsync(st var variables = this.GetFakeContextVariables(); // Act - var plugin = await this._kernel.ImportPluginFunctionsAsync("fakePlugin", new Uri(documentUri), executionParameters); + var plugin = await this._kernel.ImportOpenApiPluginFunctionsAsync("fakePlugin", new Uri(documentUri), executionParameters); var setSecretFunction = plugin["SetSecret"]; messageHandlerStub.ResetResponse(); @@ -215,7 +215,7 @@ public async Task ItShouldConvertPluginComplexResponseToStringToSaveItInContextA var fakePlugin = new FakePlugin(); - var openApiPlugins = await this._kernel.ImportPluginFunctionsAsync("fakePlugin", this._openApiDocument, executionParameters); + var openApiPlugins = await this._kernel.ImportOpenApiPluginFunctionsAsync("fakePlugin", this._openApiDocument, executionParameters); var fakePlugins = this._kernel.ImportFunctions(fakePlugin); var kernel = KernelBuilder.Create(); diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenAPI/OpenAI/KernelOpenAIPluginExtensionsTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/OpenAI/KernelOpenAIPluginExtensionsTests.cs new file mode 100644 index 000000000000..4854b65c3653 --- /dev/null +++ b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/OpenAI/KernelOpenAIPluginExtensionsTests.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Functions.OpenAPI.OpenAI; +using Moq; +using SemanticKernel.Functions.UnitTests.OpenAPI.TestPlugins; +using Xunit; + +namespace SemanticKernel.Functions.UnitTests.OpenAPI.OpenAI; + +public sealed class KernelOpenAIPluginExtensionsTests : IDisposable +{ + /// + /// OpenAPI document stream. + /// + private readonly Stream _openApiDocument; + + /// + /// IKernel instance. + /// + private readonly IKernel _kernel; + + /// + /// Creates an instance of a class. + /// + public KernelOpenAIPluginExtensionsTests() + { + this._kernel = KernelBuilder.Create(); + + this._openApiDocument = ResourcePluginsProvider.LoadFromResource("documentV2_0.json"); + } + + [Fact] + public async Task ItUsesAuthFromOpenAiPluginManifestWhenFetchingOpenApiSpecAsync() + { + //Arrange + using var reader = new StreamReader(ResourcePluginsProvider.LoadFromResource("ai-plugin.json"), Encoding.UTF8); + JsonNode openAIDocumentContent = JsonNode.Parse(await reader.ReadToEndAsync())!; + var actualOpenAIAuthConfig = openAIDocumentContent["auth"].Deserialize()!; + + using var openAiDocument = ResourcePluginsProvider.LoadFromResource("ai-plugin.json"); + using var messageHandlerStub = new HttpMessageHandlerStub(this._openApiDocument); + + using var httpClient = new HttpClient(messageHandlerStub, false); + var authCallbackMock = new Mock(); + var executionParameters = new OpenAIFunctionExecutionParameters { HttpClient = httpClient, AuthCallback = authCallbackMock.Object }; + + var pluginName = "fakePlugin"; + + //Act + var plugin = await this._kernel.ImportOpenAIPluginFunctionsAsync(pluginName, openAiDocument, executionParameters); + + //Assert + var setSecretFunction = plugin["SetSecret"]; + Assert.NotNull(setSecretFunction); + + authCallbackMock.Verify(target => target.Invoke( + It.IsAny(), + It.Is(expectedPluginName => expectedPluginName == pluginName), + It.Is(expectedOpenAIAuthConfig => expectedOpenAIAuthConfig.Scope == actualOpenAIAuthConfig.Scope)), + Times.Exactly(1)); + } + + public void Dispose() + { + this._openApiDocument.Dispose(); + } +} diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenAPI/RestApiOperationTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/RestApiOperationTests.cs index 1a3f15c2312c..e288df4fc528 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenAPI/RestApiOperationTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/RestApiOperationTests.cs @@ -272,8 +272,8 @@ public void ItShouldSkipOptionalHeaderHavingNeitherValueNorDefaultValue() var metadata = new List { - new RestApiOperationParameter(name: "fake_header_one", type : "string", isRequired : true, expand : false, location : RestApiOperationParameterLocation.Header, style: RestApiOperationParameterStyle.Simple), - new RestApiOperationParameter(name : "fake_header_two", type : "string", isRequired : false, expand : false, location : RestApiOperationParameterLocation.Header, style : RestApiOperationParameterStyle.Simple) + new(name: "fake_header_one", type : "string", isRequired : true, expand : false, location : RestApiOperationParameterLocation.Header, style: RestApiOperationParameterStyle.Simple), + new(name : "fake_header_two", type : "string", isRequired : false, expand : false, location : RestApiOperationParameterLocation.Header, style : RestApiOperationParameterStyle.Simple) }; var arguments = new Dictionary diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenAPI/TestPlugins/ai-plugin.json b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/TestPlugins/ai-plugin.json new file mode 100644 index 000000000000..c72d1d4064fc --- /dev/null +++ b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/TestPlugins/ai-plugin.json @@ -0,0 +1,24 @@ +{ + "schema_version": "v1", + "name_for_human": "AzureKeyVault", + "name_for_model": "AzureKeyVault", + "description_for_human": "Query and interact with Azure Key Vault", + "description_for_model": "Query and interact with Azure Key Vault", + "auth": { + "type": "oauth", + "client_url": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + "scope": "https://vault.azure.net/.default", + "authorization_url": "https://login.microsoftonline.com/common/oauth2/v2.0/token", + "authorization_content_type": "application/x-www-form-urlencoded", + "verification_tokens": { + "openai": "00000000000000000000000000000000" + } + }, + "api": { + "type": "openapi", + "url": "http://localhost:3001/openapi.json" + }, + "logo_url": "https://contoso.com/logo.png", + "contact_email": "contact@contoso.com", + "legal_info_url": "https://privacy.microsoft.com/en-US/privacystatement" +} \ No newline at end of file diff --git a/dotnet/src/IntegrationTests/Planners/StepwisePlanner/StepwisePlannerTests.cs b/dotnet/src/IntegrationTests/Planners/StepwisePlanner/StepwisePlannerTests.cs index 8d262e58486f..ad4ef2b741e2 100644 --- a/dotnet/src/IntegrationTests/Planners/StepwisePlanner/StepwisePlannerTests.cs +++ b/dotnet/src/IntegrationTests/Planners/StepwisePlanner/StepwisePlannerTests.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Diagnostics; -using Microsoft.SemanticKernel.Functions.OpenAPI.Extensions; +using Microsoft.SemanticKernel.Functions.OpenAPI.OpenAI; using Microsoft.SemanticKernel.Planners; using Microsoft.SemanticKernel.Plugins.Core; using Microsoft.SemanticKernel.Plugins.Web; @@ -125,7 +125,7 @@ public async Task ExecutePlanSucceedsWithAlmostTooManyFunctionsAsync() // Arrange IKernel kernel = this.InitializeKernel(); - _ = await kernel.ImportPluginFunctionsAsync("Klarna", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"), new OpenApiFunctionExecutionParameters(enableDynamicOperationPayload: true)); + _ = await kernel.ImportOpenAIPluginFunctionsAsync("Klarna", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"), new OpenAIFunctionExecutionParameters(enableDynamicOperationPayload: true)); var planner = new Microsoft.SemanticKernel.Planners.StepwisePlanner(kernel); diff --git a/dotnet/src/IntegrationTests/Plugins/PluginTests.cs b/dotnet/src/IntegrationTests/Plugins/PluginTests.cs index bde065e44624..688df51b9f37 100644 --- a/dotnet/src/IntegrationTests/Plugins/PluginTests.cs +++ b/dotnet/src/IntegrationTests/Plugins/PluginTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Functions.OpenAPI.Extensions; +using Microsoft.SemanticKernel.Functions.OpenAPI.OpenAI; using Microsoft.SemanticKernel.Orchestration; using Xunit; @@ -13,8 +14,37 @@ public class PluginTests { [Theory] [InlineData("https://www.klarna.com/.well-known/ai-plugin.json", "Klarna", "productsUsingGET", "Laptop", 3, 200, "US")] + public async Task QueryKlarnaOpenAIPluginAsync( + string pluginEndpoint, + string name, + string functionName, + string query, + int size, + int budget, + string countryCode) + { + // Arrange + var kernel = new KernelBuilder().Build(); + using HttpClient httpClient = new(); + + var plugin = await kernel.ImportOpenAIPluginFunctionsAsync( + name, + new Uri(pluginEndpoint), + new OpenAIFunctionExecutionParameters(httpClient)); + + var contextVariables = new ContextVariables(); + contextVariables["q"] = query; + contextVariables["size"] = size.ToString(System.Globalization.CultureInfo.InvariantCulture); + contextVariables["budget"] = budget.ToString(System.Globalization.CultureInfo.InvariantCulture); + contextVariables["countryCode"] = countryCode; + + // Act + await plugin[functionName].InvokeAsync(kernel.CreateNewContext(contextVariables)); + } + + [Theory] [InlineData("https://www.klarna.com/us/shopping/public/openai/v0/api-docs/", "Klarna", "productsUsingGET", "Laptop", 3, 200, "US")] - public async Task QueryKlarnaPluginAsync( + public async Task QueryKlarnaOpenApiPluginAsync( string pluginEndpoint, string name, string functionName, @@ -27,7 +57,7 @@ public async Task QueryKlarnaPluginAsync( var kernel = new KernelBuilder().Build(); using HttpClient httpClient = new(); - var plugin = await kernel.ImportPluginFunctionsAsync( + var plugin = await kernel.ImportOpenApiPluginFunctionsAsync( name, new Uri(pluginEndpoint), new OpenApiFunctionExecutionParameters(httpClient)); @@ -59,10 +89,10 @@ public async Task QueryInstacartPluginAsync( using HttpClient httpClient = new(); //note that this plugin is not compliant according to the underlying validator in SK - var plugin = await kernel.ImportPluginFunctionsAsync( + var plugin = await kernel.ImportOpenAIPluginFunctionsAsync( name, new Uri(pluginEndpoint), - new OpenApiFunctionExecutionParameters(httpClient) { IgnoreNonCompliantErrors = true }); + new OpenAIFunctionExecutionParameters(httpClient) { IgnoreNonCompliantErrors = true }); var contextVariables = new ContextVariables(); contextVariables["payload"] = payload; @@ -90,10 +120,10 @@ public async Task QueryInstacartPluginFromStreamAsync( using HttpClient httpClient = new(); //note that this plugin is not compliant according to the underlying validator in SK - var plugin = await kernel.ImportPluginFunctionsAsync( + var plugin = await kernel.ImportOpenAIPluginFunctionsAsync( name, stream, - new OpenApiFunctionExecutionParameters(httpClient) { IgnoreNonCompliantErrors = true }); + new OpenAIFunctionExecutionParameters(httpClient) { IgnoreNonCompliantErrors = true }); var contextVariables = new ContextVariables(); contextVariables["payload"] = payload; @@ -120,10 +150,10 @@ public async Task QueryInstacartPluginUsingRelativeFilePathAsync( using HttpClient httpClient = new(); //note that this plugin is not compliant according to the underlying validator in SK - var plugin = await kernel.ImportPluginFunctionsAsync( + var plugin = await kernel.ImportOpenAIPluginFunctionsAsync( name, pluginFilePath, - new OpenApiFunctionExecutionParameters(httpClient) { IgnoreNonCompliantErrors = true }); + new OpenAIFunctionExecutionParameters(httpClient) { IgnoreNonCompliantErrors = true }); var contextVariables = new ContextVariables(); contextVariables["payload"] = payload;