diff --git a/dotnet/src/Functions/Functions.OpenApi.Extensions/Extensions/ApiManifestKernelExtensions.cs b/dotnet/src/Functions/Functions.OpenApi.Extensions/Extensions/ApiManifestKernelExtensions.cs index dd2447b501a4..6c8ea84627b8 100644 --- a/dotnet/src/Functions/Functions.OpenApi.Extensions/Extensions/ApiManifestKernelExtensions.cs +++ b/dotnet/src/Functions/Functions.OpenApi.Extensions/Extensions/ApiManifestKernelExtensions.cs @@ -8,6 +8,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -31,6 +32,7 @@ public static class ApiManifestKernelExtensions /// The name of the plugin. /// The file path of the API manifest. /// Optional parameters for the plugin setup. + /// Optional chat client to use for request payload generation. /// Optional cancellation token. /// The imported plugin. public static async Task ImportPluginFromApiManifestAsync( @@ -38,9 +40,10 @@ public static async Task ImportPluginFromApiManifestAsync( string pluginName, string filePath, ApiManifestPluginParameters? pluginParameters = null, + IChatClient? chatClient = null, CancellationToken cancellationToken = default) { - KernelPlugin plugin = await kernel.CreatePluginFromApiManifestAsync(pluginName, filePath, pluginParameters, cancellationToken).ConfigureAwait(false); + KernelPlugin plugin = await kernel.CreatePluginFromApiManifestAsync(pluginName, filePath, pluginParameters, chatClient, cancellationToken).ConfigureAwait(false); kernel.Plugins.Add(plugin); return plugin; } @@ -52,6 +55,7 @@ public static async Task ImportPluginFromApiManifestAsync( /// The name of the plugin. /// The file path of the API manifest. /// Optional parameters for the plugin setup. + /// Optional chat client to use for request payload generation. /// Optional cancellation token. /// A task that represents the asynchronous operation. The task result contains the created kernel plugin. public static async Task CreatePluginFromApiManifestAsync( @@ -59,6 +63,7 @@ public static async Task CreatePluginFromApiManifestAsync( string pluginName, string filePath, ApiManifestPluginParameters? pluginParameters = null, + IChatClient? chatClient = null, CancellationToken cancellationToken = default) { Verify.NotNull(kernel); @@ -148,13 +153,18 @@ await DocumentLoader.LoadDocumentFromUriAsStreamAsync(new Uri(apiDescriptionUrl) var operationRunnerHttpClient = HttpClientProvider.GetHttpClient(openApiFunctionExecutionParameters?.HttpClient ?? kernel.Services.GetService()); #pragma warning restore CA2000 - var runner = new RestApiOperationRunner( + IRestApiOperationRunner runner = new RestApiOperationRunner( operationRunnerHttpClient, openApiFunctionExecutionParameters?.AuthCallback, openApiFunctionExecutionParameters?.UserAgent, - openApiFunctionExecutionParameters?.EnableDynamicPayload ?? true, + openApiFunctionExecutionParameters?.EnableDynamicPayload ?? chatClient is null, openApiFunctionExecutionParameters?.EnablePayloadNamespacing ?? false); + if (chatClient is not null) + { + runner = new RestApiOperationRunnerPayloadProxy((RestApiOperationRunner)runner, chatClient); + } + var server = filteredOpenApiDocument.Servers.FirstOrDefault(); if (server?.Url is null) { diff --git a/dotnet/src/Functions/Functions.OpenApi.Extensions/Extensions/CopilotAgentPluginKernelExtensions.cs b/dotnet/src/Functions/Functions.OpenApi.Extensions/Extensions/CopilotAgentPluginKernelExtensions.cs index e9e401ff2960..bc97521af661 100644 --- a/dotnet/src/Functions/Functions.OpenApi.Extensions/Extensions/CopilotAgentPluginKernelExtensions.cs +++ b/dotnet/src/Functions/Functions.OpenApi.Extensions/Extensions/CopilotAgentPluginKernelExtensions.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -30,6 +31,7 @@ public static class CopilotAgentPluginKernelExtensions /// The name of the plugin. /// The file path of the Copilot Agent Plugin. /// Optional parameters for the plugin setup. + /// Optional chat client to use for request payload generation. /// Optional cancellation token. /// The imported plugin. public static async Task ImportPluginFromCopilotAgentPluginAsync( @@ -37,9 +39,10 @@ public static async Task ImportPluginFromCopilotAgentPluginAsync( string pluginName, string filePath, CopilotAgentPluginParameters? pluginParameters = null, + IChatClient? chatClient = null, CancellationToken cancellationToken = default) { - KernelPlugin plugin = await kernel.CreatePluginFromCopilotAgentPluginAsync(pluginName, filePath, pluginParameters, cancellationToken).ConfigureAwait(false); + KernelPlugin plugin = await kernel.CreatePluginFromCopilotAgentPluginAsync(pluginName, filePath, pluginParameters, chatClient, cancellationToken).ConfigureAwait(false); kernel.Plugins.Add(plugin); return plugin; } @@ -51,6 +54,7 @@ public static async Task ImportPluginFromCopilotAgentPluginAsync( /// The name of the plugin. /// The file path of the Copilot Agent Plugin. /// Optional parameters for the plugin setup. + /// Optional chat client to use for request payload generation. /// Optional cancellation token. /// A task that represents the asynchronous operation. The task result contains the created kernel plugin. public static async Task CreatePluginFromCopilotAgentPluginAsync( @@ -58,6 +62,7 @@ public static async Task CreatePluginFromCopilotAgentPluginAsync( string pluginName, string filePath, CopilotAgentPluginParameters? pluginParameters = null, + IChatClient? chatClient = null, CancellationToken cancellationToken = default) { Verify.NotNull(kernel); @@ -156,13 +161,18 @@ await DocumentLoader.LoadDocumentFromUriAsStreamAsync(parsedDescriptionUrl, var operationRunnerHttpClient = HttpClientProvider.GetHttpClient(openApiFunctionExecutionParameters?.HttpClient ?? kernel.Services.GetService()); #pragma warning restore CA2000 - var runner = new RestApiOperationRunner( + IRestApiOperationRunner runner = new RestApiOperationRunner( operationRunnerHttpClient, openApiFunctionExecutionParameters?.AuthCallback, openApiFunctionExecutionParameters?.UserAgent, - openApiFunctionExecutionParameters?.EnableDynamicPayload ?? true, + openApiFunctionExecutionParameters?.EnableDynamicPayload ?? chatClient is null, openApiFunctionExecutionParameters?.EnablePayloadNamespacing ?? false); + if (chatClient is not null) + { + runner = new RestApiOperationRunnerPayloadProxy((RestApiOperationRunner)runner, chatClient); + } + var info = OpenApiDocumentParser.ExtractRestApiInfo(filteredOpenApiDocument); var security = OpenApiDocumentParser.CreateRestApiOperationSecurityRequirements(filteredOpenApiDocument.SecurityRequirements); foreach (var path in filteredOpenApiDocument.Paths) diff --git a/dotnet/src/Functions/Functions.OpenApi/IRestApiOperationRunner.cs b/dotnet/src/Functions/Functions.OpenApi/IRestApiOperationRunner.cs new file mode 100644 index 000000000000..a3483a30e3a9 --- /dev/null +++ b/dotnet/src/Functions/Functions.OpenApi/IRestApiOperationRunner.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Plugins.OpenApi; + +internal interface IRestApiOperationRunner +{ + /// + /// Executes the specified asynchronously, using the provided . + /// + /// The REST API operation to execute. + /// The dictionary of arguments to be passed to the operation. + /// Options for REST API operation run. + /// The cancellation token. + /// The task execution result. + public Task RunAsync( + RestApiOperation operation, + KernelArguments arguments, + RestApiOperationRunOptions? options = null, + CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs b/dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs index f46d39e681f2..bf66e09b2988 100644 --- a/dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs +++ b/dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs @@ -253,7 +253,7 @@ internal static KernelPlugin CreateOpenApiPlugin( /// An instance of class. internal static KernelFunction CreateRestApiFunction( string pluginName, - RestApiOperationRunner runner, + IRestApiOperationRunner runner, RestApiInfo info, List? security, RestApiOperation operation, diff --git a/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs b/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs index d38ca1130e8a..4e27ea1b5778 100644 --- a/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs +++ b/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunner.cs @@ -18,9 +18,9 @@ namespace Microsoft.SemanticKernel.Plugins.OpenApi; /// /// Runs REST API operation represented by RestApiOperation model class. /// -internal sealed class RestApiOperationRunner +internal sealed class RestApiOperationRunner : IRestApiOperationRunner { - private const string MediaTypeApplicationJson = "application/json"; + internal const string MediaTypeApplicationJson = "application/json"; private const string MediaTypeTextPlain = "text/plain"; private const string DefaultResponseKey = "default"; @@ -78,7 +78,7 @@ internal sealed class RestApiOperationRunner /// Determines whether the operation payload is constructed dynamically based on operation payload metadata. /// If false, the operation payload must be provided via the 'payload' property. /// - private readonly bool _enableDynamicPayload; + internal bool EnableDynamicPayload { get; private set; } /// /// Determines whether payload parameters are resolved from the arguments by @@ -113,7 +113,7 @@ public RestApiOperationRunner( { this._httpClient = httpClient; this._userAgent = userAgent ?? HttpHeaderConstant.Values.UserAgent; - this._enableDynamicPayload = enableDynamicPayload; + this.EnableDynamicPayload = enableDynamicPayload; this._enablePayloadNamespacing = enablePayloadNamespacing; this._httpResponseContentReader = httpResponseContentReader; @@ -127,21 +127,14 @@ public RestApiOperationRunner( this._authCallback = authCallback; } - this._payloadFactoryByMediaType = new() + this._payloadFactoryByMediaType = new(StringComparer.OrdinalIgnoreCase) { { MediaTypeApplicationJson, this.BuildJsonPayload }, { MediaTypeTextPlain, this.BuildPlainTextPayload } }; } - /// - /// Executes the specified asynchronously, using the provided . - /// - /// The REST API operation to execute. - /// The dictionary of arguments to be passed to the operation. - /// Options for REST API operation run. - /// The cancellation token. - /// The task execution result. + /// public Task RunAsync( RestApiOperation operation, KernelArguments arguments, @@ -171,7 +164,7 @@ public Task RunAsync( /// Options for REST API operation run. /// The cancellation token. /// Response content and content type - private async Task SendAsync( + internal async Task SendAsync( Uri url, HttpMethod method, IDictionary? headers = null, @@ -346,7 +339,7 @@ private async Task ReadContentAndCreateOperationRespon private (object? Payload, HttpContent Content) BuildJsonPayload(RestApiPayload? payloadMetadata, IDictionary arguments) { // Build operation payload dynamically - if (this._enableDynamicPayload) + if (this.EnableDynamicPayload) { if (payloadMetadata is null) { @@ -477,7 +470,7 @@ private string GetArgumentNameForPayload(string propertyName, string? propertyNa /// Override for REST API operation server url. /// The URL of REST API host. /// The operation Url. - private Uri BuildsOperationUrl(RestApiOperation operation, IDictionary arguments, Uri? serverUrlOverride = null, Uri? apiHostUrl = null) + internal Uri BuildsOperationUrl(RestApiOperation operation, IDictionary arguments, Uri? serverUrlOverride = null, Uri? apiHostUrl = null) { var url = operation.BuildOperationUrl(arguments, serverUrlOverride, apiHostUrl); diff --git a/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunnerPayloadProxy.cs b/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunnerPayloadProxy.cs new file mode 100644 index 000000000000..74f999296e34 --- /dev/null +++ b/dotnet/src/Functions/Functions.OpenApi/RestApiOperationRunnerPayloadProxy.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; + +namespace Microsoft.SemanticKernel.Plugins.OpenApi; + +/// +/// Proxy that leverages a chat client to generate the request body payload before calling the target concrete function. +/// +internal sealed class RestApiOperationRunnerPayloadProxy : IRestApiOperationRunner +{ + private readonly RestApiOperationRunner _concrete; + private readonly IChatClient _chatClient; + + /// + /// Initializes a new instance of the class. + /// + /// Operation runner to call with the generated payload + /// Chat client to generate the payload + /// If the provided Operation runner argument is null + public RestApiOperationRunnerPayloadProxy(RestApiOperationRunner concrete, IChatClient chatClient) + { + Verify.NotNull(concrete); + Verify.NotNull(chatClient); + this._concrete = concrete; + this._chatClient = chatClient; + if (concrete.EnableDynamicPayload) + { + throw new InvalidOperationException("The concrete operation runner must not support dynamic payloads."); + } + } + + /// + public async Task RunAsync(RestApiOperation operation, KernelArguments arguments, RestApiOperationRunOptions? options = null, CancellationToken cancellationToken = default) + { + var url = this._concrete.BuildsOperationUrl(operation, arguments, options?.ServerUrlOverride, options?.ApiHostUrl); + + var headers = operation.BuildHeaders(arguments); + + var operationPayload = await this.BuildOperationPayloadAsync(operation, arguments, cancellationToken).ConfigureAwait(false); + + return await this._concrete.SendAsync(url, operation.Method, headers, operationPayload.Payload, operationPayload.Content, operation.Responses.ToDictionary(static item => item.Key, static item => item.Value.Schema), options, cancellationToken).ConfigureAwait(false); + } + /// + /// Builds operation payload. + /// + /// The operation. + /// The operation payload arguments. + /// The cancellation token. + /// The raw operation payload and the corresponding HttpContent. + private Task<(object? Payload, HttpContent? Content)> BuildOperationPayloadAsync(RestApiOperation operation, IDictionary arguments, CancellationToken cancellationToken) + { + if (operation.Payload is null && !arguments.ContainsKey(RestApiOperation.PayloadArgumentName)) + { + return Task.FromResult<(object?, HttpContent?)>((null, null)); + } + + var mediaType = operation.Payload?.MediaType; + if (string.IsNullOrEmpty(mediaType)) + { + if (!arguments.TryGetValue(RestApiOperation.ContentTypeArgumentName, out object? fallback) || fallback is not string mediaTypeFallback) + { + throw new KernelException($"No media type is provided for the {operation.Id} operation."); + } + + mediaType = mediaTypeFallback; + } + + if (!RestApiOperationRunner.MediaTypeApplicationJson.Equals(mediaType!, StringComparison.OrdinalIgnoreCase)) + { + throw new KernelException($"The media type {mediaType} of the {operation.Id} operation is not supported by {nameof(RestApiOperationRunnerPayloadProxy)}."); + } + + return this.BuildJsonPayloadAsync(operation.Payload, arguments, cancellationToken); + } + /// + /// Builds "application/json" payload. + /// + /// The payload meta-data. + /// The payload arguments. + /// The cancellation token. + /// The JSON payload the corresponding HttpContent. + private async Task<(object? Payload, HttpContent? Content)> BuildJsonPayloadAsync(RestApiPayload? payloadMetadata, IDictionary arguments, CancellationToken cancellationToken) + { + string message = + """ + Given the following JSON schema, and the following context, generate the JSON payload: + """; + //TODO get the schema from the arguments, and the context + + var completion = await this._chatClient.CompleteAsync(message, cancellationToken: cancellationToken).ConfigureAwait(false); + var content = completion.Message.Text; + if (string.IsNullOrEmpty(content)) + { + throw new KernelException("The chat client did not provide a JSON payload."); + } + return (content!, new StringContent(content!, Encoding.UTF8, RestApiOperationRunner.MediaTypeApplicationJson)); + } +}