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

Open API GitHub Plugin Skill + Frontend Auth (MSAL + Basic / PAT) #641

Merged
merged 47 commits into from
May 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
9f901ef
MSAL and token auth providers
gitri-ms Apr 18, 2023
9b7d454
Add basic, rename bearer auth provider
gitri-ms Apr 19, 2023
f1db573
Renaming bug fix
gitri-ms Apr 19, 2023
b51aaac
Fix camel case, add resource uri param
gitri-ms Apr 19, 2023
7532774
Merge branch 'main' into openapi-auth-new
gitri-ms Apr 19, 2023
ae1e6bf
Remove Azure auth provider
gitri-ms Apr 19, 2023
72a8702
Documentation comments
gitri-ms Apr 19, 2023
501b477
Merge branch 'main' into openapi-auth-new
gitri-ms Apr 19, 2023
c9fb443
Merge branch 'main' into openapi-auth-new
gitri-ms Apr 19, 2023
88a9c35
Formatting
gitri-ms Apr 19, 2023
8370672
Merge branch 'main' into openapi-auth-new
gitri-ms Apr 19, 2023
7d73b8e
Address PR comments
gitri-ms Apr 20, 2023
b701c97
Update OpenAPI example to use MSAL, add unit tests
gitri-ms Apr 20, 2023
c956fd4
Merge branch 'main' into openapi-auth-new
gitri-ms Apr 20, 2023
95dc1d7
formatting
gitri-ms Apr 20, 2023
64e8da6
Merge branch 'main' of https://github.com/teresaqhoang/semantic-kerne…
teresaqhoang Apr 20, 2023
bef0e05
GitHub Open API Skill example
teresaqhoang Apr 21, 2023
5462469
merge
teresaqhoang Apr 24, 2023
f3dae56
removing random comma causing scrollbar
teresaqhoang Apr 24, 2023
c937161
Adding GitHub Swagger + hooking up basic plugin auth in Copilot Chat
teresaqhoang Apr 25, 2023
205b84d
Updating UX and addressing PR comments
teresaqhoang Apr 26, 2023
28536ec
addressing comments
teresaqhoang Apr 26, 2023
18dc551
fixing merge conflicts and adding more comments
teresaqhoang Apr 26, 2023
a35bea0
renaming plugins -> OpenApiSkills in server
teresaqhoang Apr 26, 2023
1b323fc
Merge branch 'main' into openapi-github-pr-skill
teresaqhoang Apr 26, 2023
db42051
formatting + addressing comments
teresaqhoang Apr 26, 2023
f32fc8d
Merge branch 'main' into openapi-github-pr-skill
teresaqhoang Apr 26, 2023
402db58
fixing merge conflicts / format
teresaqhoang Apr 26, 2023
5b47d89
merge
teresaqhoang Apr 26, 2023
563d018
Merge branch 'main' into openapi-github-pr-skill
adrianwyatt Apr 27, 2023
a046d86
Merge branch 'main' into openapi-github-pr-skill
adrianwyatt Apr 27, 2023
cc97f7d
merge main
teresaqhoang Apr 28, 2023
3a9ba9c
Making User Agent optional header value, adding support for additiona…
teresaqhoang Apr 28, 2023
66c1a24
unit test
teresaqhoang Apr 28, 2023
ecb8d03
Merge branch 'main' into openapi-github-pr-skill
teresaqhoang Apr 28, 2023
ef4c5ea
Converting invalid operation ids to compliant SK Function names
teresaqhoang Apr 28, 2023
b833916
Official brand icons
teresaqhoang Apr 28, 2023
f7db7fd
Merge branch 'main' into openapi-github-pr-skill
teresaqhoang Apr 28, 2023
19c9ace
dotnet format fix
teresaqhoang Apr 28, 2023
d70ebcd
Merge branch 'openapi-github-pr-skill' of https://github.com/teresaqh…
teresaqhoang Apr 28, 2023
eee92b6
updating console log
teresaqhoang Apr 28, 2023
801ecba
addressing comments
teresaqhoang Apr 28, 2023
4229bcd
addressing comments
teresaqhoang Apr 28, 2023
c6caa5a
Addressing comments
teresaqhoang May 1, 2023
305b76e
merge + comments
teresaqhoang May 1, 2023
a3a822a
Merge branch 'main' into openapi-github-pr-skill
teresaqhoang May 1, 2023
fa09cb9
Merge branch 'main' into openapi-github-pr-skill
teresaqhoang May 1, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,21 @@ internal sealed class RestApiOperationRunner : IRestApiOperationRunner
/// </summary>
private readonly AuthenticateRequestAsyncCallback _authCallback;

