Skip to content

Commit

Permalink
.Net: Enable use of authentication info from Open AI plugin manifest (m…
Browse files Browse the repository at this point in the history
…icrosoft#3304)

### 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.
-->
- today the authentication info defined in `ai-plugin.json` is not
considered.
- this PR enables the use of that info both when fetching the plugin's
spec and when subsequent calls are made to the plugin's endpoint.
- Note: an example utilizing this functionality will be added in a
follow-up PR.

### Description
1. split `ImportAIPluginFunctionsAsync` into two functions:
`ImportOpenApiPluginFunctionsAsync` and
`ImportOpenAIPluginFunctionsAsync`
2. added `OpenAIFunctionExecutionParameters` that makes use of
`OpenAIAuthenticateRequestAsyncCallback`, which takes
`OpenAIAuthentication` as an argument.

<!-- 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: -->

- [x] The code builds clean without any errors or warnings
- [x] 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
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄

---------

Co-authored-by: Mark Wallace <[email protected]>
  • Loading branch information
dehoward and markwallace-microsoft authored Nov 7, 2023
1 parent 6ff0c2b commit 49e057f
Show file tree
Hide file tree
Showing 21 changed files with 644 additions and 232 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Functions.OpenAPI.Extensions;
using Microsoft.SemanticKernel.Functions.OpenAPI.Model;
using Microsoft.SemanticKernel.Functions.OpenAPI.OpenAI;
using Microsoft.SemanticKernel.Orchestration;
using RepoUtils;

Expand All @@ -24,8 +24,8 @@ private static async Task RunChatGptPluginAsync()
//This HTTP client is optional. SK will fallback to a default internal one if omitted.
using HttpClient httpClient = new();

//Import a ChatGPT plugin via URI
var plugin = await kernel.ImportPluginFunctionsAsync("<plugin name>", new Uri("<chatGPT-plugin>"), new OpenApiFunctionExecutionParameters(httpClient));
//Import an Open AI plugin via URI
var plugin = await kernel.ImportOpenAIPluginFunctionsAsync("<plugin name>", new Uri("<chatGPT-plugin>"), new OpenAIFunctionExecutionParameters(httpClient));

//Add arguments for required parameters, arguments for optional ones can be skipped.
var contextVariables = new ContextVariables();
Expand All @@ -43,7 +43,7 @@ private static async Task RunChatGptPluginAsync()

//var kernel = new KernelBuilder().WithLoggerFactory(ConsoleLogger.LoggerFactory).Build();

//var plugin = await kernel.ImportPluginFunctionsAsync("Klarna", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"));
//var plugin = await kernel.ImportOpenAIPluginFunctionsAsync("Klarna", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"));

//var contextVariables = new ContextVariables();
//contextVariables.Set("q", "Laptop"); // A precise query that matches one very small category or product that needs to be searched for to find the products the user is looking for. If the user explicitly stated what they want, use that as a query. The query is as specific as possible to the product name or category mentioned by the user in its singular form, and don't contain any clarifiers like latest, newest, cheapest, budget, premium, expensive or similar. The query is always taken from the latest topic, if there is a new topic a new query is started.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ public static async Task GetSecretFromAzureKeyVaultWithRetryAsync(InteractiveMsa

var stream = type.Assembly.GetManifestResourceStream(type, resourceName);

// Import AI Plugin
var plugin = await kernel.ImportPluginFunctionsAsync(
// Import an Open AI Plugin via Stream
var plugin = await kernel.ImportOpenApiPluginFunctionsAsync(
PluginResourceNames.AzureKeyVault,
stream!,
new OpenApiFunctionExecutionParameters { AuthCallback = authenticationProvider.AuthenticateRequestAsync });
Expand Down Expand Up @@ -75,7 +75,7 @@ public static async Task AddSecretToAzureKeyVaultAsync(InteractiveMsalAuthentica
var stream = type.Assembly.GetManifestResourceStream(type, resourceName);

// Import AI Plugin
var plugin = await kernel.ImportPluginFunctionsAsync(
var plugin = await kernel.ImportOpenApiPluginFunctionsAsync(
PluginResourceNames.AzureKeyVault,
stream!,
new OpenApiFunctionExecutionParameters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ public static async Task RunAsync()
if (useLocalFile)
{
var apiPluginFile = "./../../../Plugins/JiraPlugin/openapi.json";
jiraFunctions = await kernel.ImportPluginFunctionsAsync("jiraPlugin", apiPluginFile, new OpenApiFunctionExecutionParameters(authCallback: tokenProvider.AuthenticateRequestAsync));
jiraFunctions = await kernel.ImportOpenApiPluginFunctionsAsync("jiraPlugin", apiPluginFile, new OpenApiFunctionExecutionParameters(authCallback: 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.ImportOpenApiPluginFunctionsAsync("jiraPlugin", apiPluginRawFileURL, new OpenApiFunctionExecutionParameters(httpClient, tokenProvider.AuthenticateRequestAsync));
}

// GetIssue Function
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
using Microsoft.SemanticKernel.AI.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI;
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.AzureSdk;
using Microsoft.SemanticKernel.Functions.OpenAPI.Extensions;
using Microsoft.SemanticKernel.Functions.OpenAPI.Model;
using Microsoft.SemanticKernel.Functions.OpenAPI.OpenAI;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.Plugins.Core;
using RepoUtils;
Expand Down Expand Up @@ -56,7 +56,7 @@ private static async Task<IKernel> InitializeKernelAsync()

// Load functions to kernel
kernel.ImportFunctions(new TimePlugin(), "TimePlugin");
await kernel.ImportPluginFunctionsAsync("KlarnaShoppingPlugin", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"), new OpenApiFunctionExecutionParameters());
await kernel.ImportOpenAIPluginFunctionsAsync("KlarnaShoppingPlugin", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"), new OpenAIFunctionExecutionParameters());

return kernel;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ public CustomAuthenticationProvider(Func<Task<string>> header, Func<Task<string>
/// Applies the header and value to the provided HTTP request message.
/// </summary>
/// <param name="request">The HTTP request message.</param>
/// <returns></returns>
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
var header = await this._header().ConfigureAwait(false);
Expand Down
23 changes: 14 additions & 9 deletions dotnet/src/Functions/Functions.OpenAPI/Authentication/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ This pattern was designed to be flexible enough to support a wide variety of aut
## Reference Authentication Providers

### [`BasicAuthenticationProvider`](./BasicAuthenticationProvider.cs)

This class implements the HTTP "basic" authentication scheme. The constructor accepts a `Func` which defines how to retrieve the user's credentials. When the `AuthenticateRequestAsync` method is called, it retrieves the credentials, encodes them as a UTF-8 encoded Base64 string, and adds them to the `HttpRequestMessage`'s authorization header.

The following code demonstrates how to use this provider:

```csharp
var basicAuthProvider = new BasicAuthenticationProvider(() =>
{
Expand All @@ -28,19 +30,21 @@ var basicAuthProvider = new BasicAuthenticationProvider(() =>
Env.Var("MY_EMAIL_ADDRESS") + ":" + Env.Var("JIRA_API_KEY")
);
});
var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.Jira, new OpenApiPluginExecutionParameters { AuthCallback = basicAuthProvider.AuthenticateRequestAsync } );
var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.Jira, new OpenApiFunctionExecutionParameters { AuthCallback = basicAuthProvider.AuthenticateRequestAsync } );
```

### [`BearerAuthenticationProvider`](./BearerAuthenticationProvider.cs)
This class implements the HTTP "bearer" authentication scheme. The constructor accepts a `Func` which defines how to retrieve the bearer token. When the `AuthenticateRequestAsync` method is called, it retrieves the token and adds it to the `HttpRequestMessage`'s authorization header.

This class implements the HTTP "bearer" authentication scheme. The constructor accepts a `Func` which defines how to retrieve the bearer token. When the `AuthenticateRequestAsync` method is called, it retrieves the token and adds it to the `HttpRequestMessage`'s authorization header.

The following code demonstrates how to use this provider:

```csharp
var bearerAuthProvider = new BearerAuthenticationProvider(() =>
{
return Task.FromResult(Env.Var("AZURE_KEYVAULT_TOKEN"));
});
var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.AzureKeyVault, new OpenApiPluginExecutionParameters { AuthCallback = bearerAuthProvider.AuthenticateRequestAsync } )
var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.AzureKeyVault, new OpenApiFunctionExecutionParameters { AuthCallback = bearerAuthProvider.AuthenticateRequestAsync } )
```

### [`InteractiveMsalAuthenticationProvider`](./InteractiveMsalAuthenticationProvider.cs)
Expand All @@ -50,10 +54,11 @@ This class uses the [Microsoft Authentication Library (MSAL)](https://learn.micr
Once the token is acquired, it is added to the HTTP authentication header via the `AuthenticateRequestAsync` method, which is inherited from `BearerAuthenticationProvider`.

To construct this provider, the caller must specify:
- *Client ID*identifier of the calling application. This is acquired by [registering your application with the Microsoft Identity platform](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app).
- *Tenant ID*identifier of the target service tenant, orcommon
- *Scopes*permissions being requested
- *Redirect URI*for redirecting the user back to the application. (When running locally, this is typically http://localhost.)

- _Client ID_identifier of the calling application. This is acquired by [registering your application with the Microsoft Identity platform](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app).
- _Tenant ID_identifier of the target service tenant, orcommon
- _Scopes_permissions being requested
- _Redirect URI_for redirecting the user back to the application. (When running locally, this is typically http://localhost.)
```csharp
var msalAuthProvider = new InteractiveMsalAuthenticationProvider(
Expand All @@ -62,5 +67,5 @@ var msalAuthProvider = new InteractiveMsalAuthenticationProvider(
new string[] { ".default" }, // scopes
new Uri("http://localhost") // redirectUri
);
var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.AzureKeyVault, new OpenApiPluginExecutionParameters { AuthCallback = msalAuthProvider.AuthenticateRequestAsync } )
```
var plugin = kernel.ImportOpenApiPluginFromResource(PluginResourceNames.AzureKeyVault, new OpenApiFunctionExecutionParameters { AuthCallback = msalAuthProvider.AuthenticateRequestAsync } )
```
64 changes: 64 additions & 0 deletions dotnet/src/Functions/Functions.OpenAPI/DocumentLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Functions.OpenAPI.Authentication;

namespace Microsoft.SemanticKernel.Functions.OpenAPI;

internal static class DocumentLoader
{
internal static async Task<string> LoadDocumentFromUriAsync(
Uri uri,
ILogger logger,
HttpClient httpClient,
AuthenticateRequestAsyncCallback? authCallback,
string? userAgent,
CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(HttpMethod.Get, uri.ToString());
request.Headers.UserAgent.Add(ProductInfoHeaderValue.Parse(userAgent ?? Telemetry.HttpUserAgent));

if (authCallback is not null)
{
await authCallback(request).ConfigureAwait(false);
}

logger.LogTrace("Importing document from {0}", uri);

using var response = await httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
return await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false);
}

internal static async Task<string> LoadDocumentFromFilePathAsync(
string filePath,
ILogger logger,
CancellationToken cancellationToken)
{
var pluginJson = string.Empty;

if (!File.Exists(filePath))
{
throw new FileNotFoundException($"Invalid URI. The specified path '{filePath}' does not exist.");
}

logger.LogTrace("Importing document from {0}", filePath);

using (var sr = File.OpenText(filePath))
{
return await sr.ReadToEndAsync().ConfigureAwait(false); // must await here to avoid stream reader being disposed before the string is read
}
}

internal static async Task<string> LoadDocumentFromStreamAsync(Stream stream)
{
using StreamReader reader = new(stream);
return await reader.ReadToEndAsync().ConfigureAwait(false);
}
}
Loading

0 comments on commit 49e057f

Please sign in to comment.