Repository: microsoft/kiota (csharp)

Issue:
Generate adaptive cards when creating API Plugins
## Current Challenge
Today, the Teams Toolkit (TTK) VS Code extension generates adaptive cards when scaffolding the project. However, with the integration between Kiota and TTK to generate API plugins, we are rethinking on the experience. The current adaptive card generation is based on the response body, but TTK/Kiota don't have a good way to identify what information should be used in the adaptive card, since this will depend on the functionality/project the developer is building.
When speaking to CAPE team, they said that having an adaptive card in the plugin was helpful for developers as an informational thing, so they could know an adaptive card could be added but that the generated adaptive card itself wasn't too helpful.

## Proposal
As part of the plugin generation, we should generate a sample adaptive card when the x-ai-adaptive-card extension is not present. 
The sample adaptive card should not use data and it must be a template that can later be updated by the user.
The sample should be created in a separate file called `<name_of_the_function>.json` file and Kiota should create the plugin and point to the `external file`.


## Description
Adaptive cards information are generated for each function that returns a complex data structure under the `response_semantics` in the API Plugin. The response_semantics property instructs the agent how it should display data it receives from the API. There are 2 required parameters when adding an adaptive card, data_path, and static_template. 

Here is an example of the desired structure Kiota should create when there is no x-ai-adaptive-card extension:

### <name_of_the_function>.json / search_issues_and_pull_requests.json
```jsonc
{
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "type": "AdaptiveCard",
    "version": "1.0",
    "body": [
        {
            "type": "Container",
            "items": [
                {
                    "type": "TextBlock",
                    "text": "This is your adaptive card template",
                    "weight": "bolder",
                    "size": "medium"
                },
                {
                    "type": "ColumnSet",
                    "columns": [
                        {
                            "type": "Column",
                            "width": "auto",
                            "items": [
                                {
                                    "type": "Image",
                                    "url": "https://github.com/microsoft/kiota/blob/main/vscode/microsoft-kiota/images/logo.png?raw=true",
                                    "altText": "Kiota logo",
                                    "size": "medium",
                                    "style": "person"
                                }
                            ]
                        },
                        {
                            "type": "Column",
                            "width": "auto",
                            "items": [
                                {
                                    "type": "TextBlock",
                                    "text": "Adaptive Card",
                                    "weight": "bolder",
                                    "wrap": true
                                }
                            ]
                        }
                    ]
                }
            ]
        },
        {
            "type": "Container",
            "items": [
                {
                    "type": "TextBlock",
                    "text": "Now that we have defined the adaptive card template, one can go to the plugin manifest file and edit it to create a card that displays the relevant information for their users.",
                    "wrap": true
                }
            ]
        },
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "width": "auto",
                    "items": [
                        {
                            "type": "TextBlock",
                            "horizontalAlignment": "center",
                            "text": "Learn about [Adaptive Cards](https://adaptivecards.io/)",
                            "wrap": true
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": "auto",
                    "items": [
                        {
                            "type": "TextBlock",
                            "horizontalAlignment": "center",
                            "text": "Learn about [API Plugin for Microsoft 365 Copilot](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/overview-api-plugins)",
                            "wrap": true
                        }
                    ]
                }
            ]
        }
    ]
}
```

### Plugin manifest
```jsonc
"functions": [
    {
        "name": "search_issues_and_pull_requests",
        "description": "Find issues by state and keyword. This method returns up to 100 results [per page](https://docs.gith",
        "capabilities": {
            "response_semantics": {
                "data_path": "$",
                "static_template": {
                    "file": "./search_issues_and_pull_requests.json"
                }
            }
        }
    }
]
```

Preview of the current adaptive card template:

