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 @@ -53,6 +53,7 @@ public ChatMessage Clone() =>
AdditionalProperties = AdditionalProperties,
_authorName = _authorName,
_contents = _contents,
CreatedAt = CreatedAt,
RawRepresentation = RawRepresentation,
Role = Role,
MessageId = MessageId,
Expand All @@ -65,6 +66,9 @@ public string? AuthorName
set => _authorName = string.IsNullOrWhiteSpace(value) ? null : value;
}

/// <summary>Gets or sets a timestamp for the chat message.</summary>
public DateTimeOffset? CreatedAt { get; set; }

/// <summary>Gets or sets the role of the author of the message.</summary>
public ChatRole Role { get; set; } = ChatRole.User;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,19 +130,19 @@ public ChatResponseUpdate[] ToChatResponseUpdates()
ChatMessage message = _messages![i];
updates[i] = new ChatResponseUpdate
{
ConversationId = ConversationId,

AdditionalProperties = message.AdditionalProperties,
AuthorName = message.AuthorName,
Contents = message.Contents,
MessageId = message.MessageId,
RawRepresentation = message.RawRepresentation,
Role = message.Role,

ResponseId = ResponseId,
MessageId = message.MessageId,
CreatedAt = CreatedAt,
ConversationId = ConversationId,
FinishReason = FinishReason,
ModelId = ModelId
ModelId = ModelId,
ResponseId = ResponseId,

CreatedAt = message.CreatedAt ?? CreatedAt,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ public static void AddMessages(this IList<ChatMessage> list, ChatResponseUpdate
var contentsList = filter is null ? update.Contents : update.Contents.Where(filter).ToList();
if (contentsList.Count > 0)
{
list.Add(new ChatMessage(update.Role ?? ChatRole.Assistant, contentsList)
list.Add(new(update.Role ?? ChatRole.Assistant, contentsList)
{
AuthorName = update.AuthorName,
CreatedAt = update.CreatedAt,
RawRepresentation = update.RawRepresentation,
AdditionalProperties = update.AdditionalProperties,
});
Expand Down Expand Up @@ -268,7 +269,7 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon

if (isNewMessage)
{
message = new ChatMessage(ChatRole.Assistant, []);
message = new(ChatRole.Assistant, []);
response.Messages.Add(message);
}
else
Expand All @@ -280,11 +281,17 @@ private static void ProcessUpdate(ChatResponseUpdate update, ChatResponse respon
// Incorporate those into the latest message; in cases where the message
// stores a single value, prefer the latest update's value over anything
// stored in the message.

if (update.AuthorName is not null)
{
message.AuthorName = update.AuthorName;
}

if (update.CreatedAt is not null)
{
message.CreatedAt = update.CreatedAt;
}

if (update.Role is ChatRole role)
{
message.Role = role;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,10 @@
"Member": "System.Collections.Generic.IList<Microsoft.Extensions.AI.AIContent> Microsoft.Extensions.AI.ChatMessage.Contents { get; set; }",
"Stage": "Stable"
},
{
"Member": "System.DateTimeOffset? Microsoft.Extensions.AI.ChatMessage.CreatedAt { get; set; }",
"Stage": "Stable"
},
{
"Member": "string? Microsoft.Extensions.AI.ChatMessage.MessageId { get; set; }",
"Stage": "Stable"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public async Task<ChatResponse> GetResponseAsync(
// Create the return message.
ChatMessage message = new(ToChatRole(response.Role), response.Content)
{
CreatedAt = response.Created,
MessageId = response.Id, // There is no per-message ID, but there's only one message per response, so use the response ID
RawRepresentation = response,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ internal static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAICompl
// Create the return message.
ChatMessage returnMessage = new()
{
CreatedAt = openAICompletion.CreatedAt,
MessageId = openAICompletion.Id, // There's no per-message ID, so we use the same value as the response ID
RawRepresentation = openAICompletion,
Role = FromOpenAIChatRole(openAICompletion.Role),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ internal static ChatResponse FromOpenAIResponse(OpenAIResponse openAIResponse, R
}
}

foreach (var message in response.Messages)
{
message.CreatedAt = openAIResponse.CreatedAt;
}

return response;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public void Constructor_Parameterless_PropsDefaulted()
ChatMessage message = new();
Assert.Null(message.AuthorName);
Assert.Empty(message.Contents);
Assert.Null(message.CreatedAt);
Assert.Equal(ChatRole.User, message.Role);
Assert.Empty(message.Text);
Assert.NotNull(message.Contents);
Expand Down Expand Up @@ -50,6 +51,7 @@ public void Constructor_RoleString_PropsRoundtrip(string? text)
}

Assert.Null(message.AuthorName);
Assert.Null(message.CreatedAt);
Assert.Null(message.RawRepresentation);
Assert.Null(message.AdditionalProperties);
Assert.Equal(text ?? string.Empty, message.ToString());
Expand Down Expand Up @@ -113,6 +115,7 @@ public void Constructor_RoleList_PropsRoundtrip(int messageCount)
}

Assert.Null(message.AuthorName);
Assert.Null(message.CreatedAt);
Assert.Null(message.RawRepresentation);
Assert.Null(message.AdditionalProperties);
}
Expand Down Expand Up @@ -230,6 +233,20 @@ public void AdditionalProperties_Roundtrips()
Assert.Same(props, message.AdditionalProperties);
}

[Fact]
public void CreatedAt_Roundtrips()
{
ChatMessage message = new();
Assert.Null(message.CreatedAt);

DateTimeOffset now = DateTimeOffset.Now;
message.CreatedAt = now;
Assert.Equal(now, message.CreatedAt);

message.CreatedAt = null;
Assert.Null(message.CreatedAt);
}

[Fact]
public void ItCanBeSerializeAndDeserialized()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public void ToString_OutputsText()
}

[Fact]
public void ToChatResponseUpdates()
public void ToChatResponseUpdates_SingleMessage()
{
ChatResponse response = new(new ChatMessage(new ChatRole("customRole"), "Text") { MessageId = "someMessage" })
{
Expand Down Expand Up @@ -153,4 +153,55 @@ public void ToChatResponseUpdates()
Assert.Equal("value1", update1.AdditionalProperties?["key1"]);
Assert.Equal(42, update1.AdditionalProperties?["key2"]);
}

[Fact]
public void ToChatResponseUpdates_MultipleMessages()
{
ChatResponse response = new(
[
new ChatMessage(new ChatRole("customRole"), "Text")
{
CreatedAt = new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero),
MessageId = "someMessage"
},
new ChatMessage(new ChatRole("secondRole"), "Another message")
{
CreatedAt = new DateTimeOffset(2025, 1, 1, 10, 30, 0, TimeSpan.Zero),
MessageId = "anotherMessage"
}
])
{
ResponseId = "12345",
ModelId = "someModel",
FinishReason = ChatFinishReason.ContentFilter,
CreatedAt = new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero),
AdditionalProperties = new() { ["key1"] = "value1", ["key2"] = 42 },
};

ChatResponseUpdate[] updates = response.ToChatResponseUpdates();
Assert.NotNull(updates);
Assert.Equal(3, updates.Length);

ChatResponseUpdate update0 = updates[0];
Assert.Equal("12345", update0.ResponseId);
Assert.Equal("someMessage", update0.MessageId);
Assert.Equal("someModel", update0.ModelId);
Assert.Equal(ChatFinishReason.ContentFilter, update0.FinishReason);
Assert.Equal(new DateTimeOffset(2024, 11, 10, 9, 20, 0, TimeSpan.Zero), update0.CreatedAt);
Assert.Equal("customRole", update0.Role?.Value);
Assert.Equal("Text", update0.Text);

ChatResponseUpdate update1 = updates[1];
Assert.Equal("12345", update1.ResponseId);
Assert.Equal("anotherMessage", update1.MessageId);
Assert.Equal("someModel", update1.ModelId);
Assert.Equal(ChatFinishReason.ContentFilter, update1.FinishReason);
Assert.Equal(new DateTimeOffset(2025, 1, 1, 10, 30, 0, TimeSpan.Zero), update1.CreatedAt);
Assert.Equal("secondRole", update1.Role?.Value);
Assert.Equal("Another message", update1.Text);

ChatResponseUpdate update2 = updates[2];
Assert.Equal("value1", update2.AdditionalProperties?["key1"]);
Assert.Equal(42, update2.AdditionalProperties?["key2"]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,65 @@ public async Task ToChatResponse_SuccessfullyCreatesResponse(bool useAsync)
Assert.Equal("Hello, world!", response.Text);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task ToChatResponse_UpdatesProduceMultipleResponseMessages(bool useAsync)
{
ChatResponseUpdate[] updates =
[

// First message - ID "msg1"
new(null, "Hi! ") { CreatedAt = new DateTimeOffset(2023, 1, 1, 10, 0, 0, TimeSpan.Zero), AuthorName = "Assistant" },
new(ChatRole.Assistant, "Hello") { MessageId = "msg1", CreatedAt = new DateTimeOffset(2024, 1, 1, 10, 0, 0, TimeSpan.Zero), AuthorName = "Assistant" },
new(null, " from") { MessageId = "msg1", CreatedAt = new DateTimeOffset(2024, 1, 1, 10, 1, 0, TimeSpan.Zero) }, // Later CreatedAt should win
new(null, " AI") { MessageId = "msg1", AuthorName = "AI Assistant" }, // Later AuthorName should win

// Second message - ID "msg2"
new(ChatRole.User, "How") { MessageId = "msg2", CreatedAt = new DateTimeOffset(2024, 1, 1, 11, 0, 0, TimeSpan.Zero), AuthorName = "User" },
new(null, " are") { MessageId = "msg2", CreatedAt = new DateTimeOffset(2024, 1, 1, 11, 1, 0, TimeSpan.Zero) },
new(null, " you?") { MessageId = "msg2", AuthorName = "Human User" }, // Later AuthorName should win

// Third message - ID "msg3"
new(ChatRole.Assistant, "I'm doing well,") { MessageId = "msg3", CreatedAt = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.Zero) },
new(null, " thank you!") { MessageId = "msg3", CreatedAt = new DateTimeOffset(2024, 1, 1, 12, 2, 0, TimeSpan.Zero) }, // Later CreatedAt should win

// Updates without MessageId should continue the last message (msg3)
new(null, " How can I help?"),
];

ChatResponse response = useAsync ?
await YieldAsync(updates).ToChatResponseAsync() :
updates.ToChatResponse();

Assert.NotNull(response);
Assert.Equal(3, response.Messages.Count);

// Verify first message
ChatMessage message1 = response.Messages[0];
Assert.Equal("msg1", message1.MessageId);
Assert.Equal(ChatRole.Assistant, message1.Role);
Assert.Equal("AI Assistant", message1.AuthorName); // Last value should win
Assert.Equal(new DateTimeOffset(2024, 1, 1, 10, 1, 0, TimeSpan.Zero), message1.CreatedAt); // Last value should win
Assert.Equal("Hi! Hello from AI", message1.Text);

// Verify second message
ChatMessage message2 = response.Messages[1];
Assert.Equal("msg2", message2.MessageId);
Assert.Equal(ChatRole.User, message2.Role);
Assert.Equal("Human User", message2.AuthorName); // Last value should win
Assert.Equal(new DateTimeOffset(2024, 1, 1, 11, 1, 0, TimeSpan.Zero), message2.CreatedAt); // Last value should win
Assert.Equal("How are you?", message2.Text);

// Verify third message
ChatMessage message3 = response.Messages[2];
Assert.Equal("msg3", message3.MessageId);
Assert.Equal(ChatRole.Assistant, message3.Role);
Assert.Null(message3.AuthorName); // No AuthorName set in later updates
Assert.Equal(new DateTimeOffset(2024, 1, 1, 12, 2, 0, TimeSpan.Zero), message3.CreatedAt); // Last value should win
Assert.Equal("I'm doing well, thank you! How can I help?", message3.Text);
}

public static IEnumerable<object[]> ToChatResponse_Coalescing_VariousSequenceAndGapLengths_MemberData()
{
foreach (bool useAsync in new[] { false, true })
Expand Down
Loading