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/Kiota.Builder.csproj b/src/Kiota.Builder/Kiota.Builder.csproj
index 70463a727c..c0b815e8ce 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
new file mode 100644
index 0000000000..56e4cf069b
--- /dev/null
+++ b/src/Kiota.Builder/Plugins/AdaptiveCardGenerator.cs
@@ -0,0 +1,63 @@
+using System;
+using System.CodeDom.Compiler;
+using System.Collections.Generic;
+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 AdaptiveCard 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);
+
+ AdaptiveCard card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 5));
+
+ foreach (var property in properties)
+ {
+
+ 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'}}",
+ });
+ }
+ }
+ return card;
+ }
+ }
+}
diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
index 1406aa91c7..bed7057172 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;
@@ -332,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
@@ -411,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)))
@@ -443,14 +445,28 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes
var summary = operation.Summary.CleanupXMLString();
var description = operation.Description.CleanupXMLString();
-
- functions.Add(new Function
+ var function = new Function
{
Name = operation.OperationId,
Description = !string.IsNullOrEmpty(description) ? description : summary,
- States = GetStatesFromOperation(operation),
+ States = GetStatesFromOperation(operation)
+ };
+
+ if (shouldGenerateAdaptiveCards)
+ {
+ var generator = new AdaptiveCardGenerator();
+ var card = generator.GenerateAdaptiveCard(operation);
+ function.Capabilities = new FunctionCapabilities
+ {
+ ResponseSemantics = new ResponseSemantics
+ {
+ StaticTemplate = JsonDocument.Parse(card.ToJson()).RootElement
+ }
+ };
+ }
+
+ functions.Add(function);
- });
conversationStarters.Add(new ConversationStarter
{
Text = !string.IsNullOrEmpty(summary) ? summary : description
@@ -461,7 +477,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/AdaptiveCardGeneratorTests.cs b/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs
new file mode 100644
index 0000000000..88f24702f6
--- /dev/null
+++ b/tests/Kiota.Builder.Tests/Plugins/AdaptiveCardGeneratorTests.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using AdaptiveCards;
+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 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 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 6743f5c63a..d04a62a27d 100644
--- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
+++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
@@ -1,10 +1,12 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
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;
@@ -815,4 +817,227 @@ 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
+ ShouldGenerateAdaptiveCards = true,
+ };
+ 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);
+
+ 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 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" };
+
+ // 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.Fail("actualJson is null");
+ }
+
+ 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);
+
+ Assert.NotNull(resultingManifest.Document);
+ Assert.Null(resultingManifest.Document.Functions[1].Capabilities);
+ }
+
+ #endregion
}