Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.Net: Added support for Structured Outputs in prompts #9873

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,99 @@ public async Task StructuredOutputsWithAzureOpenAIAsync()
// ...and more...
}

/// <summary>
/// This method shows how to enable Structured Outputs feature with Semantic Kernel functions from prompt
/// using Semantic Kernel template engine.
/// In this scenario, JSON Schema for response is specified in a prompt configuration file.
/// </summary>
[Fact]
public async Task StructuredOutputsWithFunctionsFromPromptAsync()
{
// Initialize kernel.
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: "gpt-4o-2024-08-06",
apiKey: TestConfiguration.OpenAI.ApiKey)
.Build();

// Initialize a path to plugin directory: Resources/Plugins/MoviePlugins/MoviePluginPrompt.
var pluginDirectoryPath = Path.Combine(Directory.GetCurrentDirectory(), "Resources", "Plugins", "MoviePlugins", "MoviePluginPrompt");

// Create a function from prompt.
kernel.ImportPluginFromPromptDirectory(pluginDirectoryPath, pluginName: "MoviePlugin");

var result = await kernel.InvokeAsync("MoviePlugin", "TopMovies");

// Deserialize string response to a strong type to access type properties.
// At this point, the deserialization logic won't fail, because MovieResult type was specified as desired response format.
// This ensures that response string is a serialized version of MovieResult type.
var movieResult = JsonSerializer.Deserialize<MovieResult>(result.ToString())!;

// Output the result.
this.OutputResult(movieResult);

// Output:

// Title: The Lord of the Rings: The Fellowship of the Ring
// Director: Peter Jackson
// Release year: 2001
// Rating: 8.8
// Is available on streaming: True
// Tags: Adventure,Drama,Fantasy

// ...and more...
}

/// <summary>
/// This method shows how to enable Structured Outputs feature with Semantic Kernel functions from YAML
/// using Semantic Kernel template engine.
/// In this scenario, JSON Schema for response is specified in YAML prompt file.
/// </summary>
[Fact]
public async Task StructuredOutputsWithFunctionsFromYamlAsync()
{
// Initialize kernel.
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: "gpt-4o-2024-08-06",
apiKey: TestConfiguration.OpenAI.ApiKey)
.Build();

// Initialize a path to YAML function: Resources/Plugins/MoviePlugins/MoviePluginYaml.
var functionPath = Path.Combine(Directory.GetCurrentDirectory(), "Resources", "Plugins", "MoviePlugins", "MoviePluginYaml", "TopMovies.yaml");

// Load YAML prompt.
using Stream stream = File.OpenRead(functionPath);
using StreamReader reader = new(stream);

var topMoviesYaml = reader.ReadToEnd();
markwallace-microsoft marked this conversation as resolved.
Show resolved Hide resolved

// Import a function from YAML.
var function = kernel.CreateFunctionFromPromptYaml(topMoviesYaml);
kernel.ImportPluginFromFunctions("MoviePlugin", [function]);

var result = await kernel.InvokeAsync("MoviePlugin", "TopMovies");

// Deserialize string response to a strong type to access type properties.
// At this point, the deserialization logic won't fail, because MovieResult type was specified as desired response format.
// This ensures that response string is a serialized version of MovieResult type.
var movieResult = JsonSerializer.Deserialize<MovieResult>(result.ToString())!;

// Output the result.
this.OutputResult(movieResult);

// Output:

// Title: The Lord of the Rings: The Fellowship of the Ring
// Director: Peter Jackson
// Release year: 2001
// Rating: 8.8
// Is available on streaming: True
// Tags: Adventure,Drama,Fantasy

// ...and more...
}

#region private

/// <summary>Movie result struct that will be used as desired chat completion response format (structured output).</summary>
Expand Down
9 changes: 9 additions & 0 deletions dotnet/samples/Concepts/Concepts.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@
<None Remove="Resources\Plugins\PetsPlugin\oneOfV3.json" />
</ItemGroup>
<ItemGroup>
<None Update="Resources\Plugins\MoviePlugins\MoviePluginPrompt\TopMovies\config.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Resources\Plugins\MoviePlugins\MoviePluginPrompt\TopMovies\skprompt.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Resources\Plugins\MoviePlugins\MoviePluginYaml\TopMovies.yaml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Resources\Plugins\ProductsPlugin\openapi.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"schema": 1,
"type": "completion",
"description": "Provides information about movies to the user",
"execution_settings": {
"default": {
"max_tokens": 1000,
"temperature": 0,
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "movie_result",
"strict": true,
"schema": {
"type": "object",
"properties": {
"Movies": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Title": { "type": "string" },
"Director": { "type": "string" },
"ReleaseYear": { "type": "integer" },
"Rating": { "type": "number" },
"IsAvailableOnStreaming": { "type": "boolean" },
"Tags": {
"type": "array",
"items": { "type": "string" }
}
},
"required": [ "Title", "Director", "ReleaseYear", "Rating", "IsAvailableOnStreaming", "Tags" ],
"additionalProperties": false
}
}
},
"required": [ "Movies" ],
"additionalProperties": false
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
What are the top 10 movies of all time?
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: TopMovies
template: |
What are the top 10 movies of all time?
template_format: semantic-kernel
description: Provides information about movies to the user.
execution_settings:
default:
max_tokens: 1000
temperature: 0
response_format:
type: json_schema
json_schema:
name: movie_result
strict: !!bool true
schema:
type: object
properties:
Movies:
type: array
items:
type: object
properties:
Title:
type: string
Director:
type: string
ReleaseYear:
type: integer
Rating:
type: number
IsAvailableOnStreaming:
type: boolean
Tags:
type: array
items:
type: string
required: [Title, Director, ReleaseYear, Rating, IsAvailableOnStreaming, Tags]
additionalProperties: !!bool false
required: [Movies]
additionalProperties: !!bool false
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using OpenAI.Chat;
using Xunit;

