-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
truncate openapi operation description and duplicate properties/param…
…eters
- Loading branch information
1 parent
d516994
commit f803b1c
Showing
6 changed files
with
686 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
...tions/Functions.OpenApi.Extensions/Extensions/CopilotAgentPluginOpenApiDocumentVisitor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.OpenApi.Models; | ||
using Microsoft.OpenApi.Services; | ||
|
||
namespace Microsoft.SemanticKernel.Plugins.OpenApi.Extensions; | ||
|
||
/// <summary> | ||
/// Copilot Agent Plugin OpenAPI document visitor that: | ||
/// * Normalizes the operation IDs by replacing dots with underscores. So that the operation IDs can be used as function names in semantic kernel. | ||
/// * Truncates the description to the maximum allowed length. | ||
/// * Removes properties and parameters with the same name. | ||
/// </summary> | ||
internal sealed class CopilotAgentPluginOpenApiDocumentVisitor : OpenApiVisitorBase | ||
{ | ||
private const int MaximumDescription = 1000; | ||
|
||
public override void Visit(OpenApiOperation operation) | ||
{ | ||
NormalizeOperationId(operation); | ||
|
||
TruncateOperationDescription(operation); | ||
|
||
RemoveDuplicateOperationProperties(operation); | ||
} | ||
|
||
private static void NormalizeOperationId(OpenApiOperation operation) | ||
{ | ||
if (operation is null || operation.OperationId is null) | ||
{ | ||
return; | ||
} | ||
operation.OperationId = operation.OperationId.Replace('.', '_'); | ||
} | ||
|
||
private static void TruncateOperationDescription(OpenApiOperation operation) | ||
{ | ||
if (operation.Description?.Length > MaximumDescription) | ||
{ | ||
operation.Description = operation.Description.Substring(0, MaximumDescription); | ||
} | ||
} | ||
|
||
private static void RemoveDuplicateOperationProperties(OpenApiOperation operation) | ||
{ | ||
HashSet<string> visitedNames = []; | ||
|
||
var index = 0; | ||
|
||
// Lookup for duplicate parameters and remove them | ||
while (index < operation.Parameters.Count) | ||
{ | ||
var parameter = operation.Parameters[index]; | ||
if (visitedNames.Contains(parameter.Name)) | ||
{ | ||
operation.Parameters.Remove(parameter); | ||
continue; | ||
} | ||
|
||
visitedNames.Add(parameter.Name); | ||
index++; | ||
} | ||
|
||
// Lookup for duplicate properties in request body and remove them | ||
if (operation.RequestBody is not null) | ||
{ | ||
foreach (var content in operation.RequestBody.Content) | ||
{ | ||
RemoveProperty(content.Value.Schema, visitedNames); | ||
} | ||
} | ||
} | ||
|
||
private static void RemoveProperty(OpenApiSchema schema, HashSet<string> visitedNames) | ||
{ | ||
var index = 0; | ||
|
||
while (index < schema.Properties.Count) | ||
{ | ||
var property = schema.Properties.ElementAt(index); | ||
if (visitedNames.Contains(property.Key)) | ||
{ | ||
schema.Properties.Remove(property.Key); | ||
continue; | ||
} | ||
|
||
visitedNames.Add(property.Key); | ||
RemoveProperty(property.Value, visitedNames); | ||
index++; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
...nctions/Functions.UnitTests/OpenApi/Extensions/CopilotAgentPluginKernelExtensionsTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
// Copyright (c) Microsoft. All rights reserved. | ||
|
||
using System.IO; | ||
using System.Threading.Tasks; | ||
using Microsoft.SemanticKernel; | ||
using Xunit; | ||
|
||
namespace SemanticKernel.Functions.UnitTests.OpenApi; | ||
|
||
public sealed class CopilotAgentPluginKernelExtensionsTests | ||
{ | ||
[Fact] | ||
public async Task ItCanImportPluginFromCopilotAgentPluginAsync() | ||
{ | ||
// Act | ||
var kernel = new Kernel(); | ||
var testPluginsDir = Path.Combine(Directory.GetCurrentDirectory(), "OpenApi", "TestPlugins"); | ||
var manifestFilePath = Path.Combine(testPluginsDir, "messages-apiplugin.json"); | ||
|
||
// Arrange | ||
var plugin = await kernel.ImportPluginFromCopilotAgentPluginAsync("MessagesPlugin", manifestFilePath); | ||
|
||
// Assert | ||
Assert.NotNull(plugin); | ||
Assert.Equal(2, plugin.FunctionCount); | ||
Assert.Equal(683, plugin["me_CreateMessages"].Description.Length); | ||
Assert.Equal(1000, plugin["me_ListMessages"].Description.Length); | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/messages-apiplugin.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
{ | ||
"$schema": "https://developer.microsoft.com/json-schemas/copilot/plugin/v2.1/schema.json", | ||
"schema_version": "v2.1", | ||
"name_for_human": "OData Service for namespace microsoft.graph", | ||
"description_for_human": "This OData service is located at https://graph.microsoft.com/v1.0", | ||
"description_for_model": "This OData service is located at https://graph.microsoft.com/v1.0", | ||
"contact_email": "[email protected]", | ||
"namespace": "Messages", | ||
"capabilities": { | ||
"conversation_starters": [ | ||
{ | ||
"text": "List messages" | ||
}, | ||
{ | ||
"text": "Create message" | ||
} | ||
] | ||
}, | ||
"functions": [ | ||
{ | ||
"name": "me_CreateMessages", | ||
"description": "Create a draft of a new message in either JSON or MIME format. When using JSON format, you can:\n- Include an attachment to the message.\n- Update the draft later to add content to the body or change other message properties. When using MIME format:\n- Provide the applicable Internet message headers and the MIME content, all encoded in base64 format in the request body.\n- /* Add any attachments and S/MIME properties to the MIME content. By default, this operation saves the draft in the Drafts folder. Send the draft message in a subsequent operation. Alternatively, send a new message in a single operation, or create a draft to forward, reply and reply-all to an existing message." | ||
}, | ||
{ | ||
"name": "me_ListMessages", | ||
"description": "Get the messages in the signed-in user\u0026apos;s mailbox (including the Deleted Items and Clutter folders). Depending on the page size and mailbox data, getting messages from a mailbox can incur multiple requests. The default page size is 10 messages. Use $top to customize the page size, within the range of 1 and 1000. To improve the operation response time, use $select to specify the exact properties you need; see example 1 below. Fine-tune the values for $select and $top, especially when you must use a larger page size, as returning a page with hundreds of messages each with a full response payload may trigger the gateway timeout (HTTP 504). To get the next page of messages, simply apply the entire URL returned in @odata.nextLink to the next get-messages request. This URL includes any query parameters you may have specified in the initial request. Do not try to extract the $skip value from the @odata.nextLink URL to manipulate responses. This API uses the $skip value to keep count of all the items it has gone through in the user\u0026apos;s mailbox to return a page of message-type items. It\u0026apos;s therefore possible that even in the initial response, the $skip value is larger than the page size. For more information, see Paging Microsoft Graph data in your app. Currently, this operation returns message bodies in only HTML format. There are two scenarios where an app can get messages in another user\u0026apos;s mail folder:" | ||
} | ||
], | ||
"runtimes": [ | ||
{ | ||
"type": "OpenApi", | ||
"auth": { | ||
"type": "None" | ||
}, | ||
"spec": { | ||
"url": "messages-openapi.yml" | ||
}, | ||
"run_for_functions": [ | ||
"me_ListMessages", | ||
"me_CreateMessages" | ||
] | ||
} | ||
] | ||
} |
Oops, something went wrong.