Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net: feature/llm openapi payload #9741

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,16 +32,18 @@ public static class ApiManifestKernelExtensions
/// <param name="pluginName">The name of the plugin.</param>
/// <param name="filePath">The file path of the API manifest.</param>
/// <param name="pluginParameters">Optional parameters for the plugin setup.</param>
/// <param name="chatClient">Optional chat client to use for request payload generation.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>The imported plugin.</returns>
public static async Task<KernelPlugin> ImportPluginFromApiManifestAsync(
this Kernel kernel,
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;
}
Expand All @@ -52,13 +55,15 @@ public static async Task<KernelPlugin> ImportPluginFromApiManifestAsync(
/// <param name="pluginName">The name of the plugin.</param>
/// <param name="filePath">The file path of the API manifest.</param>
/// <param name="pluginParameters">Optional parameters for the plugin setup.</param>
/// <param name="chatClient">Optional chat client to use for request payload generation.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the created kernel plugin.</returns>
public static async Task<KernelPlugin> CreatePluginFromApiManifestAsync(
this Kernel kernel,
string pluginName,
string filePath,
ApiManifestPluginParameters? pluginParameters = null,
IChatClient? chatClient = null,
CancellationToken cancellationToken = default)
{
Verify.NotNull(kernel);
Expand Down Expand Up @@ -148,13 +153,18 @@ await DocumentLoader.LoadDocumentFromUriAsStreamAsync(new Uri(apiDescriptionUrl)
var operationRunnerHttpClient = HttpClientProvider.GetHttpClient(openApiFunctionExecutionParameters?.HttpClient ?? kernel.Services.GetService<HttpClient>());
#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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,16 +31,18 @@ public static class CopilotAgentPluginKernelExtensions
/// <param name="pluginName">The name of the plugin.</param>
/// <param name="filePath">The file path of the Copilot Agent Plugin.</param>
/// <param name="pluginParameters">Optional parameters for the plugin setup.</param>
/// <param name="chatClient">Optional chat client to use for request payload generation.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>The imported plugin.</returns>
public static async Task<KernelPlugin> ImportPluginFromCopilotAgentPluginAsync(
this Kernel kernel,
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;
}
Expand All @@ -51,13 +54,15 @@ public static async Task<KernelPlugin> ImportPluginFromCopilotAgentPluginAsync(
/// <param name="pluginName">The name of the plugin.</param>
/// <param name="filePath">The file path of the Copilot Agent Plugin.</param>
/// <param name="pluginParameters">Optional parameters for the plugin setup.</param>
/// <param name="chatClient">Optional chat client to use for request payload generation.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the created kernel plugin.</returns>
public static async Task<KernelPlugin> CreatePluginFromCopilotAgentPluginAsync(
this Kernel kernel,
string pluginName,
string filePath,
CopilotAgentPluginParameters? pluginParameters = null,
IChatClient? chatClient = null,
CancellationToken cancellationToken = default)
{
Verify.NotNull(kernel);
Expand Down Expand Up @@ -156,13 +161,18 @@ await DocumentLoader.LoadDocumentFromUriAsStreamAsync(parsedDescriptionUrl,
var operationRunnerHttpClient = HttpClientProvider.GetHttpClient(openApiFunctionExecutionParameters?.HttpClient ?? kernel.Services.GetService<HttpClient>());
#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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Executes the specified <paramref name="operation"/> asynchronously, using the provided <paramref name="arguments"/>.
/// </summary>
/// <param name="operation">The REST API operation to execute.</param>
/// <param name="arguments">The dictionary of arguments to be passed to the operation.</param>
/// <param name="options">Options for REST API operation run.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The task execution result.</returns>
public Task<RestApiOperationResponse> RunAsync(
RestApiOperation operation,
KernelArguments arguments,
RestApiOperationRunOptions? options = null,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ internal static KernelPlugin CreateOpenApiPlugin(
/// <returns>An instance of <see cref="KernelFunctionFromPrompt"/> class.</returns>
internal static KernelFunction CreateRestApiFunction(
string pluginName,
RestApiOperationRunner runner,
IRestApiOperationRunner runner,
RestApiInfo info,
List<RestApiSecurityRequirement>? security,
RestApiOperation operation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ namespace Microsoft.SemanticKernel.Plugins.OpenApi;
/// <summary>
/// Runs REST API operation represented by RestApiOperation model class.
/// </summary>
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";
Expand Down Expand Up @@ -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.
/// </summary>
private readonly bool _enableDynamicPayload;
internal bool EnableDynamicPayload { get; private set; }

/// <summary>
/// Determines whether payload parameters are resolved from the arguments by
Expand Down Expand Up @@ -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;

Expand All @@ -127,21 +127,14 @@ public RestApiOperationRunner(
this._authCallback = authCallback;
}

this._payloadFactoryByMediaType = new()
this._payloadFactoryByMediaType = new(StringComparer.OrdinalIgnoreCase)
{
{ MediaTypeApplicationJson, this.BuildJsonPayload },
{ MediaTypeTextPlain, this.BuildPlainTextPayload }
};
}

/// <summary>
/// Executes the specified <paramref name="operation"/> asynchronously, using the provided <paramref name="arguments"/>.
/// </summary>
/// <param name="operation">The REST API operation to execute.</param>
/// <param name="arguments">The dictionary of arguments to be passed to the operation.</param>
/// <param name="options">Options for REST API operation run.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The task execution result.</returns>
/// <inheritdoc />
public Task<RestApiOperationResponse> RunAsync(
RestApiOperation operation,
KernelArguments arguments,
Expand Down Expand Up @@ -171,7 +164,7 @@ public Task<RestApiOperationResponse> RunAsync(
/// <param name="options">Options for REST API operation run.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Response content and content type</returns>
private async Task<RestApiOperationResponse> SendAsync(
internal async Task<RestApiOperationResponse> SendAsync(
Uri url,
HttpMethod method,
IDictionary<string, string>? headers = null,
Expand Down Expand Up @@ -346,7 +339,7 @@ private async Task<RestApiOperationResponse> ReadContentAndCreateOperationRespon
private (object? Payload, HttpContent Content) BuildJsonPayload(RestApiPayload? payloadMetadata, IDictionary<string, object?> arguments)
{
// Build operation payload dynamically
if (this._enableDynamicPayload)
if (this.EnableDynamicPayload)
{
if (payloadMetadata is null)
{
Expand Down Expand Up @@ -477,7 +470,7 @@ private string GetArgumentNameForPayload(string propertyName, string? propertyNa
/// <param name="serverUrlOverride">Override for REST API operation server url.</param>
/// <param name="apiHostUrl">The URL of REST API host.</param>
/// <returns>The operation Url.</returns>
private Uri BuildsOperationUrl(RestApiOperation operation, IDictionary<string, object?> arguments, Uri? serverUrlOverride = null, Uri? apiHostUrl = null)
internal Uri BuildsOperationUrl(RestApiOperation operation, IDictionary<string, object?> arguments, Uri? serverUrlOverride = null, Uri? apiHostUrl = null)
{
var url = operation.BuildOperationUrl(arguments, serverUrlOverride, apiHostUrl);

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Proxy that leverages a chat client to generate the request body payload before calling the target concrete function.
/// </summary>
internal sealed class RestApiOperationRunnerPayloadProxy : IRestApiOperationRunner
{
private readonly RestApiOperationRunner _concrete;
private readonly IChatClient _chatClient;

/// <summary>
/// Initializes a new instance of the <see cref="RestApiOperationRunnerPayloadProxy"/> class.
/// </summary>
/// <param name="concrete">Operation runner to call with the generated payload</param>
/// <param name="chatClient">Chat client to generate the payload</param>
/// <exception cref="ArgumentNullException">If the provided Operation runner argument is null</exception>
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.");
}
}

/// <inheritdoc />
public async Task<RestApiOperationResponse> 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);
}
/// <summary>
/// Builds operation payload.
/// </summary>
/// <param name="operation">The operation.</param>
/// <param name="arguments">The operation payload arguments.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The raw operation payload and the corresponding HttpContent.</returns>
private Task<(object? Payload, HttpContent? Content)> BuildOperationPayloadAsync(RestApiOperation operation, IDictionary<string, object?> 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);
}
/// <summary>
/// Builds "application/json" payload.
/// </summary>
/// <param name="payloadMetadata">The payload meta-data.</param>
/// <param name="arguments">The payload arguments.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The JSON payload the corresponding HttpContent.</returns>
private async Task<(object? Payload, HttpContent? Content)> BuildJsonPayloadAsync(RestApiPayload? payloadMetadata, IDictionary<string, object?> 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));
}
}
Loading