From 8cab9ba3d4798b9962cce9ede0b1e0889b631823 Mon Sep 17 00:00:00 2001 From: thewahome Date: Mon, 13 Jan 2025 12:44:28 +0300 Subject: [PATCH 1/7] add test for static template --- .../Plugins/PluginsGenerationService.cs | 10 +- .../Plugins/PluginsGenerationServiceTests.cs | 127 ++++++++++++++++++ 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 1406aa91c7..87f65ecdfd 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -443,13 +443,19 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes var summary = operation.Summary.CleanupXMLString(); var description = operation.Description.CleanupXMLString(); - + string staticTemplate = @"{""type"":""AdaptiveCard"",""$schema"":""http://adaptivecards.io/schemas/adaptive-card.json"",""version"":""1.5"",""body"":[{""type"":""Container"",""items"":[{""type"":""TextBlock"",""text"":""user.login: ${if(user.login, user.login, 'N/A')}""},{""type"":""Image"",""url"":""${if(user.avatar_url != null && user.avatar_url != '', user.avatar_url, '')}""},{""type"":""TextBlock"",""text"":""created_at: ${if(created_at, created_at, 'N/A')}"",""wrap"":true},{""type"":""TextBlock"",""text"":""closed_at: ${if(closed_at, closed_at, 'Still Open')}"",""wrap"":true},{""type"":""TextBlock"",""text"":""assignee: ${if(assignee, assignee, 'No assignees to work on this issue')}"",""wrap"":true}]}]}"; functions.Add(new Function { Name = operation.OperationId, Description = !string.IsNullOrEmpty(description) ? description : summary, States = GetStatesFromOperation(operation), - + Capabilities = new FunctionCapabilities() + { + ResponseSemantics = new ResponseSemantics() + { + StaticTemplate = JsonDocument.Parse(staticTemplate).RootElement + } + } }); conversationStarters.Add(new ConversationStarter { diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 6743f5c63a..f73b07f18c 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; @@ -815,4 +816,130 @@ public async Task MergesAllOfRequestBodyAsync(string content, Action>(); + var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, _logger); + var outputDirectory = Path.Combine(workingDirectory, "output"); + var generationConfiguration = new GenerationConfiguration + { + OutputPath = outputDirectory, + OpenAPIFilePath = "openapiPath", + PluginTypes = [PluginType.APIPlugin, PluginType.APIManifest, PluginType.OpenAI], + ClientClassName = inputPluginName, + ApiRootUrl = "http://localhost/", //Kiota builder would set this for us + }; + var (openAPIDocumentStream, _) = await openAPIDocumentDS.LoadStreamAsync(simpleDescriptionPath, generationConfiguration, null, false); + var openApiDocument = await openAPIDocumentDS.GetDocumentFromStreamAsync(openAPIDocumentStream, generationConfiguration); + KiotaBuilder.CleanupOperationIdForPlugins(openApiDocument); + var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel); + + var pluginsGenerationService = new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory, _logger); + await pluginsGenerationService.GenerateManifestAsync(); + + Assert.True(File.Exists(Path.Combine(outputDirectory, $"{inputPluginName.ToLower()}-apiplugin.json"))); + Assert.True(File.Exists(Path.Combine(outputDirectory, $"{inputPluginName.ToLower()}-apimanifest.json"))); + + // Validate the v2 plugin + var manifestContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, $"{inputPluginName.ToLower()}-apiplugin.json")); + using var jsonDocument = JsonDocument.Parse(manifestContent); + var resultingManifest = PluginManifestDocument.Load(jsonDocument.RootElement); + string expectedStaticTemplate = @"{""type"":""AdaptiveCard"",""$schema"":""http://adaptivecards.io/schemas/adaptive-card.json"",""version"":""1.5"",""body"":[{""type"":""Container"",""items"":[{""type"":""TextBlock"",""text"":""user.login: ${if(user.login, user.login, 'N/A')}""},{""type"":""Image"",""url"":""${if(user.avatar_url != null && user.avatar_url != '', user.avatar_url, '')}""},{""type"":""TextBlock"",""text"":""created_at: ${if(created_at, created_at, 'N/A')}"",""wrap"":true},{""type"":""TextBlock"",""text"":""closed_at: ${if(closed_at, closed_at, 'Still Open')}"",""wrap"":true},{""type"":""TextBlock"",""text"":""assignee: ${if(assignee, assignee, 'No assignees to work on this issue')}"",""wrap"":true}]}]}"; + + Assert.NotNull(resultingManifest.Document); + Assert.Equal($"{inputPluginName.ToLower()}-openapi.yml", resultingManifest.Document.Runtimes.OfType().First().Spec.Url); + Assert.NotNull(resultingManifest.Document.Functions[1].Capabilities.ResponseSemantics.StaticTemplate); + var expectedJson = JsonDocument.Parse(expectedStaticTemplate).RootElement; + var actualJson = resultingManifest.Document.Functions[1].Capabilities.ResponseSemantics.StaticTemplate; + if (actualJson.HasValue) + { + // Properties to compare + List propertiesToCompare = new List { "type", "$schema", "version" }; + + // Compare properties + foreach (string prop in propertiesToCompare) + { + Assert.Equal(expectedJson.GetProperty(prop).ToString(), actualJson.Value.GetProperty(prop).ToString()); + } + + // Compare the body array separately + var expectedBody = expectedJson.GetProperty("body").ToString(); + var actualBody = actualJson.Value.GetProperty("body").ToString(); + Assert.Equal(expectedBody, actualBody); + } + else + { + Assert.True(false, "actualJson is null"); + } + + Assert.Contains("description for test path with id", resultingManifest.Document.Functions[1].Description); + Assert.Equal(2, resultingManifest.Document.Functions.Count); + Assert.Contains("Summary for test path with id", resultingManifest.Document.Capabilities.ConversationStarters[1].Text); + Assert.True(resultingManifest.Document.Capabilities.ConversationStarters[1].Text.Length <= 50); + Assert.Equal(inputPluginName, resultingManifest.Document.Namespace); + Assert.Empty(resultingManifest.Problems); + Assert.Equal("test description we've created", resultingManifest.Document.DescriptionForHuman); + } + + #endregion } From c573932faa8916075f45812bf3095499b82d2b41 Mon Sep 17 00:00:00 2001 From: thewahome Date: Mon, 13 Jan 2025 16:34:48 +0300 Subject: [PATCH 2/7] create adaptive card generator from the operation --- .../Plugins/AdaptiveCardGenerator.cs | 42 ++++++++++++++ .../Plugins/AdaptiveCardGeneratorTests.cs | 55 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/Kiota.Builder/Plugins/AdaptiveCardGenerator.cs create mode 100644 tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs diff --git a/src/Kiota.Builder/Plugins/AdaptiveCardGenerator.cs b/src/Kiota.Builder/Plugins/AdaptiveCardGenerator.cs new file mode 100644 index 0000000000..8daafe706f --- /dev/null +++ b/src/Kiota.Builder/Plugins/AdaptiveCardGenerator.cs @@ -0,0 +1,42 @@ +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; + +namespace Kiota.Builder.Plugins +{ + public class AdaptiveCardGenerator + { + public AdaptiveCardGenerator() { } + + public string GenerateAdaptiveCard(OpenApiOperation operation) + { + ArgumentNullException.ThrowIfNull(operation); + + var responses = operation.Responses; + var response = responses["200"]; + ArgumentNullException.ThrowIfNull(response); + + var schema = response.Content["application/json"].Schema; + ArgumentNullException.ThrowIfNull(schema); + + var properties = schema.Properties; + ArgumentNullException.ThrowIfNull(properties); + + var builder = new StringBuilder(); + builder.Append("{\"type\":\"AdaptiveCard\",\"$schema\":\"https://adaptivecards.io/schemas/adaptive-card.json\",\"version\":\"1.5\",\"body\":["); + foreach (var property in properties) + { + builder.Append("{\"type\":\"TextBlock\",\"text\":\"" + property.Key + ": ${if(" + property.Key + ", " + property.Key + ", 'N/A')}\""); + builder.Append(",\"wrap\":true}"); + builder.Append(','); + } + builder.Remove(builder.Length - 1, 1); + builder.Append("]}"); + return builder.ToString(); + } + } +} diff --git a/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs b/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs new file mode 100644 index 0000000000..e205fd6d7e --- /dev/null +++ b/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Kiota.Builder.Plugins; +using Microsoft.OpenApi.Models; +using Xunit; + +namespace Kiota.Builder.Tests.Plugins +{ + public sealed class AdaptiveCardGeneratorTests + { + [Fact] + public void GenerateAdaptiveCardFromOperation() + { + var sample = new OpenApiOperation + { + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "OK", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary + { + ["name"] = new OpenApiSchema + { + Type = "string" + }, + ["age"] = new OpenApiSchema + { + Type = "number" + } + } + } + } + } + } + } + }; + var expected = @"{""type"":""AdaptiveCard"",""$schema"":""https://adaptivecards.io/schemas/adaptive-card.json"",""version"":""1.5"",""body"":[{""type"":""TextBlock"",""text"":""name: ${if(name, name, 'N/A')}"",""wrap"":true},{""type"":""TextBlock"",""text"":""age: ${if(age, age, 'N/A')}"",""wrap"":true}]}"; + + var generator = new AdaptiveCardGenerator(); + var card = generator.GenerateAdaptiveCard(sample); + Assert.Equal(expected, card); + } + } +} From 26c7deed640f145e03da94886c9cd0c8ea9e0b23 Mon Sep 17 00:00:00 2001 From: thewahome Date: Mon, 13 Jan 2025 16:35:09 +0300 Subject: [PATCH 3/7] use adaptive card generator in plugin generation --- .../Plugins/PluginsGenerationService.cs | 6 +- .../Plugins/PluginsGenerationServiceTests.cs | 90 +++++++++---------- 2 files changed, 45 insertions(+), 51 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 87f65ecdfd..502119eaaa 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.Emit; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; @@ -443,7 +444,10 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes var summary = operation.Summary.CleanupXMLString(); var description = operation.Description.CleanupXMLString(); - string staticTemplate = @"{""type"":""AdaptiveCard"",""$schema"":""http://adaptivecards.io/schemas/adaptive-card.json"",""version"":""1.5"",""body"":[{""type"":""Container"",""items"":[{""type"":""TextBlock"",""text"":""user.login: ${if(user.login, user.login, 'N/A')}""},{""type"":""Image"",""url"":""${if(user.avatar_url != null && user.avatar_url != '', user.avatar_url, '')}""},{""type"":""TextBlock"",""text"":""created_at: ${if(created_at, created_at, 'N/A')}"",""wrap"":true},{""type"":""TextBlock"",""text"":""closed_at: ${if(closed_at, closed_at, 'Still Open')}"",""wrap"":true},{""type"":""TextBlock"",""text"":""assignee: ${if(assignee, assignee, 'No assignees to work on this issue')}"",""wrap"":true}]}]}"; + + var generator = new AdaptiveCardGenerator(); + string staticTemplate = generator.GenerateAdaptiveCard(operation); + functions.Add(new Function { Name = operation.OperationId, diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index f73b07f18c..224b70e10c 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -824,56 +824,52 @@ public async Task GeneratesManifestWithAdaptiveCardAsync() { var simpleDescriptionContent = @"openapi: 3.0.0 info: - title: test - version: 1.0 - description: test description we've created + title: Microsoft Graph get user API + version: 1.0.0 servers: - - url: http://localhost/ - description: There's no place like home + - url: https://graph.microsoft.com/v1.0/ paths: - /test: + /me: get: - summary: summary for test path - description: description for test path responses: - '200': - description: test - /test/{id}: + 200: + description: Success! + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.user' + /me/get: get: - summary: Summary for test path with id that is longer than 50 characters - 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: Response - content: + 200: + description: Success! + content: application/json: - schema: - type: object - required: - - total_count - - incomplete_results - - items - properties: - total_count: - type: integer - incomplete_results: - type: boolean - items: - type: array - items: - $ref: '#/components/schemas/issue-search-result-item' - examples: - default: - $ref: '#/components/examples/issue-search-result-item-paginated'"; + schema: + $ref: '#/components/schemas/microsoft.graph.user' +components: + schemas: + microsoft.graph.user: + type: object + properties: + id: + type: string + displayName: + type: string + otherNames: + type: array + items: + type: string + nullable: true + importance: + $ref: '#/components/schemas/microsoft.graph.importance' + microsoft.graph.importance: + title: importance + enum: + - low + - normal + - high + type: string"; var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; var inputPluginName = "client"; @@ -904,7 +900,7 @@ public async Task GeneratesManifestWithAdaptiveCardAsync() var manifestContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, $"{inputPluginName.ToLower()}-apiplugin.json")); using var jsonDocument = JsonDocument.Parse(manifestContent); var resultingManifest = PluginManifestDocument.Load(jsonDocument.RootElement); - string expectedStaticTemplate = @"{""type"":""AdaptiveCard"",""$schema"":""http://adaptivecards.io/schemas/adaptive-card.json"",""version"":""1.5"",""body"":[{""type"":""Container"",""items"":[{""type"":""TextBlock"",""text"":""user.login: ${if(user.login, user.login, 'N/A')}""},{""type"":""Image"",""url"":""${if(user.avatar_url != null && user.avatar_url != '', user.avatar_url, '')}""},{""type"":""TextBlock"",""text"":""created_at: ${if(created_at, created_at, 'N/A')}"",""wrap"":true},{""type"":""TextBlock"",""text"":""closed_at: ${if(closed_at, closed_at, 'Still Open')}"",""wrap"":true},{""type"":""TextBlock"",""text"":""assignee: ${if(assignee, assignee, 'No assignees to work on this issue')}"",""wrap"":true}]}]}"; + string expectedStaticTemplate = "{\"type\":\"AdaptiveCard\",\"$schema\":\"https://adaptivecards.io/schemas/adaptive-card.json\",\"version\":\"1.5\",\"body\":[{\"type\":\"TextBlock\",\"text\":\"id: ${if(id, id, 'N/A')}\",\"wrap\":true},{\"type\":\"TextBlock\",\"text\":\"displayName: ${if(displayName, displayName, 'N/A')}\",\"wrap\":true},{\"type\":\"TextBlock\",\"text\":\"otherNames: ${if(otherNames, otherNames, 'N/A')}\",\"wrap\":true},{\"type\":\"TextBlock\",\"text\":\"importance: ${if(importance, importance, 'N/A')}\",\"wrap\":true}]}"; Assert.NotNull(resultingManifest.Document); Assert.Equal($"{inputPluginName.ToLower()}-openapi.yml", resultingManifest.Document.Runtimes.OfType().First().Spec.Url); @@ -932,13 +928,7 @@ public async Task GeneratesManifestWithAdaptiveCardAsync() Assert.True(false, "actualJson is null"); } - Assert.Contains("description for test path with id", resultingManifest.Document.Functions[1].Description); - Assert.Equal(2, resultingManifest.Document.Functions.Count); - Assert.Contains("Summary for test path with id", resultingManifest.Document.Capabilities.ConversationStarters[1].Text); - Assert.True(resultingManifest.Document.Capabilities.ConversationStarters[1].Text.Length <= 50); Assert.Equal(inputPluginName, resultingManifest.Document.Namespace); - Assert.Empty(resultingManifest.Problems); - Assert.Equal("test description we've created", resultingManifest.Document.DescriptionForHuman); } #endregion From add0d021bcb3c5e29da52aa857d236586435b0f7 Mon Sep 17 00:00:00 2001 From: thewahome Date: Mon, 13 Jan 2025 18:05:28 +0300 Subject: [PATCH 4/7] use the adaptive cards library instead of string builder --- src/Kiota.Builder/Kiota.Builder.csproj | 1 + .../Plugins/AdaptiveCardGenerator.cs | 45 ++++++++++++++----- .../Plugins/AdaptiveCardGeneratorTests.cs | 9 +++- .../Plugins/PluginsGenerationServiceTests.cs | 4 +- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/Kiota.Builder/Kiota.Builder.csproj b/src/Kiota.Builder/Kiota.Builder.csproj index 82b4cbf9a4..9dc7201e3d 100644 --- a/src/Kiota.Builder/Kiota.Builder.csproj +++ b/src/Kiota.Builder/Kiota.Builder.csproj @@ -35,6 +35,7 @@ $(NoWarn);CS8785;NU5048;NU5104;CA1724;CA1055;CA1848;CA1308;CA1822 + diff --git a/src/Kiota.Builder/Plugins/AdaptiveCardGenerator.cs b/src/Kiota.Builder/Plugins/AdaptiveCardGenerator.cs index 8daafe706f..aa93ba2fe2 100644 --- a/src/Kiota.Builder/Plugins/AdaptiveCardGenerator.cs +++ b/src/Kiota.Builder/Plugins/AdaptiveCardGenerator.cs @@ -4,39 +4,60 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using AdaptiveCards; using Microsoft.OpenApi.Models; namespace Kiota.Builder.Plugins { public class AdaptiveCardGenerator { - public AdaptiveCardGenerator() { } + public AdaptiveCardGenerator() + { + } public string GenerateAdaptiveCard(OpenApiOperation operation) { ArgumentNullException.ThrowIfNull(operation); - + var responses = operation.Responses; var response = responses["200"]; ArgumentNullException.ThrowIfNull(response); - + var schema = response.Content["application/json"].Schema; ArgumentNullException.ThrowIfNull(schema); var properties = schema.Properties; ArgumentNullException.ThrowIfNull(properties); - - var builder = new StringBuilder(); - builder.Append("{\"type\":\"AdaptiveCard\",\"$schema\":\"https://adaptivecards.io/schemas/adaptive-card.json\",\"version\":\"1.5\",\"body\":["); + + AdaptiveCard card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 5)); + foreach (var property in properties) { - builder.Append("{\"type\":\"TextBlock\",\"text\":\"" + property.Key + ": ${if(" + property.Key + ", " + property.Key + ", 'N/A')}\""); - builder.Append(",\"wrap\":true}"); - builder.Append(','); + + if (property.Value.Type == "string" && property.Value.Format == "uri") + { + card.Body.Add(new AdaptiveImage() + { + Url = new Uri($"${{{property.Key}}}"), + Size = AdaptiveImageSize.Large, + }); + } + else if (property.Value.Type == "array") + { + card.Body.Add(new AdaptiveTextBlock() + { + Text = $"${{{property.Key}.join(', ')}}", + }); + } + else + { + card.Body.Add(new AdaptiveTextBlock() + { + Text = $"${{{property.Key}, {property.Key}, 'N/A'}}", + }); + } } - builder.Remove(builder.Length - 1, 1); - builder.Append("]}"); - return builder.ToString(); + return card.ToJson(); } } } diff --git a/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs b/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs index e205fd6d7e..77273231f4 100644 --- a/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Kiota.Builder.Plugins; using Microsoft.OpenApi.Models; @@ -45,11 +46,15 @@ public void GenerateAdaptiveCardFromOperation() } } }; - var expected = @"{""type"":""AdaptiveCard"",""$schema"":""https://adaptivecards.io/schemas/adaptive-card.json"",""version"":""1.5"",""body"":[{""type"":""TextBlock"",""text"":""name: ${if(name, name, 'N/A')}"",""wrap"":true},{""type"":""TextBlock"",""text"":""age: ${if(age, age, 'N/A')}"",""wrap"":true}]}"; + string expected = "{\r\n \"type\": \"AdaptiveCard\",\r\n \"version\": \"1.5\",\r\n \"body\": [\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${name, name, 'N/A'}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${age, age, 'N/A'}\"\r\n }\r\n ]\r\n}"; var generator = new AdaptiveCardGenerator(); var card = generator.GenerateAdaptiveCard(sample); - Assert.Equal(expected, card); + + var expectedJson = JsonDocument.Parse(expected).RootElement.GetRawText(); + var actualJson = JsonDocument.Parse(card).RootElement.GetRawText(); + + Assert.Equal(expectedJson, actualJson); } } } diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 224b70e10c..779c97c3f1 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -900,7 +900,7 @@ public async Task GeneratesManifestWithAdaptiveCardAsync() var manifestContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, $"{inputPluginName.ToLower()}-apiplugin.json")); using var jsonDocument = JsonDocument.Parse(manifestContent); var resultingManifest = PluginManifestDocument.Load(jsonDocument.RootElement); - string expectedStaticTemplate = "{\"type\":\"AdaptiveCard\",\"$schema\":\"https://adaptivecards.io/schemas/adaptive-card.json\",\"version\":\"1.5\",\"body\":[{\"type\":\"TextBlock\",\"text\":\"id: ${if(id, id, 'N/A')}\",\"wrap\":true},{\"type\":\"TextBlock\",\"text\":\"displayName: ${if(displayName, displayName, 'N/A')}\",\"wrap\":true},{\"type\":\"TextBlock\",\"text\":\"otherNames: ${if(otherNames, otherNames, 'N/A')}\",\"wrap\":true},{\"type\":\"TextBlock\",\"text\":\"importance: ${if(importance, importance, 'N/A')}\",\"wrap\":true}]}"; + string expectedStaticTemplate = "{\r\n \"type\": \"AdaptiveCard\",\r\n \"version\": \"1.5\",\r\n \"body\": [\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${id, id, 'N/A'}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${displayName, displayName, 'N/A'}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${otherNames.join(', ')}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${importance, importance, 'N/A'}\"\r\n }\r\n ]\r\n}"; Assert.NotNull(resultingManifest.Document); Assert.Equal($"{inputPluginName.ToLower()}-openapi.yml", resultingManifest.Document.Runtimes.OfType().First().Spec.Url); @@ -910,7 +910,7 @@ public async Task GeneratesManifestWithAdaptiveCardAsync() if (actualJson.HasValue) { // Properties to compare - List propertiesToCompare = new List { "type", "$schema", "version" }; + List propertiesToCompare = new List { "type", "version", "body"}; // Compare properties foreach (string prop in propertiesToCompare) From cf7ec73a733a2cd222a54ea54ab4fd3d43379023 Mon Sep 17 00:00:00 2001 From: thewahome Date: Tue, 14 Jan 2025 12:21:29 +0300 Subject: [PATCH 5/7] Generate adaptive cards based on GenerationConfiguration setting --- .../Configuration/GenerationConfiguration.cs | 8 ++ .../Plugins/PluginsGenerationService.cs | 35 +++++--- .../Plugins/PluginsGenerationServiceTests.cs | 88 +++++++++++++++++++ 3 files changed, 117 insertions(+), 14 deletions(-) diff --git a/src/Kiota.Builder/Configuration/GenerationConfiguration.cs b/src/Kiota.Builder/Configuration/GenerationConfiguration.cs index eac80d47cd..8b015c0a5f 100644 --- a/src/Kiota.Builder/Configuration/GenerationConfiguration.cs +++ b/src/Kiota.Builder/Configuration/GenerationConfiguration.cs @@ -225,6 +225,14 @@ public PluginAuthConfiguration? PluginAuthInformation { get; set; } + + /// + /// When true, should allow generation of adaptive cards + /// + public bool? ShouldGenerateAdaptiveCards + { + get; set; + } } #pragma warning restore CA1056 #pragma warning restore CA2227 diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 502119eaaa..9a4cf8f454 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -333,7 +333,7 @@ private OpenApiDocument GetDocumentWithTrimmedComponentsAndResponses(OpenApiDocu private PluginManifestDocument GetManifestDocument(string openApiDocumentPath) { - var (runtimes, functions, conversationStarters) = GetRuntimesFunctionsAndConversationStartersFromTree(OAIDocument, Configuration.PluginAuthInformation, TreeNode, openApiDocumentPath, Logger); + var (runtimes, functions, conversationStarters) = GetRuntimesFunctionsAndConversationStartersFromTree(OAIDocument, Configuration, TreeNode, openApiDocumentPath, Logger); var descriptionForHuman = OAIDocument.Info?.Description is string d && !string.IsNullOrEmpty(d) ? d : $"Description for {OAIDocument.Info?.Title}"; var manifestInfo = ExtractInfoFromDocument(OAIDocument.Info); var pluginManifestDocument = new PluginManifestDocument @@ -412,13 +412,14 @@ private sealed record OpenApiManifestInfo( string? PrivacyUrl = null, string ContactEmail = DefaultContactEmail); - private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimesFunctionsAndConversationStartersFromTree(OpenApiDocument document, PluginAuthConfiguration? authInformation, OpenApiUrlTreeNode currentNode, + private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimesFunctionsAndConversationStartersFromTree(OpenApiDocument document, GenerationConfiguration configuration, OpenApiUrlTreeNode currentNode, string openApiDocumentPath, ILogger logger) { var runtimes = new List(); var functions = new List(); var conversationStarters = new List(); - var configAuth = authInformation?.ToPluginManifestAuth(); + var configAuth = configuration.PluginAuthInformation?.ToPluginManifestAuth(); + bool shouldGenerateAdaptiveCards = configuration.ShouldGenerateAdaptiveCards ?? false; if (currentNode.PathItems.TryGetValue(Constants.DefaultOpenApiLabel, out var pathItem)) { foreach (var operation in pathItem.Operations.Values.Where(static x => !string.IsNullOrEmpty(x.OperationId))) @@ -444,23 +445,29 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes var summary = operation.Summary.CleanupXMLString(); var description = operation.Description.CleanupXMLString(); - - var generator = new AdaptiveCardGenerator(); - string staticTemplate = generator.GenerateAdaptiveCard(operation); - - functions.Add(new Function + + var function = new Function { Name = operation.OperationId, Description = !string.IsNullOrEmpty(description) ? description : summary, - States = GetStatesFromOperation(operation), - Capabilities = new FunctionCapabilities() + States = GetStatesFromOperation(operation) + }; + + if (shouldGenerateAdaptiveCards) + { + var generator = new AdaptiveCardGenerator(); + string staticTemplate = generator.GenerateAdaptiveCard(operation); + function.Capabilities = new FunctionCapabilities { - ResponseSemantics = new ResponseSemantics() + ResponseSemantics = new ResponseSemantics { StaticTemplate = JsonDocument.Parse(staticTemplate).RootElement } - } - }); + }; + } + + functions.Add(function); + conversationStarters.Add(new ConversationStarter { Text = !string.IsNullOrEmpty(summary) ? summary : description @@ -471,7 +478,7 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes foreach (var node in currentNode.Children) { - var (childRuntimes, childFunctions, childConversationStarters) = GetRuntimesFunctionsAndConversationStartersFromTree(document, authInformation, node.Value, openApiDocumentPath, logger); + var (childRuntimes, childFunctions, childConversationStarters) = GetRuntimesFunctionsAndConversationStartersFromTree(document, configuration, node.Value, openApiDocumentPath, logger); runtimes.AddRange(childRuntimes); functions.AddRange(childFunctions); conversationStarters.AddRange(childConversationStarters); diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 779c97c3f1..c452bae0d7 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -884,6 +884,7 @@ public async Task GeneratesManifestWithAdaptiveCardAsync() PluginTypes = [PluginType.APIPlugin, PluginType.APIManifest, PluginType.OpenAI], ClientClassName = inputPluginName, ApiRootUrl = "http://localhost/", //Kiota builder would set this for us + ShouldGenerateAdaptiveCards = true, }; var (openAPIDocumentStream, _) = await openAPIDocumentDS.LoadStreamAsync(simpleDescriptionPath, generationConfiguration, null, false); var openApiDocument = await openAPIDocumentDS.GetDocumentFromStreamAsync(openAPIDocumentStream, generationConfiguration); @@ -931,5 +932,92 @@ public async Task GeneratesManifestWithAdaptiveCardAsync() Assert.Equal(inputPluginName, resultingManifest.Document.Namespace); } + [Fact] + public async Task GeneratesManifestWithoutAdaptiveCardAsync() + { + var simpleDescriptionContent = @"openapi: 3.0.0 +info: + title: Microsoft Graph get user API + version: 1.0.0 +servers: + - url: https://graph.microsoft.com/v1.0/ +paths: + /me: + get: + responses: + 200: + description: Success! + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.user' + /me/get: + get: + responses: + 200: + description: Success! + content: + application/json: + schema: + $ref: '#/components/schemas/microsoft.graph.user' +components: + schemas: + microsoft.graph.user: + type: object + properties: + id: + type: string + displayName: + type: string + otherNames: + type: array + items: + type: string + nullable: true + importance: + $ref: '#/components/schemas/microsoft.graph.importance' + microsoft.graph.importance: + title: importance + enum: + - low + - normal + - high + type: string"; + var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml"; + var inputPluginName = "client"; + await File.WriteAllTextAsync(simpleDescriptionPath, simpleDescriptionContent); + var mockLogger = new Mock>(); + var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, _logger); + var outputDirectory = Path.Combine(workingDirectory, "output"); + var generationConfiguration = new GenerationConfiguration + { + OutputPath = outputDirectory, + OpenAPIFilePath = "openapiPath", + PluginTypes = [PluginType.APIPlugin, PluginType.APIManifest, PluginType.OpenAI], + ClientClassName = inputPluginName, + ApiRootUrl = "http://localhost/", //Kiota builder would set this for us + }; + var (openAPIDocumentStream, _) = await openAPIDocumentDS.LoadStreamAsync(simpleDescriptionPath, generationConfiguration, null, false); + var openApiDocument = await openAPIDocumentDS.GetDocumentFromStreamAsync(openAPIDocumentStream, generationConfiguration); + KiotaBuilder.CleanupOperationIdForPlugins(openApiDocument); + var urlTreeNode = OpenApiUrlTreeNode.Create(openApiDocument, Constants.DefaultOpenApiLabel); + + var pluginsGenerationService = new PluginsGenerationService(openApiDocument, urlTreeNode, generationConfiguration, workingDirectory, _logger); + await pluginsGenerationService.GenerateManifestAsync(); + + Assert.True(File.Exists(Path.Combine(outputDirectory, $"{inputPluginName.ToLower()}-apiplugin.json"))); + Assert.True(File.Exists(Path.Combine(outputDirectory, $"{inputPluginName.ToLower()}-apimanifest.json"))); + + // Validate the v2 plugin + var manifestContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, $"{inputPluginName.ToLower()}-apiplugin.json")); + using var jsonDocument = JsonDocument.Parse(manifestContent); + var resultingManifest = PluginManifestDocument.Load(jsonDocument.RootElement); + string expectedStaticTemplate = "{\r\n \"type\": \"AdaptiveCard\",\r\n \"version\": \"1.5\",\r\n \"body\": [\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${id, id, 'N/A'}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${displayName, displayName, 'N/A'}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${otherNames.join(', ')}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${importance, importance, 'N/A'}\"\r\n }\r\n ]\r\n}"; + + Assert.NotNull(resultingManifest.Document); + Assert.Null(resultingManifest.Document.Functions[1].Capabilities); + } + #endregion } From 01825e30533b9d38095ff590e0cbee02457a0a17 Mon Sep 17 00:00:00 2001 From: thewahome Date: Tue, 14 Jan 2025 13:05:56 +0300 Subject: [PATCH 6/7] have the adaptive card generator, generate an adaptive card --- .../Plugins/AdaptiveCardGenerator.cs | 4 +-- .../Plugins/PluginsGenerationService.cs | 4 +-- .../Plugins/AdaptiveCardGeneratorTests.cs | 18 ++++++++---- .../Plugins/PluginsGenerationServiceTests.cs | 28 ++++++++++++++++--- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/Kiota.Builder/Plugins/AdaptiveCardGenerator.cs b/src/Kiota.Builder/Plugins/AdaptiveCardGenerator.cs index aa93ba2fe2..56e4cf069b 100644 --- a/src/Kiota.Builder/Plugins/AdaptiveCardGenerator.cs +++ b/src/Kiota.Builder/Plugins/AdaptiveCardGenerator.cs @@ -15,7 +15,7 @@ public AdaptiveCardGenerator() { } - public string GenerateAdaptiveCard(OpenApiOperation operation) + public AdaptiveCard GenerateAdaptiveCard(OpenApiOperation operation) { ArgumentNullException.ThrowIfNull(operation); @@ -57,7 +57,7 @@ public string GenerateAdaptiveCard(OpenApiOperation operation) }); } } - return card.ToJson(); + return card; } } } diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index 9a4cf8f454..d070083670 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -456,12 +456,12 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes if (shouldGenerateAdaptiveCards) { var generator = new AdaptiveCardGenerator(); - string staticTemplate = generator.GenerateAdaptiveCard(operation); + var card = generator.GenerateAdaptiveCard(operation); function.Capabilities = new FunctionCapabilities { ResponseSemantics = new ResponseSemantics { - StaticTemplate = JsonDocument.Parse(staticTemplate).RootElement + StaticTemplate = JsonDocument.Parse(card.ToJson()).RootElement } }; } diff --git a/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs b/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs index 77273231f4..69fe239b4a 100644 --- a/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs @@ -4,6 +4,7 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using AdaptiveCards; using Kiota.Builder.Plugins; using Microsoft.OpenApi.Models; using Xunit; @@ -46,15 +47,20 @@ public void GenerateAdaptiveCardFromOperation() } } }; - string expected = "{\r\n \"type\": \"AdaptiveCard\",\r\n \"version\": \"1.5\",\r\n \"body\": [\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${name, name, 'N/A'}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${age, age, 'N/A'}\"\r\n }\r\n ]\r\n}"; + var expectedCard = new AdaptiveCard(new AdaptiveSchemaVersion(1, 5)); + expectedCard.Body.Add(new AdaptiveTextBlock() + { + Text = "${name, name, 'N/A'}", + }); + expectedCard.Body.Add(new AdaptiveTextBlock() + { + Text = "${age, age, 'N/A'}", + }); var generator = new AdaptiveCardGenerator(); - var card = generator.GenerateAdaptiveCard(sample); + var actualCard = generator.GenerateAdaptiveCard(sample); - var expectedJson = JsonDocument.Parse(expected).RootElement.GetRawText(); - var actualJson = JsonDocument.Parse(card).RootElement.GetRawText(); - - Assert.Equal(expectedJson, actualJson); + Assert.Equal(expectedCard.Body.Count, actualCard.Body.Count); } } } diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index c452bae0d7..59d21bb7b8 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -6,6 +6,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using AdaptiveCards; using Kiota.Builder.Configuration; using Kiota.Builder.Plugins; using Microsoft.Extensions.Logging; @@ -901,17 +902,37 @@ public async Task GeneratesManifestWithAdaptiveCardAsync() var manifestContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, $"{inputPluginName.ToLower()}-apiplugin.json")); using var jsonDocument = JsonDocument.Parse(manifestContent); var resultingManifest = PluginManifestDocument.Load(jsonDocument.RootElement); - string expectedStaticTemplate = "{\r\n \"type\": \"AdaptiveCard\",\r\n \"version\": \"1.5\",\r\n \"body\": [\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${id, id, 'N/A'}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${displayName, displayName, 'N/A'}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${otherNames.join(', ')}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${importance, importance, 'N/A'}\"\r\n }\r\n ]\r\n}"; Assert.NotNull(resultingManifest.Document); Assert.Equal($"{inputPluginName.ToLower()}-openapi.yml", resultingManifest.Document.Runtimes.OfType().First().Spec.Url); Assert.NotNull(resultingManifest.Document.Functions[1].Capabilities.ResponseSemantics.StaticTemplate); - var expectedJson = JsonDocument.Parse(expectedStaticTemplate).RootElement; + + var expectedCard = new AdaptiveCard(new AdaptiveSchemaVersion(1, 5)); + expectedCard.Body.Add(new AdaptiveTextBlock + { + Text = "${id, id, 'N/A'}" + }); + + expectedCard.Body.Add(new AdaptiveTextBlock + { + Text = "${displayName, displayName, 'N/A'}" + }); + expectedCard.Body.Add(new AdaptiveTextBlock + { + Text = "${otherNames.join(', ')}" + }); + expectedCard.Body.Add(new AdaptiveTextBlock + { + Text = "${importance, importance, 'N/A'}" + }); + var expectedJson = JsonDocument.Parse(expectedCard.ToJson()).RootElement; + + var actualJson = resultingManifest.Document.Functions[1].Capabilities.ResponseSemantics.StaticTemplate; if (actualJson.HasValue) { // Properties to compare - List propertiesToCompare = new List { "type", "version", "body"}; + List propertiesToCompare = new List { "type", "version", "body" }; // Compare properties foreach (string prop in propertiesToCompare) @@ -1013,7 +1034,6 @@ public async Task GeneratesManifestWithoutAdaptiveCardAsync() var manifestContent = await File.ReadAllTextAsync(Path.Combine(outputDirectory, $"{inputPluginName.ToLower()}-apiplugin.json")); using var jsonDocument = JsonDocument.Parse(manifestContent); var resultingManifest = PluginManifestDocument.Load(jsonDocument.RootElement); - string expectedStaticTemplate = "{\r\n \"type\": \"AdaptiveCard\",\r\n \"version\": \"1.5\",\r\n \"body\": [\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${id, id, 'N/A'}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${displayName, displayName, 'N/A'}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${otherNames.join(', ')}\"\r\n },\r\n {\r\n \"type\": \"TextBlock\",\r\n \"text\": \"${importance, importance, 'N/A'}\"\r\n }\r\n ]\r\n}"; Assert.NotNull(resultingManifest.Document); Assert.Null(resultingManifest.Document.Functions[1].Capabilities); From b3b7a8f17cb55ab8432e455adcfb71878c41bb62 Mon Sep 17 00:00:00 2001 From: thewahome Date: Tue, 14 Jan 2025 13:18:38 +0300 Subject: [PATCH 7/7] fix formatting errors --- src/Kiota.Builder/Plugins/PluginsGenerationService.cs | 1 - tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs | 1 - .../Plugins/PluginsGenerationServiceTests.cs | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs index d070083670..bed7057172 100644 --- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs +++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs @@ -445,7 +445,6 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes var summary = operation.Summary.CleanupXMLString(); var description = operation.Description.CleanupXMLString(); - var function = new Function { Name = operation.OperationId, diff --git a/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs b/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs index 69fe239b4a..88f24702f6 100644 --- a/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs @@ -59,7 +59,6 @@ public void GenerateAdaptiveCardFromOperation() var generator = new AdaptiveCardGenerator(); var actualCard = generator.GenerateAdaptiveCard(sample); - Assert.Equal(expectedCard.Body.Count, actualCard.Body.Count); } } diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs index 59d21bb7b8..d04a62a27d 100644 --- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs @@ -947,7 +947,7 @@ public async Task GeneratesManifestWithAdaptiveCardAsync() } else { - Assert.True(false, "actualJson is null"); + Assert.Fail("actualJson is null"); } Assert.Equal(inputPluginName, resultingManifest.Document.Namespace);