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));
+ }
+}