namespace SemanticKernel.Connectors.OpenAI.UnitTests.Helpers;

/// <summary>
/// Unit tests for <see cref="OpenAIChatResponseFormatHelper"/> class.
/// </summary>
public sealed class OpenAIChatResponseFormatHelperTests
{
private readonly JsonSerializerOptions _options = new();

public OpenAIChatResponseFormatHelperTests()
{
this._options.Converters.Add(new BinaryDataJsonConverter());
}

[Theory]
[MemberData(nameof(ChatResponseFormatJson))]
public void GetJsonSchemaResponseFormatReturnsChatResponseFormatByDefault(
string chatResponseFormatJson,
string expectedSchemaName,
bool? expectedStrict)
{
// Arrange
var jsonDocument = JsonDocument.Parse(chatResponseFormatJson);
var jsonElement = jsonDocument.RootElement;

// Act
var chatResponseFormat = OpenAIChatResponseFormatHelper.GetJsonSchemaResponseFormat(jsonElement);
var responseFormat = this.GetResponseFormat(chatResponseFormat);

// Assert
Assert.True(responseFormat.TryGetProperty("JsonSchema", out var jsonSchema));
Assert.True(jsonSchema.TryGetProperty("Schema", out var schema));
Assert.True(jsonSchema.TryGetProperty("Name", out var name));
Assert.True(jsonSchema.TryGetProperty("Strict", out var strict));

Assert.Equal(expectedSchemaName, name.GetString());

if (expectedStrict is null)
{
Assert.Equal(JsonValueKind.Null, strict.ValueKind);
}
else
{
Assert.Equal(expectedStrict, strict.GetBoolean());
}

var schemaElement = JsonDocument.Parse(schema.ToString()).RootElement;
var nameProperty = schemaElement.GetProperty("properties").GetProperty("name");

Assert.Equal("object", schemaElement.GetProperty("type").GetString());
Assert.Equal("string", nameProperty.GetProperty("type").GetString());
Assert.Equal("The person's full name", nameProperty.GetProperty("description").GetString());
}

[Fact]
public void GetJsonSchemaResponseFormatThrowsExceptionWhenSchemaDoesNotExist()
{
// Arrange
var json =
"""
{
"type": "json_schema",
"json_schema": {
"name": "Schema Name"
}
}
""";

var jsonDocument = JsonDocument.Parse(json);
var jsonElement = jsonDocument.RootElement;

// Act & Assert
Assert.Throws<ArgumentException>(() => OpenAIChatResponseFormatHelper.GetJsonSchemaResponseFormat(jsonElement));
}

public static TheoryData<string, string, bool?> ChatResponseFormatJson => new()
{
{
"""
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The person's full name"
}
}
}
""",
"JsonSchema",
null
},
{
"""
{
"type": "json_schema",
"json_schema": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The person's full name"
}
}
}
}
}
""",
"JsonSchema",
null
},
{
"""
{
"type": "json_schema",
"json_schema": {
"name": "Schema Name",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The person's full name"
}
}
}
}
}
""",
"Schema Name",
true
}
};

#region private

private JsonElement GetResponseFormat(ChatResponseFormat chatResponseFormat)
{
var settings = new OpenAIPromptExecutionSettings { ResponseFormat = chatResponseFormat };
return JsonDocument.Parse(JsonSerializer.Serialize(settings, this._options)).RootElement.GetProperty("response_format");
}

private sealed class BinaryDataJsonConverter : JsonConverter<BinaryData>
{
public override BinaryData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
string jsonString = reader.GetString()!;
return BinaryData.FromString(jsonString);
}

throw new JsonException("Expected a JSON string for BinaryData.");
}

public override void Write(Utf8JsonWriter writer, BinaryData value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}

#endregion
}
Loading
Loading