Skip to content

Commit cde12d3

Browse files
authored
.Net Agents - Support AdditionalMessages for OpenAIAssistantAgent (#9737)
### 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. --> Add support for `AdditionalMessages` option when invoking a run. Fixes: #9685 ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> Allow the addition of multiple messages to a thread when invoking a `OpenAIAssistantAgent`. ### 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://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 - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone 😄
1 parent fa24473 commit cde12d3

File tree

9 files changed

+123
-5
lines changed

9 files changed

+123
-5
lines changed

dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public static IEnumerable<MessageContent> GetMessageContents(ChatMessageContent
5353
{
5454
yield return MessageContent.FromImageUri(imageContent.Uri);
5555
}
56-
else if (string.IsNullOrWhiteSpace(imageContent.DataUri))
56+
else if (!string.IsNullOrWhiteSpace(imageContent.DataUri))
5757
{
5858
yield return MessageContent.FromImageUri(new(imageContent.DataUri!));
5959
}

dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft. All rights reserved.
22
using System.Collections.Generic;
3+
using Microsoft.SemanticKernel.ChatCompletion;
34
using OpenAI.Assistants;
45

56
namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal;
@@ -45,6 +46,18 @@ public static RunCreationOptions GenerateOptions(OpenAIAssistantDefinition defin
4546
}
4647
}
4748

49+
if (invocationOptions?.AdditionalMessages != null)
50+
{
51+
foreach (ChatMessageContent message in invocationOptions.AdditionalMessages)
52+
{
53+
ThreadInitializationMessage threadMessage = new(
54+
role: message.Role == AuthorRole.User ? MessageRole.User : MessageRole.Assistant,
55+
content: AssistantMessageFactory.GetMessageContents(message));
56+
57+
options.AdditionalMessages.Add(threadMessage);
58+
}
59+
}
60+
4861
return options;
4962
}
5063

dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ public async Task<string> UploadFileAsync(Stream stream, string name, Cancellati
262262
/// <param name="threadId">The thread identifier</param>
263263
/// <param name="message">A non-system message with which to append to the conversation.</param>
264264
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
265+
/// <remarks>
266+
/// Only supports messages with role = User or Assistant:
267+
/// https://platform.openai.com/docs/api-reference/runs/createRun#runs-createrun-additional_messages
268+
/// </remarks>
265269
public Task AddChatMessageAsync(string threadId, ChatMessageContent message, CancellationToken cancellationToken = default)
266270
{
267271
this.ThrowIfDeleted();

dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ public sealed class OpenAIAssistantInvocationOptions
2424
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2525
public string? AdditionalInstructions { get; init; }
2626

27+
/// <summary>
28+
/// Additional messages to add to the thread.
29+
/// </summary>
30+
/// <remarks>
31+
/// Only supports messages with role = User or Assistant:
32+
/// https://platform.openai.com/docs/api-reference/runs/createRun#runs-createrun-additional_messages
33+
/// </remarks>
34+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
35+
public IReadOnlyList<ChatMessageContent>? AdditionalMessages { get; init; }
36+
2737
/// <summary>
2838
/// Set if code_interpreter tool is enabled.
2939
/// </summary>

dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ public sealed class OpenAIThreadCreationOptions
1818
/// <summary>
1919
/// Optional messages to initialize thread with..
2020
/// </summary>
21+
/// <remarks>
22+
/// Only supports messages with role = User or Assistant:
23+
/// https://platform.openai.com/docs/api-reference/runs/createRun#runs-createrun-additional_messages
24+
/// </remarks>
2125
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2226
public IReadOnlyList<ChatMessageContent>? Messages { get; init; }
2327

dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,11 @@ public void VerifyAssistantMessageAdapterGetMessageWithImageUrl()
150150
/// <summary>
151151
/// Verify options creation.
152152
/// </summary>
153-
[Fact(Skip = "API bug with data Uri construction")]
153+
[Fact]
154154
public void VerifyAssistantMessageAdapterGetMessageWithImageData()
155155
{
156156
// Arrange
157-
ChatMessageContent message = new(AuthorRole.User, items: [new ImageContent(new byte[] { 1, 2, 3 }, "image/png")]);
157+
ChatMessageContent message = new(AuthorRole.User, items: [new ImageContent(new byte[] { 1, 2, 3 }, "image/png") { DataUri = "data:image/png;base64,MTIz" }]);
158158

159159
// Act
160160
MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray();

dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright (c) Microsoft. All rights reserved.
22
using System.Collections.Generic;
3+
using Microsoft.SemanticKernel;
34
using Microsoft.SemanticKernel.Agents.OpenAI;
45
using Microsoft.SemanticKernel.Agents.OpenAI.Internal;
6+
using Microsoft.SemanticKernel.ChatCompletion;
57
using OpenAI.Assistants;
68
using Xunit;
79

@@ -35,6 +37,7 @@ public void AssistantRunOptionsFactoryExecutionOptionsNullTest()
3537

3638
// Assert
3739
Assert.NotNull(options);
40+
Assert.Empty(options.AdditionalMessages);
3841
Assert.Null(options.InstructionsOverride);
3942
Assert.Null(options.Temperature);
4043
Assert.Null(options.NucleusSamplingFactor);
@@ -147,4 +150,28 @@ public void AssistantRunOptionsFactoryExecutionOptionsMetadataTest()
147150
Assert.Equal("value", options.Metadata["key1"]);
148151
Assert.Equal(string.Empty, options.Metadata["key2"]);
149152
}
153+
154+
/// <summary>
155+
/// Verify run options generation with <see cref="OpenAIAssistantInvocationOptions"/> metadata.
156+
/// </summary>
157+
[Fact]
158+
public void AssistantRunOptionsFactoryExecutionOptionsMessagesTest()
159+
{
160+
// Arrange
161+
OpenAIAssistantDefinition definition = new("gpt-anything");
162+
163+
OpenAIAssistantInvocationOptions invocationOptions =
164+
new()
165+
{
166+
AdditionalMessages = [
167+
new ChatMessageContent(AuthorRole.User, "test message")
168+
]
169+
};
170+
171+
// Act
172+
RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, null, invocationOptions);
173+
174+
// Assert
175+
Assert.Single(options.AdditionalMessages);
176+
}
150177
}

dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright (c) Microsoft. All rights reserved.
22
using System.Collections.Generic;
33
using System.Text.Json;
4+
using Microsoft.SemanticKernel;
45
using Microsoft.SemanticKernel.Agents.OpenAI;
6+
using Microsoft.SemanticKernel.ChatCompletion;
57
using Xunit;
68

