diff --git a/src/Kiota.Builder/Configuration/GenerationConfiguration.cs b/src/Kiota.Builder/Configuration/GenerationConfiguration.cs
index 553599b03a..e39485dc17 100644
--- a/src/Kiota.Builder/Configuration/GenerationConfiguration.cs
+++ b/src/Kiota.Builder/Configuration/GenerationConfiguration.cs
@@ -239,6 +239,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 1ff6766af6..01556b887c 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 f1f683a1c5..6bd703fcc2 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.Json.Nodes;
using System.Text.RegularExpressions;
@@ -417,7 +418,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
@@ -499,13 +500,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) && pathItem.Operations is not null)
{
foreach (var operation in pathItem.Operations.Values.Where(static x => !string.IsNullOrEmpty(x.OperationId)))
@@ -536,15 +538,17 @@ 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),
- Capabilities = GetFunctionCapabilitiesFromOperation(operation),
+ Capabilities = GetFunctionCapabilitiesFromOperation(operation, shouldGenerateAdaptiveCards),
+ };
+
+
+ functions.Add(function);
- });
conversationStarters.Add(new ConversationStarter
{
Text = !string.IsNullOrEmpty(summary) ? summary : description
@@ -555,7 +559,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);
@@ -652,7 +656,7 @@ rExtRaw is T rExt &&
return null;
}
- private static FunctionCapabilities? GetFunctionCapabilitiesFromOperation(OpenApiOperation openApiOperation)
+ private static FunctionCapabilities? GetFunctionCapabilitiesFromOperation(OpenApiOperation openApiOperation, bool shouldGenerateAdaptiveCards = false)
{
var capabilities = GetFunctionCapabilitiesFromCapabilitiesExtension(openApiOperation, OpenApiAiCapabilitiesExtension.Name);
if (capabilities != null)
@@ -668,6 +672,19 @@ rExtRaw is T rExt &&
ResponseSemantics = responseSemantics
};
}
+
+ if (shouldGenerateAdaptiveCards)
+ {
+ var generator = new AdaptiveCardGenerator();
+ var card = generator.GenerateAdaptiveCard(openApiOperation);
+ return new FunctionCapabilities
+ {
+ ResponseSemantics = new ResponseSemantics
+ {
+ StaticTemplate = JsonDocument.Parse(card.ToJson()).RootElement
+ }
+ };
+ }
return null;
}
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 eaa9bebf3d..fa206d0376 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.DeclarativeAgents.Manifest;
@@ -1129,4 +1131,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
}