![Image](https://github.com/user-attachments/assets/861a8682-74de-44ae-a157-a042485fee3d)

## Acceptance Criteria:

1. If x-ai-adaptive-card is present, Kiota should **not** create the adaptive card file but use the OpenAPI extension to generate the right plugin instead (see https://github.com/microsoft/kiota/issues/6139 for more context)
2. For API endpoints that return a JSON schema, Kiota should add an adaptive card as the template above.
3. For API endpoints that don't return a JSON schema, Kiota should still generate the function but with no `response_semantics`.
4. Every function with `response_semantics` must include `data_path`, and the `static_template`.
5. When regenerating, Kiota should always look for x-ai-adaptive-card and act accordingly when the extension is present in the OpenAPI description.
6. When regenerating, Kiota should warn the user that any content changed in the generated files will be overridden, including plugin manifest.

Pull request:
Closes #5674

Patch:
diff --git a/src/Kiota.Builder/Kiota.Builder.csproj b/src/Kiota.Builder/Kiota.Builder.csproj
index 1ff6766af6..0a70f5ab12 100644
--- a/src/Kiota.Builder/Kiota.Builder.csproj
+++ b/src/Kiota.Builder/Kiota.Builder.csproj
@@ -57,4 +57,7 @@
   <ItemGroup>
     <None Include="../../README.md" Pack="true" PackagePath="" />
   </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="Resources\AdaptiveCardTemplate.json" />
+  </ItemGroup>
 </Project>
diff --git a/src/Kiota.Builder/Plugins/AdaptiveCardTemplate.cs b/src/Kiota.Builder/Plugins/AdaptiveCardTemplate.cs
new file mode 100644
index 0000000000..4343d0b53b
--- /dev/null
+++ b/src/Kiota.Builder/Plugins/AdaptiveCardTemplate.cs
@@ -0,0 +1,53 @@
+﻿using System.IO;
+using Microsoft.Extensions.Logging;
+
+namespace Kiota.Builder.Plugins
+{
+
+    internal class AdaptiveCardTemplate
+    {
+        private readonly ILogger<KiotaBuilder> Logger;
+        private readonly string? AdaptiveCard;
+
+        public AdaptiveCardTemplate(ILogger<KiotaBuilder> logger)
+        {
+            Logger = logger;
+            AdaptiveCard = LoadEmbeddedResource("Kiota.Builder.Resources.AdaptiveCardTemplate.json");
+        }
+
+        private string? LoadEmbeddedResource(string resourceName)
+        {
+            var assembly = typeof(AdaptiveCardTemplate).Assembly;
+            using var stream = assembly.GetManifestResourceStream(resourceName);
+            if (stream != null)
+            {
+                using var reader = new StreamReader(stream);
+                return reader.ReadToEnd();
+            }
+            Logger.LogCritical("Failed to load embedded resource: {ResourceName}", resourceName);
+            return null;
+        }
+
+        public void Write(string filePath)
+        {
+            try
+            {
+                if (AdaptiveCard is null)
+                {
+                    throw new IOException("Failed to load the adaptive card from the embedded resource");
+                }
+
+                string? directoryPath = Path.GetDirectoryName(filePath);
+                if (!string.IsNullOrEmpty(directoryPath))
+                {
+                    Directory.CreateDirectory(directoryPath);
+                }
+                File.WriteAllText(filePath, AdaptiveCard);
+            }
+            catch (IOException e)
+            {
+                throw new IOException($"Failed to write to file {filePath}", e);
+            }
+        }
+    }
+}
diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
index 91a187bf3a..2d1bec87e3 100644
--- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
+++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
@@ -417,7 +417,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 +499,13 @@ 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<KiotaBuilder> logger)
     {
         var runtimes = new List<OpenApiRuntime>();
         var functions = new List<Function>();
         var conversationStarters = new List<ConversationStarter>();
-        var configAuth = authInformation?.ToPluginManifestAuth();
+        var configAuth = configuration.PluginAuthInformation?.ToPluginManifestAuth();
         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)))
@@ -542,7 +542,7 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes
                     Name = operation.OperationId!,
                     Description = !string.IsNullOrEmpty(description) ? description : summary,
                     States = GetStatesFromOperation(operation),
-                    Capabilities = GetFunctionCapabilitiesFromOperation(operation),
+                    Capabilities = GetFunctionCapabilitiesFromOperation(operation, configuration, logger),
 
                 });
                 conversationStarters.Add(new ConversationStarter
@@ -555,7 +555,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);
@@ -653,7 +653,7 @@ rExtRaw is T rExt &&
         return null;
     }
 
-    private static FunctionCapabilities? GetFunctionCapabilitiesFromOperation(OpenApiOperation openApiOperation)
+    private static FunctionCapabilities? GetFunctionCapabilitiesFromOperation(OpenApiOperation openApiOperation, GenerationConfiguration configuration, ILogger<KiotaBuilder> logger)
     {
         var capabilities = GetFunctionCapabilitiesFromCapabilitiesExtension(openApiOperation, OpenApiAiCapabilitiesExtension.Name);
         if (capabilities != null)
@@ -661,15 +661,15 @@ rExtRaw is T rExt &&
             return capabilities;
         }
 
