Skip to content
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 @@ -70,7 +70,7 @@ public async Task<ChatResponse> GetResponseAsync(
{
_ = Throw.IfNull(messages);

var openAIChatMessages = ToOpenAIChatMessages(messages, options, AIJsonUtilities.DefaultOptions);
var openAIChatMessages = ToOpenAIChatMessages(messages, options);
var openAIOptions = ToOpenAIOptions(options);

// Make the call to OpenAI.
Expand All @@ -85,7 +85,7 @@ public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
{
_ = Throw.IfNull(messages);

var openAIChatMessages = ToOpenAIChatMessages(messages, options, AIJsonUtilities.DefaultOptions);
var openAIChatMessages = ToOpenAIChatMessages(messages, options);
var openAIOptions = ToOpenAIOptions(options);

// Make the call to OpenAI.
Expand Down Expand Up @@ -115,7 +115,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? op
}

/// <summary>Converts an Extensions chat message enumerable to an OpenAI chat message enumerable.</summary>
private static IEnumerable<OpenAI.Chat.ChatMessage> ToOpenAIChatMessages(IEnumerable<ChatMessage> inputs, ChatOptions? chatOptions, JsonSerializerOptions jsonOptions)
internal static IEnumerable<OpenAI.Chat.ChatMessage> ToOpenAIChatMessages(IEnumerable<ChatMessage> inputs, ChatOptions? chatOptions)
{
// Maps all of the M.E.AI types to the corresponding OpenAI types.
// Unrecognized or non-processable content is ignored.
Expand Down Expand Up @@ -148,7 +148,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? op
{
try
{
result = JsonSerializer.Serialize(resultContent.Result, jsonOptions.GetTypeInfo(typeof(object)));
result = JsonSerializer.Serialize(resultContent.Result, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
}
catch (NotSupportedException)
{
Expand Down Expand Up @@ -176,7 +176,7 @@ internal static ChatTool ToOpenAIChatTool(AIFunction aiFunction, ChatOptions? op
case FunctionCallContent fc:
(toolCalls ??= []).Add(
ChatToolCall.CreateFunctionToolCall(fc.CallId, fc.Name, new(JsonSerializer.SerializeToUtf8Bytes(
fc.Arguments, jsonOptions.GetTypeInfo(typeof(IDictionary<string, object?>))))));
fc.Arguments, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary<string, object?>))))));
break;

default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,18 @@ public static ResponseTool AsOpenAIResponseTool(this AIFunction function) =>
public static ConversationFunctionTool AsOpenAIConversationFunctionTool(this AIFunction function) =>
OpenAIRealtimeConversationClient.ToOpenAIConversationFunctionTool(Throw.IfNull(function));

/// <summary>Creates a sequence of OpenAI <see cref="OpenAI.Chat.ChatMessage"/> instances from the specified input messages.</summary>
/// <param name="messages">The input messages to convert.</param>
/// <returns>A sequence of OpenAI chat messages.</returns>
public static IEnumerable<OpenAI.Chat.ChatMessage> AsOpenAIChatMessages(this IEnumerable<ChatMessage> messages) =>
OpenAIChatClient.ToOpenAIChatMessages(Throw.IfNull(messages), chatOptions: null);

/// <summary>Creates a sequence of OpenAI <see cref="OpenAI.Responses.ResponseItem"/> instances from the specified input messages.</summary>
/// <param name="messages">The input messages to convert.</param>
/// <returns>A sequence of OpenAI response items.</returns>
public static IEnumerable<OpenAI.Responses.ResponseItem> AsOpenAIResponseItems(this IEnumerable<ChatMessage> messages) =>
OpenAIResponseChatClient.ToOpenAIResponseItems(Throw.IfNull(messages));

// TODO: Once we're ready to rely on C# 14 features, add an extension property ChatOptions.Strict.

/// <summary>Gets whether the properties specify that strict schema handling is desired.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#pragma warning disable S1067 // Expressions should not be too complex
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
#pragma warning disable S3604 // Member initializer values should not be redundant
#pragma warning disable SA1202 // Elements should be ordered by access
#pragma warning disable SA1204 // Static elements should appear before instance elements

namespace Microsoft.Extensions.AI;
Expand Down Expand Up @@ -466,8 +467,7 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt
}

