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";
}