-        var responseSemantics = GetResponseSemanticsFromAdaptiveCardExtension(openApiOperation, OpenApiAiAdaptiveCardExtension.Name);
-        if (responseSemantics != null)
+
+        var responseSemantics = GetResponseSemanticsFromAdaptiveCardExtension(openApiOperation, OpenApiAiAdaptiveCardExtension.Name) ??
+            GetResponseSemanticsFromTemplate(openApiOperation, configuration, logger);
+
+        return new FunctionCapabilities
         {
-            return new FunctionCapabilities
-            {
-                ResponseSemantics = responseSemantics
-            };
-        }
-        return null;
+            ResponseSemantics = responseSemantics
+        };
+
     }
 
     private static FunctionCapabilities? GetFunctionCapabilitiesFromCapabilitiesExtension(OpenApiOperation openApiOperation, string extensionName)
@@ -753,4 +753,45 @@ rExtRaw is T rExt &&
 
         return null;
     }
+
+    private static ResponseSemantics? GetResponseSemanticsFromTemplate(OpenApiOperation openApiOperation, GenerationConfiguration configuration, ILogger<KiotaBuilder> logger)
+    {
+        if (openApiOperation.Responses is null
+            || openApiOperation.Responses.Count == 0
+            || openApiOperation.OperationId is null
+            || !openApiOperation.Responses.TryGetValue("200", out var response)
+            || response is null
+            || response.Content is null
+            || response.Content.Count == 0
+            || response.Content["application/json"]?.Schema is null)
+        {
+            return null;
+        }
+
+        string functionName = openApiOperation.OperationId;
+        string fileName = $"{functionName}.json";
+        string staticTemplateJson = $"{{\"file\": \"./adaptiveCards/{fileName}\"}}";
+        try
+        {
+            WriteAdaptiveCardTemplate(configuration, fileName, logger);
+            using JsonDocument doc = JsonDocument.Parse(staticTemplateJson);
+            JsonElement staticTemplate = doc.RootElement.Clone();
+            return new ResponseSemantics()
+            {
+                DataPath = "$",
+                StaticTemplate = staticTemplate
+            };
+        }
+        catch (IOException)
+        {
+
+            return null;
+        }
+    }
+
+    private static void WriteAdaptiveCardTemplate(GenerationConfiguration configuration, string fileName, ILogger<KiotaBuilder> logger)
+    {
+        var adaptiveCardOutputPath = Path.Combine(configuration.OutputPath, "adaptiveCards", fileName);
+        new AdaptiveCardTemplate(logger).Write(adaptiveCardOutputPath);
+    }
 }