79
namespace SemanticKernel.Agents.UnitTests.OpenAI;
@@ -23,6 +25,7 @@ public void OpenAIAssistantInvocationOptionsInitialState()
2325
// Assert
2426
Assert.Null(options.ModelName);
2527
Assert.Null(options.AdditionalInstructions);
28+
Assert.Null(options.AdditionalMessages);
2629
Assert.Null(options.Metadata);
2730
Assert.Null(options.Temperature);
2831
Assert.Null(options.TopP);
@@ -50,6 +53,9 @@ public void OpenAIAssistantInvocationOptionsAssignment()
5053
{
5154
ModelName = "testmodel",
5255
AdditionalInstructions = "test instructions",
56+
AdditionalMessages = [
57+
new ChatMessageContent(AuthorRole.User, "test message")
58+
],
5359
Metadata = new Dictionary<string, string>() { { "a", "1" } },
5460
MaxCompletionTokens = 1000,
5561
MaxPromptTokens = 1000,
@@ -65,6 +71,7 @@ public void OpenAIAssistantInvocationOptionsAssignment()
6571
// Assert
6672
Assert.Equal("testmodel", options.ModelName);
6773
Assert.Equal("test instructions", options.AdditionalInstructions);
74+
Assert.Single(options.AdditionalMessages);
6875
Assert.Equal(2, options.Temperature);
6976
Assert.Equal(0, options.TopP);
7077
Assert.Equal(1000, options.MaxCompletionTokens);
@@ -89,6 +96,8 @@ private static void ValidateSerialization(OpenAIAssistantInvocationOptions sourc
8996

9097
// Assert
9198
Assert.NotNull(target);
99+
Assert.Equal(source.AdditionalInstructions, target.AdditionalInstructions);
100+
Assert.Equivalent(source.AdditionalMessages, target.AdditionalMessages);
92101
Assert.Equal(source.ModelName, target.ModelName);
93102
Assert.Equal(source.Temperature, target.Temperature);
94103
Assert.Equal(source.TopP, target.TopP);

dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ await this.ExecuteStreamingAgentAsync(
101101
}
102102

103103
/// <summary>
104-
/// Integration test for <see cref="OpenAIAssistantAgent"/> using function calling
105-
/// and targeting Azure OpenAI services.
104+
/// Integration test for <see cref="OpenAIAssistantAgent"/> adding a message with
105+
/// function result contents.
106106
/// </summary>
107107
[RetryFact(typeof(HttpOperationException))]
108108
public async Task AzureOpenAIAssistantAgentFunctionCallResultAsync()
@@ -130,6 +130,57 @@ await OpenAIAssistantAgent.CreateAsync(
130130
}
131131
}
132132

133+
/// <summary>
134+
/// Integration test for <see cref="OpenAIAssistantAgent"/> adding additional message to a thread.
135+
/// function result contents.
136+
/// </summary>
137+
[RetryFact(typeof(HttpOperationException))]
138+
public async Task AzureOpenAIAssistantAgentAdditionalMessagesAsync()
139+
{
140+
var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get<AzureOpenAIConfiguration>();
141+
Assert.NotNull(azureOpenAIConfiguration);
142+
143+
OpenAIAssistantAgent agent =
144+
await OpenAIAssistantAgent.CreateAsync(
145+
OpenAIClientProvider.ForAzureOpenAI(new AzureCliCredential(), new Uri(azureOpenAIConfiguration.Endpoint)),
146+
new(azureOpenAIConfiguration.ChatDeploymentName!),
147+
new Kernel());
148+
149+
OpenAIThreadCreationOptions threadOptions = new()
150+
{
151+
Messages = [
152+
new ChatMessageContent(AuthorRole.User, "Hello"),
153+
new ChatMessageContent(AuthorRole.Assistant, "How may I help you?"),
154+
]
155+
};
156+
string threadId = await agent.CreateThreadAsync(threadOptions);
157+
try
158+
{
159+
var messages = await agent.GetThreadMessagesAsync(threadId).ToArrayAsync();
160+
Assert.Equal(2, messages.Length);
161+
162+
OpenAIAssistantInvocationOptions invocationOptions = new()
163+
{
164+
AdditionalMessages = [
165+
new ChatMessageContent(AuthorRole.User, "This is my real question...in three parts:"),
166+
new ChatMessageContent(AuthorRole.User, "Part 1"),
167+
new ChatMessageContent(AuthorRole.User, "Part 2"),
168+
new ChatMessageContent(AuthorRole.User, "Part 3"),
169+
]
170+
};
171+
172+
messages = await agent.InvokeAsync(threadId, invocationOptions).ToArrayAsync();
173+
Assert.Single(messages);
174+
175+
messages = await agent.GetThreadMessagesAsync(threadId).ToArrayAsync();
176+
Assert.Equal(7, messages.Length);
177+
}
178+
finally
179+
{
180+
await agent.DeleteThreadAsync(threadId);
181+
}
182+
}
183+
133184
private async Task ExecuteAgentAsync(
134185
OpenAIClientProvider config,
135186
string modelName,

0 commit comments

Comments
 (0)