diff --git a/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiPlugin_AzureKeyVault.cs b/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiPlugin_AzureKeyVault.cs index b92be76a52..4199e6847f 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiPlugin_AzureKeyVault.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example22_OpenApiPlugin_AzureKeyVault.cs @@ -49,7 +49,7 @@ public static class Example22_OpenApiPlugin_AzureKeyVault var plugin = await kernel.ImportPluginFunctionsAsync( PluginResourceNames.AzureKeyVault, stream!, - new OpenApiFunctionExecutionParameters { AuthCallback = authenticationProvider.AuthenticateRequestAsync }); + new OpenApiFunctionExecutionParameters { AuthenticateCallbackProvider = (manifest) => authenticationProvider.AuthenticateRequestAsync }); // Add arguments for required parameters, arguments for optional ones can be skipped. var contextVariables = new ContextVariables(); @@ -80,7 +80,7 @@ public static class Example22_OpenApiPlugin_AzureKeyVault stream!, new OpenApiFunctionExecutionParameters { - AuthCallback = authenticationProvider.AuthenticateRequestAsync, + AuthenticateCallbackProvider = _ => authenticationProvider.AuthenticateRequestAsync, EnableDynamicPayload = true }); diff --git a/dotnet/samples/KernelSyntaxExamples/Example23_OpenApiPlugin_Github.cs b/dotnet/samples/KernelSyntaxExamples/Example23_OpenApiPlugin_Github.cs index f7872ce320..d1128a8ccb 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example23_OpenApiPlugin_Github.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example23_OpenApiPlugin_Github.cs @@ -35,7 +35,7 @@ public static class Example23_OpenApiPlugin_GitHub var plugin = await kernel.ImportPluginFunctionsAsync( "GitHubPlugin", "../../../../../../samples/dotnet/OpenApiPluginsExample/GitHubPlugin/openapi.json", - new OpenApiFunctionExecutionParameters { AuthCallback = authenticationProvider.AuthenticateRequestAsync }); + new OpenApiFunctionExecutionParameters { AuthenticateCallbackProvider = _ => authenticationProvider.AuthenticateRequestAsync }); // Add arguments for required parameters, arguments for optional ones can be skipped. var contextVariables = new ContextVariables(); @@ -69,7 +69,7 @@ public static class Example23_OpenApiPlugin_GitHub var plugin = await kernel.ImportPluginFunctionsAsync( "GitHubPlugin", "../../../../../../samples/dotnet/OpenApiPluginsExample/GitHubPlugin/openapi.json", - new OpenApiFunctionExecutionParameters { AuthCallback = authenticationProvider.AuthenticateRequestAsync }); + new OpenApiFunctionExecutionParameters { AuthenticateCallbackProvider = _ => authenticationProvider.AuthenticateRequestAsync }); // Add arguments for required parameters, arguments for optional ones can be skipped. var contextVariables = new ContextVariables(); diff --git a/dotnet/samples/KernelSyntaxExamples/Example24_OpenApiPlugin_Jira.cs b/dotnet/samples/KernelSyntaxExamples/Example24_OpenApiPlugin_Jira.cs index 41c4188e21..73ae6a9335 100644 --- a/dotnet/samples/KernelSyntaxExamples/Example24_OpenApiPlugin_Jira.cs +++ b/dotnet/samples/KernelSyntaxExamples/Example24_OpenApiPlugin_Jira.cs @@ -43,12 +43,12 @@ public static class Example24_OpenApiPlugin_Jira if (useLocalFile) { var apiPluginFile = "./../../../Plugins/JiraPlugin/openapi.json"; - jiraFunctions = await kernel.ImportPluginFunctionsAsync("jiraPlugin", apiPluginFile, new OpenApiFunctionExecutionParameters(authCallback: tokenProvider.AuthenticateRequestAsync)); + jiraFunctions = await kernel.ImportPluginFunctionsAsync("jiraPlugin", apiPluginFile, new OpenApiFunctionExecutionParameters(authCallbackProvider: (_) => 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.ImportPluginFunctionsAsync("jiraPlugin", apiPluginRawFileURL, new OpenApiFunctionExecutionParameters(authCallbackProvider: (_) => tokenProvider.AuthenticateRequestAsync)); } // GetIssue Function diff --git a/dotnet/src/Functions/Functions.OpenAPI/Authentication/AuthenticateCallbackProvider.cs b/dotnet/src/Functions/Functions.OpenAPI/Authentication/AuthenticateCallbackProvider.cs new file mode 100644 index 0000000000..64b1abd2ff --- /dev/null +++ b/dotnet/src/Functions/Functions.OpenAPI/Authentication/AuthenticateCallbackProvider.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Functions.OpenAPI.Authentication; + +/// +/// Represents a delegate that provides authentication callback based on plugin manifest auth config. +/// +/// A representing the asynchronous operation. +public delegate AuthenticateRequestAsyncCallback AuthenticateCallbackProvider(OpenAIAuthenticationManifest? manifest = null); diff --git a/dotnet/src/Functions/Functions.OpenAPI/Authentication/OpenAIAuthenticationManifest.cs b/dotnet/src/Functions/Functions.OpenAPI/Authentication/OpenAIAuthenticationManifest.cs new file mode 100644 index 0000000000..815f975862 --- /dev/null +++ b/dotnet/src/Functions/Functions.OpenAPI/Authentication/OpenAIAuthenticationManifest.cs @@ -0,0 +1,98 @@ +// 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.Authentication; + +/// +/// Represents the authentication configuration for an OpenAI plugin. +/// +public record OpenAIAuthenticationManifest +{ + /// + /// The type of authentication. + /// + public OpenAIAuthenticationType Type { get; set; } = OpenAIAuthenticationType.None; + + /// + /// The type of authorization. + /// + public OpenAIAuthorizationType? AuthorizationType { get; set; } + + /// + /// The client URL. + /// + public Uri? ClientUrl { get; set; } + + /// + /// The authorization URL. + /// + public Uri? AuthorizationUrl { get; set; } + + /// + /// The authorization content type. + /// + public string? AuthorizationContentType { get; set; } + + /// + /// The authorization scope. + /// + public string? Scope { get; set; } + + /// + /// The verification tokens. + /// + public Dictionary? VerificationTokens { get; set; } +} + +/// +/// Represents the type of authentication for an OpenAI plugin. +/// +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 +} diff --git a/dotnet/src/Functions/Functions.OpenAPI/Authentication/OpenAIAuthenticationProvider.cs b/dotnet/src/Functions/Functions.OpenAPI/Authentication/OpenAIAuthenticationProvider.cs new file mode 100644 index 0000000000..897a047362 --- /dev/null +++ b/dotnet/src/Functions/Functions.OpenAPI/Authentication/OpenAIAuthenticationProvider.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Functions.OpenAPI.Authentication; + +/// +/// An example of OpenAI auth provider. +/// +public class OpenAIAuthenticationProvider +{ + private readonly OpenAIAuthenticationManifest _manifest; + + /// + /// Creates an instance of the class. + /// + /// OpenAI auth manifest + public OpenAIAuthenticationProvider(OpenAIAuthenticationManifest manifest) + { + this._manifest = manifest; + } + + /// + /// Applies the token to the provided HTTP request message. + /// + /// The HTTP request message. + public async Task AuthenticateRequestAsync(HttpRequestMessage request) + { + var token = "fake-token"; //Implement all the required logic to parse the manifest and issue an auth token according to the manifest. + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + } +} diff --git a/dotnet/src/Functions/Functions.OpenAPI/Extensions/KernelAIPluginExtensions.cs b/dotnet/src/Functions/Functions.OpenAPI/Extensions/KernelAIPluginExtensions.cs index ff0ba2a22f..8073cd6546 100644 --- a/dotnet/src/Functions/Functions.OpenAPI/Extensions/KernelAIPluginExtensions.cs +++ b/dotnet/src/Functions/Functions.OpenAPI/Extensions/KernelAIPluginExtensions.cs @@ -15,9 +15,13 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Functions.OpenAPI.Authentication; using Microsoft.SemanticKernel.Functions.OpenAPI.Model; using Microsoft.SemanticKernel.Functions.OpenAPI.OpenApi; using Microsoft.SemanticKernel.Orchestration; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using Newtonsoft.Json; namespace Microsoft.SemanticKernel.Functions.OpenAPI.Extensions; @@ -119,8 +123,9 @@ public static class KernelAIPluginExtensions var pluginContents = await LoadDocumentFromUriAsync( kernel, uri, - executionParameters, httpClient, + executionParameters?.UserAgent, + executionParameters?.AuthenticateCallbackProvider?.Invoke(null), cancellationToken).ConfigureAwait(false); return await CompleteImportAsync( @@ -178,15 +183,27 @@ public static class KernelAIPluginExtensions Uri? documentUri = null, CancellationToken cancellationToken = default) { + var document = pluginContents; + + AuthenticateRequestAsyncCallback? authCallback = null; + if (TryParseAIPluginForUrl(pluginContents, out var openApiUrl)) { - return await kernel - .ImportPluginFunctionsAsync( - pluginName, - new Uri(openApiUrl), - executionParameters, - cancellationToken: cancellationToken) - .ConfigureAwait(false); + if (TryParseAIPluginForAuth(document, out var openApiManifestAuth) && openApiManifestAuth!.Type != OpenAIAuthenticationType.None) + { + if (executionParameters?.AuthenticateCallbackProvider != null) + { + authCallback = executionParameters.AuthenticateCallbackProvider(openApiManifestAuth); + } + } + + document = await LoadDocumentFromUriAsync( + kernel, + new Uri(openApiUrl), + httpClient, + executionParameters?.UserAgent, + authCallback, + cancellationToken).ConfigureAwait(false); } return await LoadPluginAsync( @@ -194,7 +211,8 @@ public static class KernelAIPluginExtensions pluginName, executionParameters, httpClient, - pluginContents, + document, + authCallback, documentUri, cancellationToken).ConfigureAwait(false); } @@ -205,6 +223,7 @@ public static class KernelAIPluginExtensions OpenApiFunctionExecutionParameters? executionParameters, HttpClient httpClient, string pluginJson, + AuthenticateRequestAsyncCallback? authCallback, Uri? documentUri = null, CancellationToken cancellationToken = default) { @@ -216,7 +235,7 @@ public static class KernelAIPluginExtensions var runner = new RestApiOperationRunner( httpClient, - executionParameters?.AuthCallback, + authCallback, executionParameters?.UserAgent, executionParameters?.EnableDynamicPayload ?? false, executionParameters?.EnablePayloadNamespacing ?? false); @@ -247,15 +266,20 @@ public static class KernelAIPluginExtensions private static async Task LoadDocumentFromUriAsync( IKernel kernel, Uri uri, - OpenApiFunctionExecutionParameters? executionParameters, HttpClient httpClient, + string? userAgent, + AuthenticateRequestAsyncCallback? authCallback, CancellationToken cancellationToken) { - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri.ToString()); + using var request = new HttpRequestMessage(HttpMethod.Get, uri.ToString()); + request.Headers.UserAgent.Add(ProductInfoHeaderValue.Parse(userAgent ?? Telemetry.HttpUserAgent)); - requestMessage.Headers.UserAgent.Add(ProductInfoHeaderValue.Parse(executionParameters?.UserAgent ?? Telemetry.HttpUserAgent)); + if (authCallback != null) + { + await authCallback(request).ConfigureAwait(false); + } - using var response = await httpClient.SendWithSuccessCheckAsync(requestMessage, cancellationToken).ConfigureAwait(false); + using var response = await httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); return await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false); } @@ -322,6 +346,31 @@ public static class KernelAIPluginExtensions } } + private static bool TryParseAIPluginForAuth(string gptPluginJson, out OpenAIAuthenticationManifest? openApiManifestAuth) + { + try + { + var serializerSettings = new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + } + }; + + JObject? gptPlugin = JsonConvert.DeserializeObject(gptPluginJson, serializerSettings); + openApiManifestAuth = gptPlugin?["auth"]?.ToObject(JsonSerializer.Create(serializerSettings)); + + return openApiManifestAuth != null; + } + catch (System.Text.Json.JsonException) + { + openApiManifestAuth = null; + + return false; + } + } + /// /// Registers SKFunction for a REST API operation. /// diff --git a/dotnet/src/Functions/Functions.OpenAPI/Extensions/OpenApiFunctionExecutionParameters.cs b/dotnet/src/Functions/Functions.OpenAPI/Extensions/OpenApiFunctionExecutionParameters.cs index 5d7a045b19..f611e8fa4d 100644 --- a/dotnet/src/Functions/Functions.OpenAPI/Extensions/OpenApiFunctionExecutionParameters.cs +++ b/dotnet/src/Functions/Functions.OpenAPI/Extensions/OpenApiFunctionExecutionParameters.cs @@ -20,8 +20,14 @@ public class OpenApiFunctionExecutionParameters /// /// Callback for adding authentication data to HTTP requests. /// + [Obsolete("This property will be removed in one of the next releases. Please use the AuthenticateCallbackProvider property instead.")] public AuthenticateRequestAsyncCallback? AuthCallback { get; set; } + /// + /// Provider to get authentication callback. + /// + public AuthenticateCallbackProvider? AuthenticateCallbackProvider { get; set; } + /// /// Override for REST API operation server url. /// @@ -58,8 +64,8 @@ public class OpenApiFunctionExecutionParameters /// Initializes a new instance of the class. /// /// The HttpClient to use for sending HTTP requests. - /// The callback for adding authentication data to HTTP requests. /// The override for the REST API operation server URL. + /// The provider to return callback for adding authentication data to HTTP requests. /// Optional user agent header value. /// A flag indicating whether to ignore non-compliant errors or not /// If set to true, the operation execution will not throw exceptions for non-compliant documents. @@ -70,15 +76,15 @@ public class OpenApiFunctionExecutionParameters /// Namespaces prevent naming conflicts by adding the parent parameter name as a prefix, separated by dots. public OpenApiFunctionExecutionParameters( HttpClient? httpClient = null, - AuthenticateRequestAsyncCallback? authCallback = null, Uri? serverUrlOverride = null, + AuthenticateCallbackProvider? authCallbackProvider = null, string userAgent = Telemetry.HttpUserAgent, bool ignoreNonCompliantErrors = false, bool enableDynamicOperationPayload = false, bool enablePayloadNamespacing = false) { this.HttpClient = httpClient; - this.AuthCallback = authCallback; + this.AuthenticateCallbackProvider = authCallbackProvider; this.ServerUrlOverride = serverUrlOverride; this.UserAgent = userAgent; this.IgnoreNonCompliantErrors = ignoreNonCompliantErrors; diff --git a/dotnet/src/Functions/Functions.UnitTests/Functions.UnitTests.csproj b/dotnet/src/Functions/Functions.UnitTests/Functions.UnitTests.csproj index cc3d8351e0..4a210a75c4 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Functions.UnitTests.csproj +++ b/dotnet/src/Functions/Functions.UnitTests/Functions.UnitTests.csproj @@ -16,6 +16,7 @@ + @@ -23,6 +24,7 @@ + diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenAPI/Extensions/KernelAIPluginExtensionsTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/Extensions/KernelAIPluginExtensionsTests.cs index d8d34f1f1f..b481b3bfed 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenAPI/Extensions/KernelAIPluginExtensionsTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/Extensions/KernelAIPluginExtensionsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -9,10 +10,13 @@ using System.Net.Mime; using System.Text; using System.Threading.Tasks; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Functions.OpenAPI.Authentication; using Microsoft.SemanticKernel.Functions.OpenAPI.Extensions; using Microsoft.SemanticKernel.Functions.OpenAPI.Model; using Microsoft.SemanticKernel.Functions.OpenAPI.OpenApi; using Microsoft.SemanticKernel.Orchestration; +using Moq; using SemanticKernel.Functions.UnitTests.OpenAPI.TestPlugins; using Xunit; @@ -47,6 +51,60 @@ public sealed class KernelAIPluginExtensionsTests : IDisposable this._sut = new OpenApiDocumentParser(); } + [Fact] + public async Task ItUsesAuthFromOpenAiPluginManifestWhenFetchingOpenApiSpecAsync() + { + //Arrange + using var messageHandlerStub = new HttpMessageHandlerStub((HttpRequestMessage message) => + { + if (message!.RequestUri!.AbsoluteUri == "https://fake-random-test-host/openai_manifest.json") + { + var response = new HttpResponseMessage(); + response.Content = new StreamContent(ResourcePluginsProvider.LoadFromResource("ai-plugin.json")); + return response; + } + + if (message!.RequestUri!.AbsoluteUri == "https://fake-random-test-host/openapi.json") + { + var response = new HttpResponseMessage(); + response.Content = new StreamContent(this._openApiDocument); + return response; + } + + throw new SKException($"Unexpected url - {message!.RequestUri!.AbsoluteUri}"); + }); + + using var httpClient = new HttpClient(messageHandlerStub, false); + + var providerActualArguments = new List(); + + var authCallbackMock = new Mock(); + + var authCallbackProviderMock = new Mock(); + authCallbackProviderMock + .Setup(p => p.Invoke(It.IsAny())) + .Callback(c => providerActualArguments.Add(c)) + .Returns(authCallbackMock.Object); + + //Act + var plugin = await this._kernel.ImportPluginFunctionsAsync( + "fakePlugin", + new Uri("https://fake-random-test-host/openai_manifest.json"), + new OpenApiFunctionExecutionParameters { HttpClient = httpClient, AuthenticateCallbackProvider = authCallbackProviderMock.Object }); + + //Assert + var setSecretFunction = plugin["SetSecret"]; + Assert.NotNull(setSecretFunction); + + //Check provider is called two times: first time with null when loading the OpenAI manifest document and the second time with auth config when loading the OpenAPI document. + authCallbackProviderMock.Verify(target => target.Invoke(It.IsAny()), Times.Exactly(2)); + Assert.Equal(2, providerActualArguments.Count); + Assert.Null(providerActualArguments[0]); + Assert.Equal("https://vault.azure.net/.default", providerActualArguments[1]?.Scope); + + authCallbackMock.Verify(target => target.Invoke(It.IsAny()), Times.Exactly(2)); + } + [Fact] public async Task ItCanIncludeOpenApiOperationParameterTypesIntoFunctionParametersViewAsync() { @@ -203,8 +261,8 @@ public sealed class KernelAIPluginExtensionsTests : IDisposable public async Task ItShouldConvertPluginComplexResponseToStringToSaveItInContextAsync() { //Arrange - using var messageHandlerStub = new HttpMessageHandlerStub(); - messageHandlerStub.ResponseToReturn.Content = new StringContent("fake-content", Encoding.UTF8, MediaTypeNames.Application.Json); + using var content = new StringContent("fake-content", Encoding.UTF8, MediaTypeNames.Application.Json); + using var messageHandlerStub = new HttpMessageHandlerStub(content); using var httpClient = new HttpClient(messageHandlerStub, false); diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenAPI/HttpMessageHandlerStub.cs b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/HttpMessageHandlerStub.cs index 542aa33a7c..c7ebba55f1 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenAPI/HttpMessageHandlerStub.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenAPI/HttpMessageHandlerStub.cs @@ -23,24 +23,51 @@ internal sealed class HttpMessageHandlerStub : DelegatingHandler public HttpMethod? Method { get; private set; } - public HttpResponseMessage ResponseToReturn { get; set; } + public Func ResponseToReturnProvider { get; set; } public HttpMessageHandlerStub() { - this.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK); - this.ResponseToReturn.Content = new StringContent("{}", Encoding.UTF8, MediaTypeNames.Application.Json); + this.ResponseToReturnProvider = (_) => + { + var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK); + response.Content = new StringContent("{}", Encoding.UTF8, MediaTypeNames.Application.Json); + return response; + }; } public HttpMessageHandlerStub(Stream responseToReturn) { - this.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK); - this.ResponseToReturn.Content = new StreamContent(responseToReturn); + this.ResponseToReturnProvider = (_) => + { + var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK); + response.Content = new StreamContent(responseToReturn); + return response; + }; + } + + public HttpMessageHandlerStub(HttpContent content) + { + this.ResponseToReturnProvider = (_) => + { + var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK); + response.Content = content; + return response; + }; + } + + public HttpMessageHandlerStub(Func responseToReturn) + { + this.ResponseToReturnProvider = responseToReturn; } public void ResetResponse() { - this.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK); - this.ResponseToReturn.Content = new StringContent("{}", Encoding.UTF8, MediaTypeNames.Application.Json); + this.ResponseToReturnProvider = (_) => + { + var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK); + response.Content = new StringContent("{}", Encoding.UTF8, MediaTypeNames.Application.Json); + return response; + }; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) @@ -51,6 +78,6 @@ internal sealed class HttpMessageHandlerStub : DelegatingHandler this.RequestContent = request.Content == null ? null : await request.Content.ReadAsByteArrayAsync(cancellationToken); this.ContentHeaders = request.Content?.Headers; - return await Task.FromResult(this.ResponseToReturn); + return await Task.FromResult(this.ResponseToReturnProvider(request)); } } 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 0000000000..5b58f7847d --- /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": "https://fake-random-test-host/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