/// <summary>
/// Request-header field containing information about the user agent originating the request
/// </summary>
private readonly string? _userAgent;

/// <summary>
/// Creates an instance of a <see cref="RestApiOperationRunner"/> class.
/// </summary>
/// <param name="httpClient">An instance of the HttpClient class.</param>
/// <param name="authCallback">Optional callback for adding auth data to the API requests.</param>
public RestApiOperationRunner(HttpClient httpClient, AuthenticateRequestAsyncCallback? authCallback = null)
/// <param name="userAgent">Optional request-header field containing information about the user agent originating the request</param>
public RestApiOperationRunner(HttpClient httpClient, AuthenticateRequestAsyncCallback? authCallback = null, string? userAgent = null)
{
this._httpClient = httpClient;
this._userAgent = userAgent;

// If no auth callback provided, use empty function
if (authCallback == null)
Expand Down Expand Up @@ -84,6 +91,11 @@ public RestApiOperationRunner(HttpClient httpClient, AuthenticateRequestAsyncCal
requestMessage.Content = payload;
}

if (!string.IsNullOrWhiteSpace(this._userAgent))
{
requestMessage.Headers.Add("User-Agent", this._userAgent);
}

if (headers != null)
{
foreach (var header in headers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,39 @@ public async Task ItShouldAddHeadersToHttpRequestAsync()
Assert.Contains(this._httpMessageHandlerStub.RequestHeaders, h => h.Key == "fake-header" && h.Value.Contains("fake-header-value"));
}

[Fact]
public async Task ItShouldAddUserAgentHeaderToHttpRequestIfConfiguredAsync()
{
// Arrange
var headers = new Dictionary<string, string>();
headers.Add("fake-header", string.Empty);

var operation = new RestApiOperation(
"fake-id",
"https://fake-random-test-host",
"fake-path",
HttpMethod.Get,
"fake-description",
new List<RestApiOperationParameter>(),
headers
);

var arguments = new Dictionary<string, string>();
arguments.Add("fake-header", "fake-header-value");

var sut = new RestApiOperationRunner(this._httpClient, this._authenticationHandlerMock.Object, "fake-user-agent");

// Act
await sut.RunAsync(operation, arguments);

// Assert
Assert.NotNull(this._httpMessageHandlerStub.RequestHeaders);
Assert.Equal(2, this._httpMessageHandlerStub.RequestHeaders.Count());

Assert.Contains(this._httpMessageHandlerStub.RequestHeaders, h => h.Key == "fake-header" && h.Value.Contains("fake-header-value"));
Assert.Contains(this._httpMessageHandlerStub.RequestHeaders, h => h.Key == "User-Agent" && h.Value.Contains("fake-user-agent"));
}

[Fact]
public async Task ItShouldUsePayloadAndContentTypeArgumentsIfPayloadMetadataIsMissingAsync()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,14 @@ public static class KernelOpenApiExtensions
AuthenticateRequestAsyncCallback? authCallback = null,
CancellationToken cancellationToken = default)
{
const string OpenAPIFile = "openapi.json";
const string OPENAPI_FILE = "openapi.json";

Verify.ValidSkillName(skillDirectoryName);

var skillDir = Path.Combine(parentDirectory, skillDirectoryName);
Verify.DirectoryExists(skillDir);

var openApiDocumentPath = Path.Combine(skillDir, OpenAPIFile);
var openApiDocumentPath = Path.Combine(skillDir, OPENAPI_FILE);
if (!File.Exists(openApiDocumentPath))
{
throw new FileNotFoundException($"No OpenApi document for the specified path - {openApiDocumentPath} is found.");
Expand Down Expand Up @@ -214,7 +214,7 @@ public static class KernelOpenApiExtensions
try
{
kernel.Log.LogTrace("Registering Rest function {0}.{1}", skillName, operation.Id);
var function = kernel.RegisterRestApiFunction(skillName, operation, authCallback, cancellationToken);
var function = kernel.RegisterRestApiFunction(skillName, operation, authCallback, cancellationToken: cancellationToken);
skill[function.Name] = function;
}
catch (Exception ex) when (!ex.IsCriticalException())
Expand All @@ -238,21 +238,27 @@ public static class KernelOpenApiExtensions
/// <param name="operation">The REST API operation.</param>
/// <param name="authCallback">Optional callback for adding auth data to the API requests.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="userAgent">Optional override for request-header field containing information about the user agent originating the request</param>
/// <returns>An instance of <see cref="SKFunction"/> class.</returns>
private static ISKFunction RegisterRestApiFunction(
this IKernel kernel,
string skillName,
RestApiOperation operation,
AuthenticateRequestAsyncCallback? authCallback = null,
CancellationToken cancellationToken = default)
string? userAgent = "Microsoft-Semantic-Kernel",
CancellationToken cancellationToken = default
)
{
var restOperationParameters = operation.GetParameters();

// User Agent may be a required request header fields for some Rest APIs,
// but this detail isn't specified in OpenAPI specs, so defaulting for all Rest APIs imported.
// Other applications can override this value by passing it as a parameter on execution.
async Task<SKContext> ExecuteAsync(SKContext context)
{
try
{
var runner = new RestApiOperationRunner(new HttpClient(), authCallback);
var runner = new RestApiOperationRunner(new HttpClient(), authCallback, userAgent);

// Extract function arguments from context
var arguments = new Dictionary<string, string>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
using Microsoft.SemanticKernel.Connectors.WebApi.Rest.Model;
using Microsoft.SemanticKernel.Diagnostics;
using Microsoft.SemanticKernel.Text;

namespace Microsoft.SemanticKernel.Skills.OpenAPI.OpenApi;
Expand Down Expand Up @@ -145,6 +147,15 @@ private static List<RestApiOperation> CreateRestApiOperations(string serverUrl,

var operationItem = operationPair.Value;

try
{
Verify.ValidFunctionName(operationItem.OperationId);
}
catch (KernelException)
{
operationItem.OperationId = ConvertOperationIdToValidFunctionName(operationItem.OperationId);
}

var operation = new RestApiOperation(
operationItem.OperationId,
serverUrl,
Expand Down Expand Up @@ -373,5 +384,32 @@ private static List<RestApiOperationParameter> CreateRestApiOperationParameters(
/// </summary>
private const string OpenApiVersionPropertyName = "openapi";

/// <summary>
/// Converts operation id to valid SK Function name.
/// A function name can contain only ASCII letters, digits, and underscores.
/// </summary>
/// <param name="operationId">The operation id.</param>
/// <returns>Valid SK Function name.</returns>
private static string ConvertOperationIdToValidFunctionName(string operationId)
{

// Tokenize operation id on forward and back slashes
string[] tokens = operationId.Split('/', '\\');
string result = "";

foreach (string token in tokens)
{
// Removes all characters that are not ASCII letters, digits, and underscores.
string formattedToken = Regex.Replace(token, "[^0-9A-Za-z_]", "");
result += CultureInfo.CurrentCulture.TextInfo.ToTitleCase(formattedToken.ToLower(CultureInfo.CurrentCulture));
}

Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("Operation name \"{0}\" converted to \"{1}\" to comply with SK Function name requirements. Use \"{1}\" when invoking function.", operationId, result);
Console.ResetColor();

return result;
}

#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
using Microsoft.SemanticKernel.AI;
using Microsoft.SemanticKernel.Orchestration;
using Microsoft.SemanticKernel.SkillDefinition;
using Microsoft.SemanticKernel.Skills.OpenAPI.Authentication;
using SemanticKernel.Service.Config;
using SemanticKernel.Service.Model;
using SemanticKernel.Service.Skills;
using SemanticKernel.Service.Skills.OpenAPI.Authentication;
teresaqhoang marked this conversation as resolved.
Show resolved Hide resolved
using SemanticKernel.Service.Storage;

namespace SemanticKernel.Service.Controllers;
Expand Down Expand Up @@ -47,6 +49,7 @@ public class SemanticKernelController : ControllerBase
/// <param name="planner">Planner to use to create function sequences.</param>
/// <param name="plannerOptions">Options for the planner.</param>
/// <param name="ask">Prompt along with its parameters</param>
/// <param name="openApiSkillsAuthHeaders">Authentication headers to connect to OpenAPI Skills</param>
/// <param name="skillName">Skill in which function to invoke resides</param>
/// <param name="functionName">Name of function to invoke</param>
/// <returns>Results consisting of text generated by invoked function along with the variable in the SK that generated it</returns>
Expand All @@ -64,6 +67,7 @@ public class SemanticKernelController : ControllerBase
[FromServices] CopilotChatPlanner planner,
[FromServices] IOptions<PlannerOptions> plannerOptions,
[FromBody] Ask ask,
[FromHeader] OpenApiSkillsAuthHeaders openApiSkillsAuthHeaders,
string skillName, string functionName)
{
this._logger.LogDebug("Received call to invoke {SkillName}/{FunctionName}", skillName, functionName);
Expand All @@ -82,7 +86,7 @@ public class SemanticKernelController : ControllerBase
// Register skills with the planner if enabled.
if (plannerOptions.Value.Enabled)
{
await this.RegisterPlannerSkillsAsync(planner, plannerOptions.Value);
await this.RegisterPlannerSkillsAsync(planner, plannerOptions.Value, openApiSkillsAuthHeaders);
amsacha marked this conversation as resolved.
Show resolved Hide resolved
}

// Register native skills with the chat's kernel
Expand Down Expand Up @@ -131,10 +135,22 @@ public class SemanticKernelController : ControllerBase
/// <summary>
/// Register skills with the planner's kernel.
/// </summary>
private async Task RegisterPlannerSkillsAsync(CopilotChatPlanner planner, PlannerOptions options)
private async Task RegisterPlannerSkillsAsync(CopilotChatPlanner planner, PlannerOptions options, OpenApiSkillsAuthHeaders openApiSkillsAuthHeaders)
{
await planner.Kernel.ImportChatGptPluginSkillFromUrlAsync("KlarnaShopping", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"));

// Register authenticated OpenAPI skills with the planner's kernel
// if the request includes an auth header for an OpenAPI skill.
// Else, don't register the skill as it'll fail on auth.
if (openApiSkillsAuthHeaders.GithubAuthentication != null)
{
var authenticationProvider = new BearerAuthenticationProvider(() => { return Task.FromResult(openApiSkillsAuthHeaders.GithubAuthentication); });
this._logger.LogInformation("Registering GitHub Skill");

var filePath = Path.Combine(Directory.GetCurrentDirectory(), @"Skills/OpenApiSkills/GitHubSkill/openapi.json");
var skill = await planner.Kernel.ImportOpenApiSkillFromFileAsync("GitHubSkill", filePath, authenticationProvider.AuthenticateRequestAsync);
}

planner.Kernel.ImportSkill(new Microsoft.SemanticKernel.CoreSkills.TextSkill(), "text");
planner.Kernel.ImportSkill(new Microsoft.SemanticKernel.CoreSkills.TimeSkill(), "time");
planner.Kernel.ImportSkill(new Microsoft.SemanticKernel.CoreSkills.MathSkill(), "math");
Expand Down
1 change: 1 addition & 0 deletions samples/apps/copilot-chat-app/webapi/CopilotChatApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

<ItemGroup>
<ProjectReference Include="..\..\..\..\dotnet\src\Connectors\Connectors.AI.OpenAI\Connectors.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\dotnet\src\Skills\Skills.OpenAPI\Skills.OpenAPI.csproj" />
<ProjectReference Include="..\..\..\..\dotnet\src\Connectors\Connectors.Memory.Qdrant\Connectors.Memory.Qdrant.csproj" />
<ProjectReference Include="..\..\..\..\dotnet\src\Extensions\Planning.SequentialPlanner\Planning.SequentialPlanner.csproj" />
<ProjectReference Include="..\..\..\..\dotnet\src\SemanticKernel\SemanticKernel.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;

namespace SemanticKernel.Service.Skills.OpenAPI.Authentication;

/// /// <summary>
/// Represents the authentication headers for imported OpenAPI Plugin Skills.
/// </summary>
public class OpenApiSkillsAuthHeaders
{
/// <summary>
/// Gets or sets the MS Graph authentication header value.
/// </summary>
[FromHeader(Name = "x-sk-copilot-graph-auth")]
public string? GraphAuthentication { get; set; }

/// <summary>
/// Gets or sets the Jira authentication header value.
/// </summary>
[FromHeader(Name = "x-sk-copilot-jira-auth")]
public string? JiraAuthentication { get; set; }

/// <summary>
/// Gets or sets the GitHub authentication header value.
/// </summary>
[FromHeader(Name = "x-sk-copilot-github-auth")]
public string? GithubAuthentication { get; set; }
}