Skip to content

Commit

Permalink
.Net: Use empty string instead of null content in assistant message i…
Browse files Browse the repository at this point in the history
…n Azure/OpenAI connectors (#9150)

### Motivation and Context

<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

Resolves: #8072
Resolves: #8488

Azure Chat Completion with data service returns `400 (Bad Request)
Invalid chat message detected: message content must be string` response
when assistant message in chat history doesn't contain `content`
property or when this property is an array of content items. This case
is applicable for function calling, where AI model returns set of tools
to call instead of message content.

This PR contains a change to set `content` property as an empty string
only for cases when content doesn't exist (`null`).

### 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: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https:/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https:/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄
  • Loading branch information
dmytrostruk authored Oct 8, 2024
1 parent 60d555e commit 639df11
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,51 @@ public async Task ItDoesNotChangeDefaultsForToolsAndChoiceIfNeitherOfFunctionCal
Assert.False(optionsJson.TryGetProperty("tool_choice", out var _));
}

[Fact]
public async Task ItSendsEmptyStringWhenAssistantMessageContentIsNull()
{
// Arrange
var sut = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient);

using var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json"))
};
this._messageHandlerStub.ResponsesToReturn.Add(responseMessage);

List<ChatToolCall> assistantToolCalls = [ChatToolCall.CreateFunctionToolCall("id", "name", BinaryData.FromString("args"))];

var chatHistory = new ChatHistory()
{
new ChatMessageContent(role: AuthorRole.User, content: "User content", modelId: "any"),
new ChatMessageContent(role: AuthorRole.Assistant, content: null, modelId: "any", metadata: new Dictionary<string, object?>
{
["ChatResponseMessage.FunctionToolCalls"] = assistantToolCalls
}),
new ChatMessageContent(role: AuthorRole.Tool, content: null, modelId: "any")
{
Items = [new FunctionResultContent("FunctionName", "PluginName", "CallId", "Function result")]
},
};

var executionSettings = new AzureOpenAIPromptExecutionSettings();

// Act
await sut.GetChatMessageContentsAsync(chatHistory, executionSettings);

// Assert
var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!);
Assert.NotNull(actualRequestContent);

var requestContent = JsonSerializer.Deserialize<JsonElement>(actualRequestContent);
var messages = requestContent.GetProperty("messages").EnumerateArray().ToList();

var assistantMessage = messages.First(message => message.GetProperty("role").GetString() == "assistant");
var assistantMessageContent = assistantMessage.GetProperty("content").GetString();

Assert.Equal(string.Empty, assistantMessageContent);
}

public void Dispose()
{
this._httpClient.Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1359,6 +1359,47 @@ public async Task ItDoesNotChangeDefaultsForToolsAndChoiceIfNeitherOfFunctionCal
Assert.False(optionsJson.TryGetProperty("tool_choice", out var _));
}

[Fact]
public async Task ItSendsEmptyStringWhenAssistantMessageContentIsNull()
{
// Arrange
var chatCompletion = new OpenAIChatCompletionService(modelId: "any", apiKey: "NOKEY", httpClient: this._httpClient);
this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(ChatCompletionResponse)
};

List<ChatToolCall> assistantToolCalls = [ChatToolCall.CreateFunctionToolCall("id", "name", BinaryData.FromString("args"))];

var chatHistory = new ChatHistory()
{
new ChatMessageContent(role: AuthorRole.User, content: "User content", modelId: "any"),
new ChatMessageContent(role: AuthorRole.Assistant, content: null, modelId: "any", metadata: new Dictionary<string, object?>
{
["ChatResponseMessage.FunctionToolCalls"] = assistantToolCalls
}),
new ChatMessageContent(role: AuthorRole.Tool, content: null, modelId: "any")
{
Items = [new FunctionResultContent("FunctionName", "PluginName", "CallId", "Function result")]
},
};

// Act
await chatCompletion.GetChatMessageContentsAsync(chatHistory, this._executionSettings);

// Assert
var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!);
Assert.NotNull(actualRequestContent);

var requestContent = JsonSerializer.Deserialize<JsonElement>(actualRequestContent);
var messages = requestContent.GetProperty("messages").EnumerateArray().ToList();

var assistantMessage = messages.First(message => message.GetProperty("role").GetString() == "assistant");
var assistantMessageContent = assistantMessage.GetProperty("content").GetString();

Assert.Equal(string.Empty, assistantMessageContent);
}

public void Dispose()
{
this._httpClient.Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -758,10 +758,10 @@ private static List<ChatMessage> CreateRequestMessages(ChatMessageContent messag
}

var assistantMessage = new AssistantChatMessage(toolCalls) { ParticipantName = message.AuthorName };
if (message.Content is { } content)
{
assistantMessage.Content.Add(content);
}

// If message content is null, adding it as empty string,
// because chat message content must be string.
assistantMessage.Content.Add(message.Content ?? string.Empty);

return [assistantMessage];
}
Expand Down

0 comments on commit 639df11

Please sign in to comment.