Skip to content

Commit

Permalink
truncate openapi operation description and duplicate properties/param…
Browse files Browse the repository at this point in the history
…eters
  • Loading branch information
SergeyMenshykh committed Dec 5, 2024
1 parent d516994 commit f803b1c
Show file tree
Hide file tree
Showing 6 changed files with 686 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public static async Task<KernelPlugin> CreatePluginFromCopilotAgentPluginAsync(
}

var functions = new List<KernelFunction>();
var documentWalker = new OpenApiWalker(new OperationIdNormalizationOpenApiVisitor());
var documentWalker = new OpenApiWalker(new CopilotAgentPluginOpenApiDocumentVisitor());
foreach (var runtime in openAPIRuntimes)
{
var manifestFunctions = document?.Functions?.Where(f => runtime.RunForFunctions.Contains(f.Name)).ToList();
Expand Down
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++;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,22 @@
<ProjectReference Include="..\..\SemanticKernel.Core\SemanticKernel.Core.csproj" />
<ProjectReference Include="..\Functions.Grpc\Functions.Grpc.csproj" />
<ProjectReference Include="..\Functions.Markdown\Functions.Markdown.csproj" />
<ProjectReference Include="..\Functions.OpenApi.Extensions\Functions.OpenApi.Extensions.csproj" />
<ProjectReference Include="..\Functions.OpenApi\Functions.OpenApi.csproj" />
<ProjectReference Include="..\Functions.Yaml\Functions.Yaml.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="OpenApi\TestPlugins\multipart-form-data.json" />
</ItemGroup>
<ItemGroup>
<Folder Include="OpenApi\Extensions\Visitors\" />
</ItemGroup>
<ItemGroup>
<None Update="OpenApi\TestPlugins\messages-apiplugin.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="OpenApi\TestPlugins\messages-openapi.yml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
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);
}
}
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"
]
}
]
}
Loading

0 comments on commit f803b1c

Please sign in to comment.