Repository: microsoft/kiota (csharp)

Issue:
Define an adaptive card within the plugin manifest
### Is your feature request related to a problem? Please describe the problem.

The problem we're trying to solve is the user bringing existing adaptive cards, and wanting to use them in the plugin.


### Describe the solution you'd like

Kiota currently must define the adaptive card within the plugin manifest. There are plans to work with TTK to support an external file reference from within the manifest.

Requirements:

- TypeSpec will reference the external AC file.
- Kiota must either:
1. add the adaptive card JSON to plugin manifest 
2. reference in the external AC file

Kiota does not support passing AC file on CLI yet. Kiota must handle the path to the AC on the CLI and TypeSpec must pass it.

### Additional context

More details have been mentioned in https://github.com/microsoft/typespec-copilot-skills/issues/140

Pull request description:
Fixes https://github.com/microsoft/kiota/issues/6139

Patch:
diff --git a/src/Kiota.Builder/Extensions/OpenApiSettingsExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiSettingsExtensions.cs
index a8c984b28e..9fc6d469ee 100644
--- a/src/Kiota.Builder/Extensions/OpenApiSettingsExtensions.cs
+++ b/src/Kiota.Builder/Extensions/OpenApiSettingsExtensions.cs
@@ -20,6 +20,7 @@ public static void AddPluginsExtensions(this OpenApiReaderSettings settings)
         settings.ExtensionParsers.TryAdd(OpenApiAiReasoningInstructionsExtension.Name, static (i, _) => OpenApiAiReasoningInstructionsExtension.Parse(i));
         settings.ExtensionParsers.TryAdd(OpenApiAiRespondingInstructionsExtension.Name, static (i, _) => OpenApiAiRespondingInstructionsExtension.Parse(i));
         settings.ExtensionParsers.TryAdd(OpenApiAiAuthReferenceIdExtension.Name, static (i, _) => OpenApiAiAuthReferenceIdExtension.Parse(i));
+        settings.ExtensionParsers.TryAdd(OpenApiAiAdaptiveCardExtension.Name, static (i, _) => OpenApiAiAdaptiveCardExtension.Parse(i));
     }
 
     public static void AddGenerationExtensions(this OpenApiReaderSettings settings)
diff --git a/src/Kiota.Builder/OpenApiExtensions/OpenApiAiAdaptiveCardExtension.cs b/src/Kiota.Builder/OpenApiExtensions/OpenApiAiAdaptiveCardExtension.cs
new file mode 100644
index 0000000000..0fe538b364
--- /dev/null
+++ b/src/Kiota.Builder/OpenApiExtensions/OpenApiAiAdaptiveCardExtension.cs
@@ -0,0 +1,51 @@
+﻿using System;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Kiota.Builder.Extensions;
+using Microsoft.OpenApi;
+using Microsoft.OpenApi.Interfaces;
+namespace Kiota.Builder.OpenApiExtensions;
+using Microsoft.OpenApi.Writers;
+
+public class OpenApiAiAdaptiveCardExtension : IOpenApiExtension
+{
+    public static string Name => "x-ai-adaptive-card";
+    public string? DataPath
+    {
+        get; set;
+    }
+    public string? File
+    {
+        get; set;
+    }
+    public static OpenApiAiAdaptiveCardExtension Parse(JsonNode source)
+    {
+        if (source is not JsonObject rawObject) throw new ArgumentOutOfRangeException(nameof(source));
+        var extension = new OpenApiAiAdaptiveCardExtension();
+        if (rawObject.TryGetPropertyValue(nameof(DataPath).ToFirstCharacterLowerCase(), out var dataPath) && dataPath is JsonValue dataPathValue && dataPathValue.GetValueKind() is JsonValueKind.String && dataPathValue.TryGetValue<string>(out var dataPathStrValue))
+        {
+            extension.DataPath = dataPathStrValue;
+        }
+        if (rawObject.TryGetPropertyValue(nameof(File).ToFirstCharacterLowerCase(), out var file) && file is JsonValue fileValue && fileValue.GetValueKind() is JsonValueKind.String && fileValue.TryGetValue<string>(out var fileStrValue))
+        {
+            extension.File = fileStrValue;
+        }
+        if (string.IsNullOrEmpty(extension.DataPath) || string.IsNullOrEmpty(extension.File))
+            throw new ArgumentOutOfRangeException(nameof(source), "Both of the properties 'x-ai-adaptive-card.dataPath' and 'x-ai-adaptive-card.file' must be set.");
+        return extension;
+    }
+
+    public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion)
+    {
+        ArgumentNullException.ThrowIfNull(writer);
+        if (!string.IsNullOrEmpty(DataPath) && !string.IsNullOrEmpty(File))
+        {
+            writer.WriteStartObject();
+            writer.WritePropertyName(nameof(DataPath).ToFirstCharacterLowerCase());
+            writer.WriteValue(DataPath);
+            writer.WritePropertyName(nameof(File).ToFirstCharacterLowerCase());
+            writer.WriteValue(File);
+            writer.WriteEndObject();
+        }
+    }
+}
diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
index bc8ba71132..5b5aaa0181 100644
--- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
+++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
@@ -3,6 +3,7 @@
 using System.IO;
 using System.Linq;
 using System.Text.Json;
