Skip to content

Commit

Permalink
.Net: Remove the artificially created server-url parameter from OpenA…
Browse files Browse the repository at this point in the history
…PI function parameter view (#3427)

### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->
The kernel will artificially add a parameter named `serverl-url` to the
parameter view of an OpenAPI function when the function is registered to
the kernel. The optional `server-url` parameter allows planners to
dynamically provide an alternative server url to override the one that
is registered with the plugin. The flexibility offered by this parameter
can occasionally lead to confusion among planners, causing them to
create plans that fail to execute.

Local plugins and OpenAPI plugins should be indifferent to the planners.
We are removing this artificially created parameter to improve the
performance of planners in creating valid function calls regardless of
the function type.

We are aware that there are OpenAPI plugins that require `server-url`
override, such as the Jira plugin and the Azure Key Vault plugin. For
these plugins, it's still possible to provide a server url at
import/registration time, through the optional
`OpenAIFunctionExecutionParameters` parameter (please refer to the
updated kernel syntax example 22 and 24).

Tracked by: #3083

### Description
1. Remove the `server-url` from the parameter view of OpenAPI functions.
2. Update unit tests.
3. Update kernel syntax example 22 and 24.

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [ ] The code builds clean without any errors or warnings
- [ ] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄
  • Loading branch information
TaoChenOSU committed Nov 10, 2023
1 parent 4f4a663 commit 3791635
Show file tree
Hide file tree
Showing 11 changed files with 74 additions and 179 deletions.
Expand Up @@ -49,11 +49,14 @@ public static async Task GetSecretFromAzureKeyVaultWithRetryAsync(InteractiveMsa
var plugin = await kernel.ImportOpenApiPluginFunctionsAsync(
PluginResourceNames.AzureKeyVault,
stream!,
new OpenApiFunctionExecutionParameters { AuthCallback = authenticationProvider.AuthenticateRequestAsync });
new OpenApiFunctionExecutionParameters
{
AuthCallback = authenticationProvider.AuthenticateRequestAsync,
ServerUrlOverride = new Uri(TestConfiguration.KeyVault.Endpoint),
});

// Add arguments for required parameters, arguments for optional ones can be skipped.
var contextVariables = new ContextVariables();
contextVariables.Set("server-url", TestConfiguration.KeyVault.Endpoint);
contextVariables.Set("secret-name", "<secret-name>");
contextVariables.Set("api-version", "7.0");

Expand Down
67 changes: 52 additions & 15 deletions dotnet/samples/KernelSyntaxExamples/Example24_OpenApiPlugin_Jira.cs
Expand Up @@ -3,31 +3,42 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Functions.OpenAPI.Authentication;
using Microsoft.SemanticKernel.Functions.OpenAPI.Extensions;
using Microsoft.SemanticKernel.Functions.OpenAPI.Model;
using Microsoft.SemanticKernel.Orchestration;

using Newtonsoft.Json;
using RepoUtils;

/// <summary>
/// This sample shows how to connect the Semantic Kernel to Jira as an Open Api plugin based on the Open Api schema.
/// This format of registering the plugin and its operations, and subsequently executing those operations can be applied
/// to an Open Api plugin that follows the Open Api Schema.
/// </summary>
// ReSharper disable once InconsistentNaming
public static class Example24_OpenApiPlugin_Jira
{
/// <summary>
/// This sample shows how to connect the Semantic Kernel to Jira as an Open Api plugin based on the Open Api schema.
/// This format of registering the plugin and its operations, and subsequently executing those operations can be applied
/// to an Open Api plugin that follows the Open Api Schema.
/// To use this example, there are a few requirements:
/// 1. You must have a Jira instance that you can authenticate to with your email and api key.
/// Follow the instructions here to get your api key:
/// https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/
/// 2. You must create a new project in your Jira instance and create two issues named TEST-1 and TEST-2 respectively.
/// Follow the instructions here to create a new project and issues:
/// https://support.atlassian.com/jira-software-cloud/docs/create-a-new-project/
/// https://support.atlassian.com/jira-software-cloud/docs/create-an-issue-and-a-sub-task/
/// 3. You can find your domain under the "Products" tab in your account management page.
/// To go to your account management page, click on your profile picture in the top right corner of your Jira
/// instance then select "Manage account".
/// 4. Configure the secrets as described by the ReadMe.md in the dotnet/samples/KernelSyntaxExamples folder.
/// </summary>
public static async Task RunAsync()
{
var kernel = new KernelBuilder().WithLoggerFactory(ConsoleLogger.LoggerFactory).Build();
var contextVariables = new ContextVariables();

// Change <your-domain> to a jira instance you have access to with your authentication credentials
string serverUrl = $"https://{TestConfiguration.Jira.Domain}.atlassian.net/rest/api/latest/";
contextVariables.Set("server-url", serverUrl);

IDictionary<string, ISKFunction> jiraFunctions;
var tokenProvider = new BasicAuthenticationProvider(() =>
Expand All @@ -43,38 +54,64 @@ public static async Task RunAsync()
if (useLocalFile)
{
var apiPluginFile = "./../../../Plugins/JiraPlugin/openapi.json";
jiraFunctions = await kernel.ImportOpenApiPluginFunctionsAsync("jiraPlugin", apiPluginFile, new OpenApiFunctionExecutionParameters(authCallback: tokenProvider.AuthenticateRequestAsync));
jiraFunctions = await kernel.ImportOpenApiPluginFunctionsAsync(
"jiraPlugin",
apiPluginFile,
new OpenApiFunctionExecutionParameters(
authCallback: tokenProvider.AuthenticateRequestAsync,
serverUrlOverride: new Uri(serverUrl)
)
);
}
else
{
var apiPluginRawFileURL = new Uri("https://raw.githubusercontent.com/microsoft/PowerPlatformConnectors/dev/certified-connectors/JIRA/apiDefinition.swagger.json");
jiraFunctions = await kernel.ImportOpenApiPluginFunctionsAsync("jiraPlugin", apiPluginRawFileURL, new OpenApiFunctionExecutionParameters(httpClient, tokenProvider.AuthenticateRequestAsync));
jiraFunctions = await kernel.ImportOpenApiPluginFunctionsAsync(
"jiraPlugin",
apiPluginRawFileURL,
new OpenApiFunctionExecutionParameters(
httpClient, tokenProvider.AuthenticateRequestAsync,
serverUrlOverride: new Uri(serverUrl)
)
);
}

// GetIssue Function
{
// Set Properties for the Get Issue operation in the openAPI.swagger.json
contextVariables.Set("issueKey", "SKTES-2");
// Make sure the issue exists in your Jira instance or it will return a 404
contextVariables.Set("issueKey", "TEST-1");

// Run operation via the semantic kernel
var result = await kernel.RunAsync(contextVariables, jiraFunctions["GetIssue"]);

Console.WriteLine("\n\n\n");
var formattedContent = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result.GetValue<string>()!), Formatting.Indented);
var formattedContent = JsonSerializer.Serialize(
result.GetValue<RestApiOperationResponse>(),
new JsonSerializerOptions()
{
WriteIndented = true
});
Console.WriteLine("GetIssue jiraPlugin response: \n{0}", formattedContent);
}

// AddComment Function
{
// Set Properties for the AddComment operation in the openAPI.swagger.json
contextVariables.Set("issueKey", "SKTES-1");
contextVariables.Set("body", "Here is a rad comment");
// Make sure the issue exists in your Jira instance or it will return a 404
contextVariables.Set("issueKey", "TEST-2");
contextVariables.Set(RestApiOperation.PayloadArgumentName, "{\"body\": \"Here is a rad comment\"}");

// Run operation via the semantic kernel
var result = await kernel.RunAsync(contextVariables, jiraFunctions["AddComment"]);

Console.WriteLine("\n\n\n");
var formattedContent = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result.GetValue<string>()!), Formatting.Indented);
var formattedContent = JsonSerializer.Serialize(
result.GetValue<RestApiOperationResponse>(),
new JsonSerializerOptions()
{
WriteIndented = true
});
Console.WriteLine("AddComment jiraPlugin response: \n{0}", formattedContent);
}
}
Expand Down
Expand Up @@ -203,10 +203,8 @@ public static class KernelOpenApiPluginExtensions
CancellationToken cancellationToken = default)
{
var restOperationParameters = operation.GetParameters(
executionParameters?.ServerUrlOverride,
executionParameters?.EnableDynamicPayload ?? false,
executionParameters?.EnablePayloadNamespacing ?? false,
documentUri
executionParameters?.EnablePayloadNamespacing ?? false
);

var logger = kernel.LoggerFactory is not null ? kernel.LoggerFactory.CreateLogger(typeof(KernelOpenApiPluginExtensions)) : NullLogger.Instance;
Expand Down
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
Expand All @@ -21,7 +20,6 @@ internal static class RestApiOperationExtensions
/// Returns list of REST API operation parameters.
/// </summary>
/// <param name="operation">The REST API operation.</param>
/// <param name="serverUrlOverride">The server URL override.</param>
/// <param name="addPayloadParamsFromMetadata">Determines whether to include the operation payload parameters from payload metadata.
/// If false, the 'payload' and 'content-type' artificial parameters are added instead.
/// </param>
Expand All @@ -31,37 +29,15 @@ internal static class RestApiOperationExtensions
/// would be resolved from the same 'email' argument, which is incorrect. However, by employing namespaces,
/// the parameters 'sender.email' and 'receiver.mail' will be correctly resolved from arguments with the same names.
/// </param>
/// <param name="documentUri">The URI of OpenApi document.</param>
/// <returns>The list of parameters.</returns>
public static IReadOnlyList<RestApiOperationParameter> GetParameters(
this RestApiOperation operation,
Uri? serverUrlOverride = null,
bool addPayloadParamsFromMetadata = false,
bool enablePayloadNamespacing = false,
Uri? documentUri = null)
bool enablePayloadNamespacing = false)
{
string? serverUrlString = null;
Uri? serverUrl = serverUrlOverride ?? operation.ServerUrl ?? documentUri;
var parameters = new List<RestApiOperationParameter>(operation.Parameters);

if (serverUrl is not null)
{
serverUrlString = $"{serverUrl.GetLeftPart(UriPartial.Authority)}/";
}

var parameters = new List<RestApiOperationParameter>(operation.Parameters)
{
// Register the "server-url" parameter if override is provided
new(
name: RestApiOperation.ServerUrlArgumentName,
type: "string",
isRequired: false,
expand: false,
RestApiOperationParameterLocation.Path,
RestApiOperationParameterStyle.Simple,
defaultValue: serverUrlString)
};

//Add payload parameters
// Add payload parameters
if (operation.Method == HttpMethod.Put || operation.Method == HttpMethod.Post)
{
parameters.AddRange(GetPayloadParameters(operation, addPayloadParamsFromMetadata, enablePayloadNamespacing));
Expand Down
15 changes: 2 additions & 13 deletions dotnet/src/Functions/Functions.OpenAPI/Model/RestApiOperation.cs
Expand Up @@ -14,11 +14,6 @@ namespace Microsoft.SemanticKernel.Functions.OpenAPI.Model;
/// </summary>
public sealed class RestApiOperation
{
/// <summary>
/// An artificial parameter that is added to be able to override REST API operation server url.
/// </summary>
public const string ServerUrlArgumentName = "server-url";

/// <summary>
/// An artificial parameter to be used for operation having "text/plain" payload media type.
/// </summary>
Expand Down Expand Up @@ -109,7 +104,7 @@ public sealed class RestApiOperation
/// <returns>The operation Url.</returns>
public Uri BuildOperationUrl(IDictionary<string, string> arguments, Uri? serverUrlOverride = null, Uri? apiHostUrl = null)
{
var serverUrl = this.GetServerUrl(arguments, serverUrlOverride, apiHostUrl);
var serverUrl = this.GetServerUrl(serverUrlOverride, apiHostUrl);

var path = this.ReplacePathParameters(this.Path, arguments);

Expand Down Expand Up @@ -203,23 +198,17 @@ string ReplaceParameter(Match match)
/// <summary>
/// Returns operation server Url.
/// </summary>
/// <param name="arguments">The operation arguments.</param>
/// <param name="serverUrlOverride">Override for REST API operation server url.</param>
/// <param name="apiHostUrl">The URL of REST API host.</param>
/// <returns>The operation server url.</returns>
private Uri GetServerUrl(IDictionary<string, string> arguments, Uri? serverUrlOverride, Uri? apiHostUrl)
private Uri GetServerUrl(Uri? serverUrlOverride, Uri? apiHostUrl)
{
string serverUrlString;

if (serverUrlOverride is not null)
{
serverUrlString = serverUrlOverride.AbsoluteUri;
}
else if (arguments.TryGetValue(ServerUrlArgumentName, out string serverUrlFromArgument))
{
// Override defined server url - https://api.example.com/v1 by the one from arguments.
serverUrlString = serverUrlFromArgument;
}
else
{
serverUrlString =
Expand Down
Expand Up @@ -104,14 +104,6 @@ public async Task ItUsesServerUrlOverrideIfProvidedAsync(bool removeServersPrope
var result = await this._kernel.RunAsync(setSecretFunction, variables);

// Assert
Assert.NotNull(setSecretFunction);

var functionView = setSecretFunction.Describe();
Assert.NotNull(functionView);

var serverUrlParameter = functionView.Parameters.First(p => p.Name == "server_url");
Assert.Equal(ServerUrlOverride, serverUrlParameter.DefaultValue);

Assert.NotNull(messageHandlerStub.RequestUri);
Assert.StartsWith(ServerUrlOverride, messageHandlerStub.RequestUri.AbsoluteUri, StringComparison.Ordinal);
}
Expand Down Expand Up @@ -142,14 +134,6 @@ public async Task ItUsesServerUrlFromOpenApiDocumentAsync(string documentFileNam
var result = await this._kernel.RunAsync(setSecretFunction, variables);

// Assert
Assert.NotNull(setSecretFunction);

var functionView = setSecretFunction.Describe();
Assert.NotNull(functionView);

var serverUrlParameter = functionView.Parameters.First(p => p.Name == "server_url");
Assert.Equal(ServerUrlFromDocument, serverUrlParameter.DefaultValue);

Assert.NotNull(messageHandlerStub.RequestUri);
Assert.StartsWith(ServerUrlFromDocument, messageHandlerStub.RequestUri.AbsoluteUri, StringComparison.Ordinal);
}
Expand Down Expand Up @@ -187,14 +171,6 @@ public async Task ItUsesOpenApiDocumentHostUrlWhenServerUrlIsNotProvidedAsync(st
var result = await this._kernel.RunAsync(setSecretFunction, variables);

// Assert
Assert.NotNull(setSecretFunction);

var functionView = setSecretFunction.Describe();
Assert.NotNull(functionView);

var serverUrlParameter = functionView.Parameters.First(p => p.Name == "server_url");
Assert.Equal(expectedServerUrl, serverUrlParameter.DefaultValue);

Assert.NotNull(messageHandlerStub.RequestUri);
Assert.StartsWith(expectedServerUrl, messageHandlerStub.RequestUri.AbsoluteUri, StringComparison.Ordinal);
}
Expand Down

0 comments on commit 3791635

Please sign in to comment.