From 1a004c2ca14cf06838688bbd3c9e1c85a7ed4aea Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Wed, 15 May 2024 10:32:40 +0300 Subject: [PATCH 1/3] Add support for OpenAI generation --- .vscode/launch.json | 4 +- src/Kiota.Builder/Kiota.Builder.csproj | 2 +- src/Kiota.Builder/KiotaBuilder.cs | 2 - .../Plugins/PluginsGenerationService.cs | 105 +++++++++++++----- .../Plugins/PluginsGenerationServiceTests.cs | 32 +++++- 5 files changed, 113 insertions(+), 32 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index cdb61f49af..df1e40d5f1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -352,7 +352,9 @@ "--type", "ApiManifest", "--type", - "microsoft" + "microsoft", + "--type", + "OpenAI" ], "cwd": "${workspaceFolder}/samples/msgraph-mail/dotnet", "console": "internalConsole", diff --git a/src/Kiota.Builder/Kiota.Builder.csproj b/src/Kiota.Builder/Kiota.Builder.csproj index 38e7441a51..b3e8067a3f 100644 --- a/src/Kiota.Builder/Kiota.Builder.csproj +++ b/src/Kiota.Builder/Kiota.Builder.csproj @@ -47,7 +47,7 @@ - + diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 2de899ec17..559815502a 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -231,8 +231,6 @@ public async Task GeneratePluginAsync(CancellationToken cancellationToken) { return await GenerateConsumerAsync(async (sw, stepId, openApiTree, CancellationToken) => { - if (config.PluginTypes.Contains(PluginType.OpenAI)) - throw new NotImplementedException("The OpenAI plugin type is not supported for generation"); if (openApiDocument is null || openApiTree is null) throw new InvalidOperationException("The OpenAPI document and the URL tree must be loaded before generating the plugins"); // generate plugin diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 07a4a03ac5..7a51fe4b2e 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -37,6 +37,7 @@ public PluginsGenerationService(OpenApiDocument document, OpenApiUrlTreeNode ope private static readonly OpenAPIRuntimeComparer _openAPIRuntimeComparer = new(); private const string ManifestFileNameSuffix = ".json"; private const string DescriptionPathSuffix = "openapi.yml"; + private const string OpenAIManifestFileName = "openai-plugins"; public async Task GenerateManifestAsync(CancellationToken cancellationToken = default) { // write the description @@ -56,7 +57,8 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de // write the plugins foreach (var pluginType in Configuration.PluginTypes) { - var manifestOutputPath = Path.Combine(Configuration.OutputPath, $"{Configuration.ClientClassName.ToLowerInvariant()}-{pluginType.ToString().ToLowerInvariant()}{ManifestFileNameSuffix}"); + var manifestFileName = pluginType == PluginType.OpenAI ? OpenAIManifestFileName : $"{Configuration.ClientClassName.ToLowerInvariant()}-{pluginType.ToString().ToLowerInvariant()}"; + var manifestOutputPath = Path.Combine(Configuration.OutputPath, $"{manifestFileName}{ManifestFileNameSuffix}"); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task await using var fileStream = File.Create(manifestOutputPath, 4096); await using var writer = new Utf8JsonWriter(fileStream, new JsonWriterOptions { Indented = true }); @@ -70,51 +72,67 @@ public async Task GenerateManifestAsync(CancellationToken cancellationToken = de break; case PluginType.APIManifest: var apiManifest = new ApiManifestDocument("application"); //TODO add application name - // pass empty cong hash so that its not included in this manifest. + // pass empty config hash so that its not included in this manifest. apiManifest.ApiDependencies.AddOrReplace(Configuration.ClientClassName, Configuration.ToApiDependency(string.Empty, TreeNode?.GetRequestInfo().ToDictionary(static x => x.Key, static x => x.Value) ?? [], WorkingDirectory)); + var publisherName = string.IsNullOrEmpty(OAIDocument.Info?.Contact?.Name) + ? DefaultContactName + : OAIDocument.Info.Contact.Name; + var publisherEmail = string.IsNullOrEmpty(OAIDocument.Info?.Contact?.Email) + ? DefaultContactEmail + : OAIDocument.Info.Contact.Email; + apiManifest.Publisher = new Publisher(publisherName, publisherEmail); apiManifest.Write(writer); break; - case PluginType.OpenAI://TODO add support for OpenAI plugin type generation - // intentional drop to the default case + case PluginType.OpenAI: + var pluginDocumentV1 = GetV1ManifestDocument(descriptionRelativePath); + pluginDocumentV1.Write(writer); + break; default: throw new NotImplementedException($"The {pluginType} plugin is not implemented."); } await writer.FlushAsync(cancellationToken).ConfigureAwait(false); } } + private PluginManifestDocument GetV1ManifestDocument(string openApiDocumentPath) + { + var descriptionForHuman = OAIDocument.Info?.Description.CleanupXMLString() is string d && !string.IsNullOrEmpty(d) ? d : $"Description for {OAIDocument.Info?.Title.CleanupXMLString()}"; + var manifestInfo = ExtractInfoFromDocument(OAIDocument.Info); + return new PluginManifestDocument + { + SchemaVersion = "v1", + NameForHuman = OAIDocument.Info?.Title.CleanupXMLString(), + NameForModel = OAIDocument.Info?.Title.CleanupXMLString(), + DescriptionForHuman = descriptionForHuman, + DescriptionForModel = manifestInfo.DescriptionForModel ?? descriptionForHuman, + Auth = new V1AnonymousAuth(), + Api = new Api() + { + Type = ApiType.openapi, + URL = openApiDocumentPath + }, + ContactEmail = manifestInfo.ContactEmail, + LogoUrl = manifestInfo.LogoUrl, + LegalInfoUrl = manifestInfo.LegalUrl, + }; + } + private PluginManifestDocument GetManifestDocument(string openApiDocumentPath) { var (runtimes, functions) = GetRuntimesAndFunctionsFromTree(TreeNode, openApiDocumentPath); var descriptionForHuman = OAIDocument.Info?.Description.CleanupXMLString() is string d && !string.IsNullOrEmpty(d) ? d : $"Description for {OAIDocument.Info?.Title.CleanupXMLString()}"; - var descriptionForModel = descriptionForHuman; - string? legalUrl = null; - string? logoUrl = null; - string? privacyUrl = null; - if (OAIDocument.Info is not null) - { - if (OAIDocument.Info.Extensions.TryGetValue(OpenApiDescriptionForModelExtension.Name, out var descriptionExtension) && - descriptionExtension is OpenApiDescriptionForModelExtension extension && - !string.IsNullOrEmpty(extension.Description)) - descriptionForModel = extension.Description.CleanupXMLString(); - if (OAIDocument.Info.Extensions.TryGetValue(OpenApiLegalInfoUrlExtension.Name, out var legalExtension) && legalExtension is OpenApiLegalInfoUrlExtension legal) - legalUrl = legal.Legal; - if (OAIDocument.Info.Extensions.TryGetValue(OpenApiLogoExtension.Name, out var logoExtension) && logoExtension is OpenApiLogoExtension logo) - logoUrl = logo.Url; - if (OAIDocument.Info.Extensions.TryGetValue(OpenApiPrivacyPolicyUrlExtension.Name, out var privacyExtension) && privacyExtension is OpenApiPrivacyPolicyUrlExtension privacy) - privacyUrl = privacy.Privacy; - } + var manifestInfo = ExtractInfoFromDocument(OAIDocument.Info); return new PluginManifestDocument { SchemaVersion = "v2", NameForHuman = OAIDocument.Info?.Title.CleanupXMLString(), // TODO name for model ??? DescriptionForHuman = descriptionForHuman, - DescriptionForModel = descriptionForModel, - ContactEmail = OAIDocument.Info?.Contact?.Email, + DescriptionForModel = manifestInfo.DescriptionForModel ?? descriptionForHuman, + ContactEmail = manifestInfo.ContactEmail, Namespace = Configuration.ClientClassName, - LogoUrl = logoUrl, - LegalInfoUrl = legalUrl, - PrivacyPolicyUrl = privacyUrl, + LogoUrl = manifestInfo.LogoUrl, + LegalInfoUrl = manifestInfo.LegalUrl, + PrivacyPolicyUrl = manifestInfo.PrivacyUrl, Runtimes = [.. runtimes .GroupBy(static x => x, _openAPIRuntimeComparer) .Select(static x => @@ -127,7 +145,40 @@ descriptionExtension is OpenApiDescriptionForModelExtension extension && Functions = [.. functions.OrderBy(static x => x.Name, StringComparer.OrdinalIgnoreCase)] }; } - private (OpenApiRuntime[], Function[]) GetRuntimesAndFunctionsFromTree(OpenApiUrlTreeNode currentNode, string openApiDocumentPath) + + private static OpenApiManifestInfo ExtractInfoFromDocument(OpenApiInfo? openApiInfo) + { + var manifestInfo = new OpenApiManifestInfo(); + + if (openApiInfo is null) + return manifestInfo; + + string? descriptionForModel = null; + string? legalUrl = null; + string? logoUrl = null; + string? privacyUrl = null; + string contactEmail = string.IsNullOrEmpty(openApiInfo.Contact?.Email) + ? DefaultContactEmail + : openApiInfo.Contact.Email; + + if (openApiInfo.Extensions.TryGetValue(OpenApiDescriptionForModelExtension.Name, out var descriptionExtension) && + descriptionExtension is OpenApiDescriptionForModelExtension extension && + !string.IsNullOrEmpty(extension.Description)) + descriptionForModel = extension.Description.CleanupXMLString(); + if (openApiInfo.Extensions.TryGetValue(OpenApiLegalInfoUrlExtension.Name, out var legalExtension) && legalExtension is OpenApiLegalInfoUrlExtension legal) + legalUrl = legal.Legal; + if (openApiInfo.Extensions.TryGetValue(OpenApiLogoExtension.Name, out var logoExtension) && logoExtension is OpenApiLogoExtension logo) + logoUrl = logo.Url; + if (openApiInfo.Extensions.TryGetValue(OpenApiPrivacyPolicyUrlExtension.Name, out var privacyExtension) && privacyExtension is OpenApiPrivacyPolicyUrlExtension privacy) + privacyUrl = privacy.Privacy; + + return new OpenApiManifestInfo(descriptionForModel, legalUrl, logoUrl, privacyUrl, contactEmail); + + } + private const string DefaultContactName = "publisher-name"; + private const string DefaultContactEmail = "publisher-email@example.com"; + private sealed record OpenApiManifestInfo(string? DescriptionForModel = null, string? LegalUrl = null, string? LogoUrl = null, string? PrivacyUrl = null, string ContactEmail = DefaultContactEmail); + private static (OpenApiRuntime[], Function[]) GetRuntimesAndFunctionsFromTree(OpenApiUrlTreeNode currentNode, string openApiDocumentPath) { var runtimes = new List(); var functions = new List(); diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index d638375cce..ca604039e0 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -43,7 +43,23 @@ public async Task GeneratesManifest() paths: /test: get: + description: description for test path operationId: test + responses: + '200': + description: test + /test/{id}: + get: + description: description for test path with id + operationId: test_WithId + parameters: + - name: id + in: path + required: true + description: The id of the test + schema: + type: integer + format: int32 responses: '200': description: test"; @@ -57,7 +73,7 @@ public async Task GeneratesManifest() { OutputPath = outputDirectory, OpenAPIFilePath = "openapiPath", - PluginTypes = [PluginType.Microsoft, PluginType.APIManifest], + PluginTypes = [PluginType.Microsoft, PluginType.APIManifest, PluginType.OpenAI], ClientClassName = "client", ApiRootUrl = "http://localhost/", //Kiota builder would set this for us }; @@ -70,12 +86,26 @@ public async Task GeneratesManifest() Assert.True(File.Exists(Path.Combine(outputDirectory, ManifestFileName))); Assert.True(File.Exists(Path.Combine(outputDirectory, "client-apimanifest.json"))); + Assert.True(File.Exists(Path.Combine(outputDirectory, OpenAIPluginFileName))); Assert.True(File.Exists(Path.Combine(outputDirectory, OpenApiFileName))); + + // Validate the v2 plugin var manifestContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, ManifestFileName)); using var jsonDocument = JsonDocument.Parse(manifestContent); var resultingManifest = PluginManifestDocument.Load(jsonDocument.RootElement); + Assert.NotNull(resultingManifest.Document); Assert.Equal(OpenApiFileName, resultingManifest.Document.Runtimes.OfType().First().Spec.Url); + Assert.Empty(resultingManifest.Problems); + + // Validate the v1 plugin + var v1ManifestContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, OpenAIPluginFileName)); + using var v1JsonDocument = JsonDocument.Parse(v1ManifestContent); + var v1Manifest = PluginManifestDocument.Load(v1JsonDocument.RootElement); + Assert.NotNull(resultingManifest.Document); + Assert.Equal(OpenApiFileName, v1Manifest.Document.Api.URL); + Assert.Empty(v1Manifest.Problems); } private const string ManifestFileName = "client-microsoft.json"; + private const string OpenAIPluginFileName = "openai-plugins.json"; private const string OpenApiFileName = "client-openapi.yml"; } From ef20b87e62fa99cb89db362bf4d7aa963f4f1f81 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 15 May 2024 07:22:18 -0400 Subject: [PATCH 2/3] Update src/Kiota.Builder/Kiota.Builder.csproj --- src/Kiota.Builder/Kiota.Builder.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kiota.Builder/Kiota.Builder.csproj b/src/Kiota.Builder/Kiota.Builder.csproj index b3e8067a3f..1d6f61c0cc 100644 --- a/src/Kiota.Builder/Kiota.Builder.csproj +++ b/src/Kiota.Builder/Kiota.Builder.csproj @@ -47,7 +47,7 @@ - + From 2f1e8e69985348f67b32ded049d88c5688a74758 Mon Sep 17 00:00:00 2001 From: Andrew Omondi Date: Wed, 15 May 2024 14:42:57 +0300 Subject: [PATCH 3/3] Update src/Kiota.Builder/Kiota.Builder.csproj --- src/Kiota.Builder/Kiota.Builder.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kiota.Builder/Kiota.Builder.csproj b/src/Kiota.Builder/Kiota.Builder.csproj index 1d6f61c0cc..b3e8067a3f 100644 --- a/src/Kiota.Builder/Kiota.Builder.csproj +++ b/src/Kiota.Builder/Kiota.Builder.csproj @@ -47,7 +47,7 @@ - +