+using System.Text.Json.Nodes;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
@@ -494,6 +495,7 @@ private static (OpenApiRuntime[], Function[], ConversationStarter[]) GetRuntimes
                     Name = operation.OperationId!,
                     Description = !string.IsNullOrEmpty(description) ? description : summary,
                     States = GetStatesFromOperation(operation),
+                    Capabilities = GetFunctionCapabilitiesFromOperation(operation),
 
                 });
                 conversationStarters.Add(new ConversationStarter
@@ -590,4 +592,34 @@ rExtRaw is T rExt &&
 
         return null;
     }
+
+    private static FunctionCapabilities? GetFunctionCapabilitiesFromOperation(OpenApiOperation openApiOperation)
+    {
+        var responseSemantics = GetResponseSemanticsFromAdaptiveCardExtension(openApiOperation, OpenApiAiAdaptiveCardExtension.Name);
+        if (responseSemantics is null)
+            return null;
+        return new FunctionCapabilities
+        {
+            ResponseSemantics = responseSemantics,
+        };
+    }
+
+    private static ResponseSemantics? GetResponseSemanticsFromAdaptiveCardExtension(OpenApiOperation openApiOperation, string extensionName)
+    {
+        if (openApiOperation.Extensions is not null &&
+            openApiOperation.Extensions.TryGetValue(extensionName, out var adaptiveCardExtension) && adaptiveCardExtension is OpenApiAiAdaptiveCardExtension adaptiveCard)
+        {
+            JsonNode node = new JsonObject();
+            node["file"] = JsonValue.Create(adaptiveCard.File);
+            using JsonDocument doc = JsonDocument.Parse(node.ToJsonString());
+            JsonElement staticTemplate = doc.RootElement.Clone();
+            return new ResponseSemantics
+            {
+                DataPath = adaptiveCard.DataPath,
+                StaticTemplate = staticTemplate,
+            };
+        }
+
+        return null;
+    }
 }


