diff --git a/dotnet/samples/Concepts/Memory/OpenAI_EmbeddingGeneration.cs b/dotnet/samples/Concepts/Memory/OpenAI_EmbeddingGeneration.cs new file mode 100644 index 000000000000..93ddda59d614 --- /dev/null +++ b/dotnet/samples/Concepts/Memory/OpenAI_EmbeddingGeneration.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Embeddings; +using xRetry; + +#pragma warning disable format // Format item can be simplified +#pragma warning disable CA1861 // Avoid constant arrays as arguments + +namespace Memory; + +// The following example shows how to use Semantic Kernel with OpenAI. +public class OpenAI_EmbeddingGeneration(ITestOutputHelper output) : BaseTest(output) +{ + [RetryFact(typeof(HttpOperationException))] + public async Task RunEmbeddingAsync() + { + Assert.NotNull(TestConfiguration.OpenAI.EmbeddingModelId); + Assert.NotNull(TestConfiguration.OpenAI.ApiKey); + + IKernelBuilder kernelBuilder = Kernel.CreateBuilder(); + kernelBuilder.AddOpenAITextEmbeddingGeneration( + modelId: TestConfiguration.OpenAI.EmbeddingModelId!, + apiKey: TestConfiguration.OpenAI.ApiKey!); + Kernel kernel = kernelBuilder.Build(); + + var embeddingGenerator = kernel.GetRequiredService(); + + // Generate embeddings for the specified text. + var embeddings = await embeddingGenerator.GenerateEmbeddingsAsync(["Semantic Kernel is a lightweight, open-source development kit that lets you easily build AI agents and integrate the latest AI models into your C#, Python, or Java codebase."]); + + Console.WriteLine($"Generated {embeddings.Count} embeddings for the provided text"); + } +} diff --git a/dotnet/samples/Concepts/README.md b/dotnet/samples/Concepts/README.md index d53367f532d0..6b0f28b329ca 100644 --- a/dotnet/samples/Concepts/README.md +++ b/dotnet/samples/Concepts/README.md @@ -123,6 +123,7 @@ dotnet test -l "console;verbosity=detailed" --filter "FullyQualifiedName=ChatCom ### Memory - Using AI [`Memory`](https://github.com/microsoft/semantic-kernel/tree/main/dotnet/src/SemanticKernel.Abstractions/Memory) concepts +- [OpenAI_EmbeddingGeneration](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Memory/OpenAI_EmbeddingGeneration.cs) - [Ollama_EmbeddingGeneration](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Memory/Ollama_EmbeddingGeneration.cs) - [Onnx_EmbeddingGeneration](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Memory/Onnx_EmbeddingGeneration.cs) - [HuggingFace_EmbeddingGeneration](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/Concepts/Memory/HuggingFace_EmbeddingGeneration.cs) diff --git a/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs b/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs index 964c55828337..67ba2d34e79a 100644 --- a/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs +++ b/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs @@ -430,7 +430,7 @@ private static List CreateRestApiOperationParameters(string op return null; } - var mediaType = s_supportedMediaTypes.FirstOrDefault(requestBody.Content.ContainsKey) ?? throw new KernelException($"Neither of the media types of {operationId} is supported."); + var mediaType = GetMediaType(requestBody.Content) ?? throw new KernelException($"Neither of the media types of {operationId} is supported."); var mediaTypeMetadata = requestBody.Content[mediaType]; var payloadProperties = GetPayloadProperties(operationId, mediaTypeMetadata.Schema); @@ -438,11 +438,40 @@ private static List CreateRestApiOperationParameters(string op return new RestApiPayload(mediaType, payloadProperties, requestBody.Description, mediaTypeMetadata?.Schema?.ToJsonSchema()); } + /// + /// Returns the first supported media type. If none of the media types are supported, an exception is thrown. + /// + /// + /// Handles the case when the media type contains additional parameters e.g. application/json; x-api-version=2.0. + /// + /// The OpenAPI request body content. + /// The first support ed media type. + /// + private static string? GetMediaType(IDictionary content) + { + foreach (var mediaType in s_supportedMediaTypes) + { + foreach (var key in content.Keys) + { + var keyParts = key.Split(';'); + if (keyParts[0].Equals(mediaType, StringComparison.OrdinalIgnoreCase)) + { + return key; + } + } + } + return null; + } + + /// + /// Create collection of expected responses for the REST API operation for the supported media types. + /// + /// Responses from the OpenAPI endpoint. private static IEnumerable<(string, RestApiExpectedResponse)> CreateRestApiOperationExpectedResponses(OpenApiResponses responses) { foreach (var response in responses) { - var mediaType = s_supportedMediaTypes.FirstOrDefault(response.Value.Content.ContainsKey); + var mediaType = GetMediaType(response.Value.Content); if (mediaType is not null) { var matchingSchema = response.Value.Content[mediaType].Schema; diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserExtensionsTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserExtensionsTests.cs index 0dde27b47306..88cb52d183e6 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserExtensionsTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserExtensionsTests.cs @@ -77,4 +77,23 @@ public async Task ItCanExtractExtensionsOfAllTypesAsync(string documentName) Assert.True(operation.Extensions.TryGetValue("x-object-extension", out var objectValue)); Assert.Equal("{\"key1\":\"value1\",\"key2\":\"value2\"}", objectValue); } + + [Theory] + [InlineData("documentV3_0.json")] + [InlineData("documentV3_1.yaml")] + public async Task ItCanParseMediaTypeAsync(string documentName) + { + // Arrange. + using var openApiDocument = ResourcePluginsProvider.LoadFromResource(documentName); + + // Act. + var restApi = await this._sut.ParseAsync(openApiDocument); + + // Assert. + Assert.NotNull(restApi.Operations); + Assert.Equal(7, restApi.Operations.Count); + var operation = restApi.Operations.Single(o => o.Id == "Joke"); + Assert.NotNull(operation); + Assert.Equal("application/json; x-api-version=2.0", operation.Payload?.MediaType); + } } diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs index 6a00410e24e6..8728771ac54a 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs @@ -236,7 +236,7 @@ public async Task ItCanExtractAllPathsAsOperationsAsync() var restApi = await this._sut.ParseAsync(this._openApiDocument); // Assert - Assert.Equal(6, restApi.Operations.Count); + Assert.Equal(7, restApi.Operations.Count); } [Fact] diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs index 79f3d8330694..6455b95dd34b 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs @@ -236,7 +236,7 @@ public async Task ItCanExtractAllPathsAsOperationsAsync() var restApi = await this._sut.ParseAsync(this._openApiDocument); // Assert - Assert.Equal(6, restApi.Operations.Count); + Assert.Equal(7, restApi.Operations.Count); } [Fact] diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs index f05eab224b5e..2242f5032610 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs @@ -353,7 +353,7 @@ public async Task ItShouldHandleEmptyOperationNameAsync() var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("fakePlugin", content, this._executionParameters); // Assert - Assert.Equal(6, plugin.Count()); + Assert.Equal(7, plugin.Count()); Assert.True(plugin.TryGetFunction("GetSecretsSecretname", out var _)); } @@ -372,7 +372,7 @@ public async Task ItShouldHandleNullOperationNameAsync() var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("fakePlugin", content, this._executionParameters); // Assert - Assert.Equal(6, plugin.Count()); + Assert.Equal(7, plugin.Count()); Assert.True(plugin.TryGetFunction("GetSecretsSecretname", out var _)); } diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_0.json b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_0.json index d92e8b50dde3..a2990fb86f90 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_0.json +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_0.json @@ -212,6 +212,63 @@ } } }, + "/FunPlugin/Joke": { + "post": { + "summary": "Generate a funny joke", + "operationId": "Joke", + "requestBody": { + "description": "Joke subject", + "content": { + "text/plain; x-api-version=2.0": { + "schema": { + "type": "string" + } + }, + "application/json; x-api-version=2.0": { + "schema": { + "required": [ + "scenario" + ], + "type": "object", + "properties": { + "scenario": { + "type": "string", + "description": "Joke subject" + } + } + } + } + }, + "x-bodyName": "body" + }, + "responses": { + "200": { + "description": "The OK response", + "content": { + "text/plain; x-api-version=2.0": { + "schema": { + "type": "string" + } + }, + "application/json; x-api-version=2.0": { + "schema": { + "required": [ + "scenario" + ], + "type": "object", + "properties": { + "scenario": { + "type": "string", + "description": "Joke subject" + } + } + } + } + } + } + } + } + }, "/test-default-values/{string-parameter}": { "put": { "summary": "Operation to test default parameter values.", diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_1.yaml b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_1.yaml index 54005930d0d0..8c250db741cb 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_1.yaml +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_1.yaml @@ -138,6 +138,38 @@ paths: text/plain: schema: type: string + /FunPlugin/Joke: + post: + summary: Gneerate a funny joke + operationId: Joke + requestBody: + description: Joke subject + content: + application/json; x-api-version=2.0: + schema: + type: object + properties: + scenario: + type: string + description: Joke subject + text/plain; x-api-version=2.0: + schema: + type: string + x-bodyName: body + responses: + '200': + description: The OK response + content: + text/plain; x-api-version=2.0: + schema: + type: string + application/json; x-api-version=2.0: + schema: + type: object + properties: + scenario: + type: string + description: Joke subject '/test-default-values/{string-parameter}': put: summary: Operation to test default parameter values.