Skip to content

Commit

Permalink
.Net: BugFix Serializing and Deserializing ChatHistory with ToolCalli…
Browse files Browse the repository at this point in the history
…ng details Update OpenAI Connector to use FunctionToolCallsProperty and add ChatHistoryTests (#4358)

Serializing the ToolCalls was losing the details about Name and
Arguments, and serializing this lack of content back was rendering
ToolCalls as an invalid history message and giving the Issue Error.

Resolves #4336 

Next PR, moving the Integration Tests as Unit Tests
## Summary
This pull request updates the OpenAI Connector to use the
FunctionToolCallsProperty metadata key instead of the ToolCallsProperty
metadata key. This change allows for the use of a list of
ChatCompletionsFunctionToolCall objects instead of
ChatCompletionsToolCall objects. Additionally, a new integration test,
ChatHistoryTests.cs, is added to the IntegrationTests/Connectors/OpenAI
directory. The test file contains tests for the ChatHistory feature of
the OpenAI Connector, ensuring that the feature returns the expected
results and handles errors correctly.

## Changes
- Update ClientCore.cs to use FunctionToolCallsProperty instead of
ToolCallsProperty
- Update OpenAIChatMessageContent.cs to use FunctionToolCallsProperty
instead of ToolCallsProperty
- Add ChatHistoryTests.cs to IntegrationTests/Connectors/OpenAI
directory
- Implement tests for ChatHistory feature of OpenAI Connector
- Dispose of logger and test output helper in ChatHistoryTests.cs

---
*Powered by [Microsoft Semantic
Kernel](https://github.com/microsoft/semantic-kernel)*
  • Loading branch information
RogerBarreto committed Dec 18, 2023
1 parent 48a6223 commit 95a621a
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -841,13 +841,13 @@ private static ChatRequestMessage GetRequestMessage(ChatMessageContent message)
var asstMessage = new ChatRequestAssistantMessage(message.Content);

IEnumerable<ChatCompletionsToolCall>? tools = (message as OpenAIChatMessageContent)?.ToolCalls;
if (tools is null && message.Metadata?.TryGetValue(OpenAIChatMessageContent.ToolCallsProperty, out object? toolCallsObject) is true)
if (tools is null && message.Metadata?.TryGetValue(OpenAIChatMessageContent.FunctionToolCallsProperty, out object? toolCallsObject) is true)
{
tools = toolCallsObject as IEnumerable<ChatCompletionsToolCall>;
tools = toolCallsObject as IEnumerable<ChatCompletionsFunctionToolCall>;
if (tools is null && toolCallsObject is JsonElement { ValueKind: JsonValueKind.Array } array)
{
int length = array.GetArrayLength();
var ftcs = new List<ChatCompletionsFunctionToolCall>(length);
var ftcs = new List<ChatCompletionsToolCall>(length);
for (int i = 0; i < length; i++)
{
JsonElement e = array[i];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Azure.AI.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;

Expand All @@ -18,9 +19,9 @@ public sealed class OpenAIChatMessageContent : ChatMessageContent
public static string ToolIdProperty => $"{nameof(ChatCompletionsToolCall)}.{nameof(ChatCompletionsToolCall.Id)}";

/// <summary>
/// Gets the metadata key for the <see cref="ChatCompletionsToolCall.Id"/> name property.
/// Gets the metadata key for the list of <see cref="ChatCompletionsFunctionToolCall"/>.
/// </summary>
public static string ToolCallsProperty => $"{nameof(ChatResponseMessage)}.{nameof(ChatResponseMessage.ToolCalls)}";
internal static string FunctionToolCallsProperty => $"{nameof(ChatResponseMessage)}.FunctionToolCalls";

/// <summary>
/// Initializes a new instance of the <see cref="OpenAIChatMessageContent"/> class.
Expand Down Expand Up @@ -110,7 +111,7 @@ public IReadOnlyList<OpenAIFunctionToolCall> GetOpenAIFunctionToolCalls()
}

// Add the additional entry.
newDictionary.Add(ToolCallsProperty, toolCalls);
newDictionary.Add(FunctionToolCallsProperty, toolCalls.OfType<ChatCompletionsFunctionToolCall>().ToList());

return newDictionary;
}
Expand Down
116 changes: 116 additions & 0 deletions dotnet/src/IntegrationTests/Connectors/OpenAI/ChatHistoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.ComponentModel;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using SemanticKernel.IntegrationTests.TestSettings;
using Xunit;
using Xunit.Abstractions;

namespace SemanticKernel.IntegrationTests.Connectors.OpenAI;

public sealed class ChatHistoryTests : IDisposable
{
private readonly IKernelBuilder _kernelBuilder;
private readonly XunitLogger<Kernel> _logger;
private readonly RedirectOutput _testOutputHelper;
private readonly IConfigurationRoot _configuration;
private static readonly JsonSerializerOptions s_jsonOptionsCache = new() { WriteIndented = true };
public ChatHistoryTests(ITestOutputHelper output)
{
this._logger = new XunitLogger<Kernel>(output);
this._testOutputHelper = new RedirectOutput(output);
Console.SetOut(this._testOutputHelper);

// Load configuration
this._configuration = new ConfigurationBuilder()
.AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.AddUserSecrets<OpenAICompletionTests>()
.Build();

this._kernelBuilder = Kernel.CreateBuilder();
}

[Fact]
public async Task ItSerializesAndDeserializesChatHistoryAsync()
{
// Arrange
this._kernelBuilder.Services.AddSingleton<ILoggerFactory>(this._logger);
var builder = this._kernelBuilder;
this.ConfigureAzureOpenAIChatAsText(builder);
builder.Plugins.AddFromType<FakePlugin>();
var kernel = builder.Build();

OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions };
ChatHistory history = new();

// Act
history.AddUserMessage("Make me a special poem");
var historyBeforeJson = JsonSerializer.Serialize(history.ToList(), s_jsonOptionsCache);
var service = kernel.GetRequiredService<IChatCompletionService>();
ChatMessageContent result = await service.GetChatMessageContentAsync(history, settings, kernel);
history.AddUserMessage("Ok thank you");

ChatMessageContent resultOriginalWorking = await service.GetChatMessageContentAsync(history, settings, kernel);
var historyJson = JsonSerializer.Serialize(history, s_jsonOptionsCache);
var historyAfterSerialization = JsonSerializer.Deserialize<ChatHistory>(historyJson);
var exception = await Record.ExceptionAsync(() => service.GetChatMessageContentAsync(historyAfterSerialization!, settings, kernel));

// Assert
Assert.Null(exception);
}

private void ConfigureAzureOpenAIChatAsText(IKernelBuilder kernelBuilder)
{
var azureOpenAIConfiguration = this._configuration.GetSection("Planners:AzureOpenAI").Get<AzureOpenAIConfiguration>();

Assert.NotNull(azureOpenAIConfiguration);
Assert.NotNull(azureOpenAIConfiguration.ChatDeploymentName);
Assert.NotNull(azureOpenAIConfiguration.ApiKey);
Assert.NotNull(azureOpenAIConfiguration.Endpoint);
Assert.NotNull(azureOpenAIConfiguration.ServiceId);

kernelBuilder.AddAzureOpenAIChatCompletion(
deploymentName: azureOpenAIConfiguration.ChatDeploymentName,
modelId: azureOpenAIConfiguration.ChatModelId,
endpoint: azureOpenAIConfiguration.Endpoint,
apiKey: azureOpenAIConfiguration.ApiKey,
serviceId: azureOpenAIConfiguration.ServiceId);
}

public class FakePlugin
{
[KernelFunction, Description("creates a special poem")]
public string CreateSpecialPoem()
{
return "ABCDE";
}
}

public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}

private void Dispose(bool disposing)
{
if (disposing)
{
this._logger.Dispose();
this._testOutputHelper.Dispose();
}
}
}

0 comments on commit 95a621a

Please sign in to comment.