Skip to content

Commit

Permalink
.Net: Fix OpenApiDocumentParser fail when X-API-version is provide in…
Browse files Browse the repository at this point in the history
… the content key (#9959)

### Motivation and Context

1. Closes #9952
2. Also includes an OpenAI text embedding sample

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [ ] The code builds clean without any errors or warnings
- [ ] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone 😄

---------

Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
  • Loading branch information
markwallace-microsoft and SergeyMenshykh authored Dec 13, 2024
1 parent e66883e commit e883843
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 6 deletions.
34 changes: 34 additions & 0 deletions dotnet/samples/Concepts/Memory/OpenAI_EmbeddingGeneration.cs
Original file line number Diff line number Diff line change
@@ -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<ITextEmbeddingGenerationService>();

// 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");
}
}
1 change: 1 addition & 0 deletions dotnet/samples/Concepts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,19 +430,48 @@ private static List<RestApiParameter> 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);

return new RestApiPayload(mediaType, payloadProperties, requestBody.Description, mediaTypeMetadata?.Schema?.ToJsonSchema());
}

/// <summary>
/// Returns the first supported media type. If none of the media types are supported, an exception is thrown.
/// </summary>
/// <remarks>
/// Handles the case when the media type contains additional parameters e.g. application/json; x-api-version=2.0.
/// </remarks>
/// <param name="content">The OpenAPI request body content.</param>
/// <returns>The first support ed media type.</returns>
/// <exception cref="KernelException"></exception>
private static string? GetMediaType(IDictionary<string, OpenApiMediaType> 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;
}

/// <summary>
/// Create collection of expected responses for the REST API operation for the supported media types.
/// </summary>
/// <param name="responses">Responses from the OpenAPI endpoint.</param>
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 _));
}

Expand All @@ -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 _));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit e883843

Please sign in to comment.