diff --git a/src/Kiota.Builder/Resources/AdaptiveCardTemplate.json b/src/Kiota.Builder/Resources/AdaptiveCardTemplate.json
new file mode 100644
index 0000000000..3c39d83b76
--- /dev/null
+++ b/src/Kiota.Builder/Resources/AdaptiveCardTemplate.json
@@ -0,0 +1,87 @@
+{
+  "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
+  "type": "AdaptiveCard",
+  "version": "1.0",
+  "body": [
+    {
+      "type": "Container",
+      "items": [
+        {
+          "type": "TextBlock",
+          "text": "This is your adaptive card template",
+          "weight": "bolder",
+          "size": "medium"
+        },
+        {
+          "type": "ColumnSet",
+          "columns": [
+            {
+              "type": "Column",
+              "width": "auto",
+              "items": [
+                {
+                  "type": "Image",
+                  "url": "https://github.com/microsoft/kiota/blob/main/vscode/microsoft-kiota/images/logo.png?raw=true",
+                  "altText": "Kiota logo",
+                  "size": "medium",
+                  "style": "person"
+                }
+              ]
+            },
+            {
+              "type": "Column",
+              "width": "auto",
+              "items": [
+                {
+                  "type": "TextBlock",
+                  "text": "Adaptive Card",
+                  "weight": "bolder",
+                  "wrap": true
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "type": "Container",
+      "items": [
+        {
+          "type": "TextBlock",
+          "text": "Now that we have defined the adaptive card template, one can go to the plugin manifest file and edit it to create a card that displays the relevant information for their users.",
+          "wrap": true
+        }
+      ]
+    },
+    {
+      "type": "ColumnSet",
+      "columns": [
+        {
+          "type": "Column",
+          "width": "auto",
+          "items": [
+            {
+              "type": "TextBlock",
+              "horizontalAlignment": "center",
+              "text": "Learn about [Adaptive Cards](https://adaptivecards.io/)",
+              "wrap": true
+            }
+          ]
+        },
+        {
+          "type": "Column",
+          "width": "auto",
+          "items": [
+            {
+              "type": "TextBlock",
+              "horizontalAlignment": "center",
+              "text": "Learn about [API Plugin for Microsoft 365 Copilot](https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/overview-api-plugins)",
+              "wrap": true
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}


Test patch:
diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
index a2aa8d32d0..4bcaa4c285 100644
--- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
+++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
@@ -344,10 +344,6 @@ public async Task GeneratesManifestWithAdaptiveCardExtensionAsync()
       responses:
         '200':
           description:
-          content:
-            application/json:
-              schema:
-                $ref: '#/components/schemas/microsoft.graph.message'
         '500':
           description: api error response
 components:
@@ -412,7 +408,120 @@ public async Task GeneratesManifestWithAdaptiveCardExtensionAsync()
         using JsonDocument doc = JsonDocument.Parse(jsonString);
         JsonElement staticTemplate = doc.RootElement.Clone();
         Assert.Equal(staticTemplate.ToString(), resultingManifest.Document.Functions[0].Capabilities.ResponseSemantics.StaticTemplate.ToString());
-        Assert.Null(resultingManifest.Document.Functions[1].Capabilities);// no function capabilities is added if no adaptive card
+        Assert.Null(resultingManifest.Document.Functions[1].Capabilities.ResponseSemantics);// no response semantics is added if no adaptive card
+    }
+
+
+    [Fact]
+    public async Task GeneratesManifestWithAdaptiveCardWithoutExtensionAsync()
+    {
+        var simpleDescriptionContent = @"openapi: 3.0.0
+info:
+  title: test
+  version: 1.0
+servers:
+  - url: http://localhost/
+    description: There's no place like home
+paths:
+  /test:
+    get:
+      description: description for test path
+      externalDocs:
+        description: external docs for test path
+        url: http://localhost/test
+      responses:
+        '200':
+          description: test
+        '400':
+          description: client error response
+  /test/{id}:
+    get:
+      summary: 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:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/microsoft.graph.message'
+        '500':
+          description: api error response
+components:
+  schemas:
+    microsoft.graph.entity:
+      title: entity
+      required:
+        - '@odata.type'
+      type: object
+      properties:
+        id:
+          anyOf:
+          - type: string
+          - type: integer
+        '@odata.type':
+          type: string
+    microsoft.graph.message:
+      allOf:
+      - $ref: '#/components/schemas/microsoft.graph.entity'
+      - type: object
+        title: message
+        properties:
+          subject:
+            type: string
+          body:
+            type: string";
+
+        var workingDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+        var simpleDescriptionPath = Path.Combine(workingDirectory) + "description.yaml";
+        await File.WriteAllTextAsync(simpleDescriptionPath, simpleDescriptionContent);
+        var openAPIDocumentDS = new OpenApiDocumentDownloadService(_httpClient, _logger);
+        var outputDirectory = Path.Combine(workingDirectory, "output");
+        var generationConfiguration = new GenerationConfiguration
+        {
+            OutputPath = outputDirectory,
+            OpenAPIFilePath = simpleDescriptionPath,
+            PluginTypes = [PluginType.APIPlugin],
+            ClientClassName = "client",
+            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, ManifestFileName)));
+        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<OpenApiRuntime>().First().Spec.Url);
+        Assert.Equal(2, resultingManifest.Document.Functions.Count);// all functions are generated despite missing operationIds
+        Assert.Null(resultingManifest.Document.Functions[0].Capabilities.ResponseSemantics); // no response semantics is added if no schema
+        Assert.NotNull(resultingManifest.Document.Functions[1].Capabilities.ResponseSemantics); // response semantics is added if response has schema
+        string jsonString = $"{{\"file\": \"./adaptiveCards/{resultingManifest.Document.Functions[1].Name}.json\"}}";
+        using JsonDocument doc = JsonDocument.Parse(jsonString);
+        JsonElement staticTemplate = doc.RootElement.Clone();
+        Assert.Equal(staticTemplate.ToString(), resultingManifest.Document.Functions[1].Capabilities.ResponseSemantics.StaticTemplate.ToString()); // adaptive card present
+
+        // validate presence of adaptive card
+        var path = Path.Combine(outputDirectory, "adaptiveCards", $"{resultingManifest.Document.Functions[1].Name}.json");
+        Assert.True(File.Exists(path));
+
     }
 
 
@@ -881,8 +990,8 @@ public async Task GeneratesManifestWithAiCapabilitiesExtensionAsync()
         Assert.Contains("sensitiveData", resultingManifest.Document.Functions[0].Capabilities.SecurityInfo.DataHandling);
         Assert.Contains("personalData", resultingManifest.Document.Functions[0].Capabilities.SecurityInfo.DataHandling);
 
-        // Second function has no capabilities
-        Assert.Null(resultingManifest.Document.Functions[1].Capabilities);
+        // Second function has no response semantics
+        Assert.Null(resultingManifest.Document.Functions[1].Capabilities.ResponseSemantics);
     }
 
     [Fact]