Test patch:
diff --git a/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiAdaptiveCardExtensionTests.cs b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiAdaptiveCardExtensionTests.cs
new file mode 100644
index 0000000000..619fd0bf12
--- /dev/null
+++ b/tests/Kiota.Builder.Tests/OpenApiExtensions/OpenApiAiAdaptiveCardExtensionTests.cs
@@ -0,0 +1,133 @@
+﻿using System;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json.Nodes;
+using System.Threading.Tasks;
+using Kiota.Builder.Configuration;
+using Kiota.Builder.OpenApiExtensions;
+using Microsoft.Extensions.Logging;
+using Microsoft.OpenApi;
+using Microsoft.OpenApi.Writers;
+using Moq;
+using Xunit;
+
+namespace Kiota.Builder.Tests.OpenApiExtensions;
+public sealed class OpenApiAiAdaptiveCardExtensionTest : IDisposable
+{
+    private readonly HttpClient _httpClient = new();
+    public void Dispose()
+    {
+        _httpClient.Dispose();
+    }
+    [Fact]
+    public void Parses()
+    {
+        var oaiValueRepresentation =
+        """
+        {
+            "dataPath": "$.items",
+            "file": "path_to_file"
+        }
+        """;
+        using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(oaiValueRepresentation));
+        var oaiValue = JsonNode.Parse(stream);
+        var value = OpenApiAiAdaptiveCardExtension.Parse(oaiValue);
+        Assert.NotNull(value);
+        Assert.Equal("$.items", value.DataPath);
+        Assert.Equal("path_to_file", value.File);
+    }
+    private readonly string TempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+    [Fact]
+    public async Task ParsesInDocumentAsync()
+    {
+        var documentContent = @"openapi: 3.0.0
+info:
+  title: Graph Users
+  version: 0.0.0
+servers:
+  - url: https://graph.microsoft.com/v1.0
+    description: The Microsoft Graph API
+tags: []
+paths:
+  /users:
+    get:
+      operationId: getUsers
+      parameters: []
+      responses:
+        '200':
+          description: The request has succeeded.
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/User'
+      x-ai-adaptive-card:
+        dataPath: $.users
+        file: path_to_file
+  /users/{id}:
+    get:
+      operationId: getUser
+      parameters:
+        - name: id
+          in: path
+          required: true
+          description: The user id
+          schema:
+            type: string
+      responses:
+        '200':
+          description: The request has succeeded.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/User'
+      x-ai-adaptive-card:
+        dataPath: $.user
+        file: path_to_file
+components:
+  schemas:
+    User:
+      type: object
+      required:
+        - id
+        - displayName
+      properties:
+        id:
+          type: string
+        displayName:
+          type: string";
+        Directory.CreateDirectory(TempDirectory);
+        var documentPath = Path.Combine(TempDirectory, "document.yaml");
+        await File.WriteAllTextAsync(documentPath, documentContent);
+        var mockLogger = new Mock<ILogger<OpenApiAiAdaptiveCardExtension>>();
+        var documentDownloadService = new OpenApiDocumentDownloadService(_httpClient, mockLogger.Object);
+        var generationConfig = new GenerationConfiguration { OutputPath = TempDirectory, PluginTypes = [PluginType.APIPlugin] };
+        var (openApiDocumentStream, _) = await documentDownloadService.LoadStreamAsync(documentPath, generationConfig);
+        var document = await documentDownloadService.GetDocumentFromStreamAsync(openApiDocumentStream, generationConfig);
+        Assert.NotNull(document);
+        Assert.NotNull(document.Paths);
+        Assert.NotNull(document.Paths["/users"].Operations.FirstOrDefault().Value.Extensions);
+        Assert.True(document.Paths["/users"].Operations.FirstOrDefault().Value.Extensions.TryGetValue(OpenApiAiAdaptiveCardExtension.Name, out var adaptiveCardExtension));
+        Assert.NotNull(adaptiveCardExtension);
+    }
+
+    [Fact]
+    public void Serializes()
+    {
+        var value = new OpenApiAiAdaptiveCardExtension
+        {
+            DataPath = "$.items",
+            File = "path_to_file"
+        };
+        using var sWriter = new StringWriter();
+        OpenApiJsonWriter writer = new(sWriter, new OpenApiJsonWriterSettings { Terse = true });
+
+
+        value.Write(writer, OpenApiSpecVersion.OpenApi3_0);
+        var result = sWriter.ToString();
+        Assert.Equal("{\"dataPath\":\"$.items\",\"file\":\"path_to_file\"}", result);
+    }
+}
diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
index 8a435ec803..227a7856a4 100644
--- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
+++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
@@ -304,6 +304,118 @@ public async Task GeneratesManifestAndCleansUpInputDescriptionAsync()
         Assert.DoesNotContain("500", resultDocument.Paths["/test/{id}"].Operations[OperationType.Get].Responses.Keys, StringComparer.OrdinalIgnoreCase); // We removed the error response
     }
 
+    [Fact]
+    public async Task GeneratesManifestWithAdaptiveCardExtensionAsync()
+    {
+        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
+      x-ai-adaptive-card:
+        dataPath: $.test
+        file: path_to_file
+      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.NotNull(resultingManifest.Document.Functions[0].Capabilities);
+        Assert.Equal("$.test", resultingManifest.Document.Functions[0].Capabilities.ResponseSemantics.DataPath);
+        string jsonString = "{\"file\":\"path_to_file\"}";
+        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
+    }
+
+
     #region Security
 
     public static TheoryData<string, string, string, PluginAuthConfiguration, Action<DocumentValidationResults<PluginManifestDocument>>>