/// <summary>Convert a sequence of <see cref="ChatMessage"/>s to <see cref="ResponseItem"/>s.</summary>
private static IEnumerable<ResponseItem> ToOpenAIResponseItems(
IEnumerable<ChatMessage> inputs)
internal static IEnumerable<ResponseItem> ToOpenAIResponseItems(IEnumerable<ChatMessage> inputs)
{
foreach (ChatMessage input in inputs)
{
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.Json;
using OpenAI.Assistants;
using OpenAI.Chat;
using OpenAI.Realtime;
using OpenAI.Responses;
using Xunit;

namespace Microsoft.Extensions.AI;

public class OpenAIConversionTests
{
private static readonly AIFunction _testFunction = AIFunctionFactory.Create(
([Description("The name parameter")] string name) => name,
"test_function",
"A test function for conversion");

[Fact]
public void AsOpenAIChatTool_ProducesValidInstance()
{
var tool = _testFunction.AsOpenAIChatTool();

Assert.NotNull(tool);
Assert.Equal("test_function", tool.FunctionName);
Assert.Equal("A test function for conversion", tool.FunctionDescription);
ValidateSchemaParameters(tool.FunctionParameters);
}

[Fact]
public void AsOpenAIResponseTool_ProducesValidInstance()
{
var tool = _testFunction.AsOpenAIResponseTool();

Assert.NotNull(tool);
}

[Fact]
public void AsOpenAIConversationFunctionTool_ProducesValidInstance()
{
var tool = _testFunction.AsOpenAIConversationFunctionTool();

Assert.NotNull(tool);
Assert.Equal("test_function", tool.Name);
Assert.Equal("A test function for conversion", tool.Description);
ValidateSchemaParameters(tool.Parameters);
}

[Fact]
public void AsOpenAIAssistantsFunctionToolDefinition_ProducesValidInstance()
{
var tool = _testFunction.AsOpenAIAssistantsFunctionToolDefinition();

Assert.NotNull(tool);
Assert.Equal("test_function", tool.FunctionName);
Assert.Equal("A test function for conversion", tool.Description);
ValidateSchemaParameters(tool.Parameters);
}

/// <summary>Helper method to validate function parameters match our schema.</summary>
private static void ValidateSchemaParameters(BinaryData parameters)
{
Assert.NotNull(parameters);

using var jsonDoc = JsonDocument.Parse(parameters);
var root = jsonDoc.RootElement;

Assert.Equal("object", root.GetProperty("type").GetString());
Assert.True(root.TryGetProperty("properties", out var properties));
Assert.True(properties.TryGetProperty("name", out var nameProperty));
Assert.Equal("string", nameProperty.GetProperty("type").GetString());
Assert.Equal("The name parameter", nameProperty.GetProperty("description").GetString());
}

[Fact]
public void AsOpenAIChatMessages_ProducesExpectedOutput()
{
Assert.Throws<ArgumentNullException>("messages", () => ((IEnumerable<ChatMessage>)null!).AsOpenAIChatMessages());

List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helpful assistant."),
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant,
[
new TextContent("Hi there!"),
new FunctionCallContent("callid123", "SomeFunction", new Dictionary<string, object?>
{
["param1"] = "value1",
["param2"] = 42
}),
]),
new(ChatRole.Tool, [new FunctionResultContent("callid123", "theresult")]),
new(ChatRole.Assistant, "The answer is 42."),
];

var convertedMessages = messages.AsOpenAIChatMessages().ToArray();

Assert.Equal(5, convertedMessages.Length);

SystemChatMessage m0 = Assert.IsType<SystemChatMessage>(convertedMessages[0]);
Assert.Equal("You are a helpful assistant.", Assert.Single(m0.Content).Text);

UserChatMessage m1 = Assert.IsType<UserChatMessage>(convertedMessages[1]);
Assert.Equal("Hello", Assert.Single(m1.Content).Text);

AssistantChatMessage m2 = Assert.IsType<AssistantChatMessage>(convertedMessages[2]);
Assert.Single(m2.Content);
Assert.Equal("Hi there!", m2.Content[0].Text);
var tc = Assert.Single(m2.ToolCalls);
Assert.Equal("callid123", tc.Id);
Assert.Equal("SomeFunction", tc.FunctionName);
Assert.True(JsonElement.DeepEquals(JsonSerializer.SerializeToElement(new Dictionary<string, object?>
{
["param1"] = "value1",
["param2"] = 42
}), JsonSerializer.Deserialize<JsonElement>(tc.FunctionArguments.ToMemory().Span)));

ToolChatMessage m3 = Assert.IsType<ToolChatMessage>(convertedMessages[3]);
Assert.Equal("callid123", m3.ToolCallId);
Assert.Equal("theresult", Assert.Single(m3.Content).Text);

AssistantChatMessage m4 = Assert.IsType<AssistantChatMessage>(convertedMessages[4]);
Assert.Equal("The answer is 42.", Assert.Single(m4.Content).Text);
}

[Fact]
public void AsOpenAIResponseItems_ProducesExpectedOutput()
{
Assert.Throws<ArgumentNullException>("messages", () => ((IEnumerable<ChatMessage>)null!).AsOpenAIResponseItems());

List<ChatMessage> messages =
[
new(ChatRole.System, "You are a helpful assistant."),
new(ChatRole.User, "Hello"),
new(ChatRole.Assistant,
[
new TextContent("Hi there!"),
new FunctionCallContent("callid123", "SomeFunction", new Dictionary<string, object?>
{
["param1"] = "value1",
["param2"] = 42
}),
]),
new(ChatRole.Tool, [new FunctionResultContent("callid123", "theresult")]),
new(ChatRole.Assistant, "The answer is 42."),
];

var convertedItems = messages.AsOpenAIResponseItems().ToArray();

Assert.Equal(6, convertedItems.Length);

MessageResponseItem m0 = Assert.IsAssignableFrom<MessageResponseItem>(convertedItems[0]);
Assert.Equal("You are a helpful assistant.", Assert.Single(m0.Content).Text);

MessageResponseItem m1 = Assert.IsAssignableFrom<MessageResponseItem>(convertedItems[1]);
Assert.Equal(OpenAI.Responses.MessageRole.User, m1.Role);
Assert.Equal("Hello", Assert.Single(m1.Content).Text);

MessageResponseItem m2 = Assert.IsAssignableFrom<MessageResponseItem>(convertedItems[2]);
Assert.Equal(OpenAI.Responses.MessageRole.Assistant, m2.Role);
Assert.Equal("Hi there!", Assert.Single(m2.Content).Text);

FunctionCallResponseItem m3 = Assert.IsAssignableFrom<FunctionCallResponseItem>(convertedItems[3]);
Assert.Equal("callid123", m3.CallId);
Assert.Equal("SomeFunction", m3.FunctionName);
Assert.True(JsonElement.DeepEquals(JsonSerializer.SerializeToElement(new Dictionary<string, object?>
{
["param1"] = "value1",
["param2"] = 42
}), JsonSerializer.Deserialize<JsonElement>(m3.FunctionArguments.ToMemory().Span)));

FunctionCallOutputResponseItem m4 = Assert.IsAssignableFrom<FunctionCallOutputResponseItem>(convertedItems[4]);
Assert.Equal("callid123", m4.CallId);
Assert.Equal("theresult", m4.FunctionOutput);

MessageResponseItem m5 = Assert.IsAssignableFrom<MessageResponseItem>(convertedItems[5]);
Assert.Equal(OpenAI.Responses.MessageRole.Assistant, m5.Role);
Assert.Equal("The answer is 42.", Assert.Single(m5.Content).Text);
}
}
Loading