.NET: Make serialize methods sync and rename one to match others. (#946)

* Make serialize methods sync and rename one to match others.

* Remove unnecessary async postfixes.

* Remove nullability of ChatMessageStore.Serialize return type, since the default JsonElement already represents an undefined json element.

* Fix unit test
This commit is contained in:
westey
2025-09-29 10:32:01 +01:00
committed by GitHub
Unverified
parent 7c5b553c7e
commit bf5931932e
21 changed files with 64 additions and 86 deletions
@@ -30,7 +30,7 @@ AgentThread thread = agent.GetNewThread();
Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread));
// Serialize the thread state to a JsonElement, so it can be stored for later use.
JsonElement serializedThread = await thread.SerializeAsync();
JsonElement serializedThread = thread.Serialize();
// Save the serialized thread to a temporary file (for demonstration purposes).
string tempFilePath = Path.GetTempFileName();
@@ -56,7 +56,7 @@ Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.", thread)
// Serialize the thread state, so it can be stored for later use.
// Since the chat history is stored in the vector store, the serialized thread
// only contains the guid that the messages are stored under in the vector store.
JsonElement serializedThread = await thread.SerializeAsync();
JsonElement serializedThread = thread.Serialize();
Console.WriteLine("\n--- Serialized thread ---\n");
Console.WriteLine(JsonSerializer.Serialize(serializedThread, new JsonSerializerOptions { WriteIndented = true }));
@@ -131,9 +131,9 @@ namespace SampleApp
return messages;
}
public override ValueTask<JsonElement?> SerializeStateAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) =>
// We have to serialize the thread id, so that on deserialization we can retrieve the messages using the same thread id.
new(JsonSerializer.SerializeToElement(this.ThreadDbKey));
JsonSerializer.SerializeToElement(this.ThreadDbKey);
/// <summary>
/// The data structure used to store chat history items in the vector store.
@@ -52,7 +52,7 @@ Console.WriteLine(await agent.RunAsync("My name is Ruaidhrí", thread));
Console.WriteLine(await agent.RunAsync("I am 20 years old", thread));
// We can serialize the thread. The serialized state will include the state of the memory component.
var threadElement = await thread.SerializeAsync();
var threadElement = thread.Serialize();
Console.WriteLine("\n>> Use deserialized thread with previously created memories\n");
@@ -148,9 +148,9 @@ namespace SampleApp
});
}
public override ValueTask<JsonElement?> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
return new ValueTask<JsonElement?>(JsonSerializer.SerializeToElement(this.UserInfo, jsonSerializerOptions));
return JsonSerializer.SerializeToElement(this.UserInfo, jsonSerializerOptions);
}
}
@@ -39,20 +39,15 @@ public abstract class AIContextProvider
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task that completes when the context has been rendered and returned.</returns>
public virtual ValueTask InvokedAsync(InvokedContext context, CancellationToken cancellationToken = default)
{
return default;
}
=> default;
/// <summary>
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
/// </summary>
/// <param name="jsonSerializerOptions">The JSON serialization options to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
public virtual ValueTask<JsonElement?> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
{
return default;
}
public virtual JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> default;
/// <summary>Asks the <see cref="AIContextProvider"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <param name="serviceType">The type of object being requested.</param>
@@ -27,10 +27,9 @@ public abstract class AgentThread
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
/// </summary>
/// <param name="jsonSerializerOptions">The JSON serialization options to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
public virtual Task<JsonElement> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
=> Task.FromResult(default(JsonElement));
public virtual JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> default;
/// <summary>
/// This method is called when new messages have been contributed to the chat by any participant.
@@ -51,9 +51,8 @@ public abstract class ChatMessageStore
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
/// </summary>
/// <param name="jsonSerializerOptions">The JSON serialization options to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
public abstract ValueTask<JsonElement?> SerializeStateAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default);
public abstract JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null);
/// <summary>Asks the <see cref="ChatMessageStore"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
/// <param name="serviceType">The type of object being requested.</param>
@@ -67,11 +67,10 @@ public abstract class InMemoryAgentThread : AgentThread
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
/// </summary>
/// <param name="jsonSerializerOptions">The JSON serialization options to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
public override async Task<JsonElement> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
var storeState = await this.MessageStore.SerializeStateAsync(jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
var storeState = this.MessageStore.Serialize(jsonSerializerOptions);
var state = new InMemoryAgentThreadState
{
@@ -121,14 +121,14 @@ public sealed class InMemoryChatMessageStore : ChatMessageStore, IList<ChatMessa
}
/// <inheritdoc />
public override ValueTask<JsonElement?> SerializeStateAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
StoreState state = new()
{
Messages = this._messages,
};
return new ValueTask<JsonElement?>(JsonSerializer.SerializeToElement(state, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(StoreState))));
return JsonSerializer.SerializeToElement(state, AgentAbstractionsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(StoreState)));
}
/// <inheritdoc />
@@ -2,8 +2,6 @@
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
@@ -63,9 +61,8 @@ public abstract class ServiceIdAgentThread : AgentThread
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
/// </summary>
/// <param name="jsonSerializerOptions">The JSON serialization options to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
public override async Task<JsonElement> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
var state = new ServiceIdAgentThreadState
{
@@ -121,7 +121,7 @@ internal sealed class AgentActor(
}
var serializedRunResponse = JsonSerializer.SerializeToElement(updates.ToAgentRunResponse(), AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(AgentRunResponse)));
var updatedThread = await this._thread.SerializeAsync(AgentHostingJsonUtilities.DefaultOptions, cancellationToken).ConfigureAwait(false);
var updatedThread = this._thread.Serialize(AgentHostingJsonUtilities.DefaultOptions);
var writeResponse = await context.WriteAsync(
new(this._etag,
@@ -149,21 +149,16 @@ public class ChatClientAgentThread : AgentThread
/// </summary>
public AIContextProvider? AIContextProvider { get; internal set; }
/// <summary>
/// Serializes the current object's state to a <see cref="JsonElement"/> using the specified serialization options.
/// </summary>
/// <param name="jsonSerializerOptions">The JSON serialization options to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A <see cref="JsonElement"/> representation of the object's state.</returns>
public override async Task<JsonElement> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
/// <inheritdoc/>
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
var storeState = this._messageStore is null ?
JsonElement? storeState = this._messageStore is null ?
null :
await this._messageStore.SerializeStateAsync(jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
this._messageStore.Serialize(jsonSerializerOptions);
var aiContextProviderState = this.AIContextProvider is null ?
JsonElement? aiContextProviderState = this.AIContextProvider is null ?
null :
await this.AIContextProvider.SerializeAsync(jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
this.AIContextProvider.Serialize(jsonSerializerOptions);
var state = new ThreadState
{
@@ -30,7 +30,7 @@ internal sealed class AIAgentHostExecutor : ChatProtocolExecutor
Task threadTask = Task.CompletedTask;
if (this._thread is not null)
{
JsonElement threadValue = await this._thread.SerializeAsync(cancellationToken: cancellation).ConfigureAwait(false);
JsonElement threadValue = this._thread.Serialize();
threadTask = context.QueueStateUpdateAsync(ThreadStateKey, threadValue).AsTask();
}
@@ -65,7 +65,7 @@ internal sealed class WorkflowMessageStore : ChatMessageStore
public void UpdateBookmark() => this._bookmark = this._chatMessages.Count;
public override ValueTask<JsonElement?> SerializeStateAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
StoreState state = new()
{
@@ -73,8 +73,7 @@ internal sealed class WorkflowMessageStore : ChatMessageStore
Messages = this._chatMessages,
};
return new ValueTask<JsonElement?>
(JsonSerializer.SerializeToElement(state,
WorkflowsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(StoreState))));
return JsonSerializer.SerializeToElement(state,
WorkflowsJsonUtilities.DefaultOptions.GetTypeInfo(typeof(StoreState)));
}
}
@@ -2,8 +2,6 @@
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
@@ -28,7 +26,8 @@ internal sealed class WorkflowThread : AgentThread
public string ResponseId => $"{this.RunId}@{this.Halts}";
public override Task<JsonElement> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) => throw new NotImplementedException("Pending Checkpointing work.");
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> throw new NotImplementedException("Pending Checkpointing work.");
public AgentRunResponseUpdate CreateUpdate(params AIContent[] parts)
{
@@ -21,10 +21,10 @@ public class AIContextProviderTests
}
[Fact]
public async Task SerializeAsync_ReturnsEmptyElementAsync()
public void Serialize_ReturnsEmptyElement()
{
var provider = new TestAIContextProvider();
var actual = await provider.SerializeAsync();
var actual = provider.Serialize();
Assert.Equal(default, actual);
}
@@ -163,9 +163,9 @@ public class AIContextProviderTests
return default;
}
public override async ValueTask<JsonElement?> SerializeAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
return await base.SerializeAsync(jsonSerializerOptions, cancellationToken);
return base.Serialize(jsonSerializerOptions);
}
}
}
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
#pragma warning disable CA1861 // Avoid constant arrays as arguments
@@ -15,10 +14,10 @@ namespace Microsoft.Agents.AI.Abstractions.UnitTests;
public class AgentThreadTests
{
[Fact]
public async Task SerializeAsync_ReturnsDefaultJsonElementAsync()
public void Serialize_ReturnsDefaultJsonElement()
{
var thread = new TestAgentThread();
var result = await thread.SerializeAsync();
var result = thread.Serialize();
Assert.Equal(default, result);
}
@@ -84,7 +84,7 @@ public class ChatMessageStoreTests
public override Task AddMessagesAsync(IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public override ValueTask<JsonElement?> SerializeStateAsync(JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
=> default;
}
}
@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
@@ -58,11 +57,11 @@ public class InMemoryAgentThreadTests
}
[Fact]
public async Task Constructor_WithSerializedState_SetsPropertyAsync()
public void Constructor_WithSerializedState_SetsProperty()
{
// Arrange
InMemoryChatMessageStore store = [new(ChatRole.User, "TestMsg")];
var storeState = await store.SerializeStateAsync();
var storeState = store.Serialize();
var json = JsonSerializer.SerializeToElement(new { storeState });
// Act
@@ -89,13 +88,13 @@ public class InMemoryAgentThreadTests
#region SerializeAsync Tests
[Fact]
public async Task SerializeAsync_ReturnsCorrectJson_WhenMessagesExistAsync()
public void Serialize_ReturnsCorrectJson_WhenMessagesExist()
{
// Arrange
var thread = new TestInMemoryAgentThread([new(ChatRole.User, "TestContent")]);
// Act
var json = await thread.SerializeAsync();
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
@@ -108,13 +107,13 @@ public class InMemoryAgentThreadTests
}
[Fact]
public async Task SerializeAsync_ReturnsEmptyMessages_WhenNoMessagesAsync()
public void Serialize_ReturnsEmptyMessages_WhenNoMessages()
{
// Arrange
var thread = new TestInMemoryAgentThread();
// Act
var json = await thread.SerializeAsync();
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
@@ -106,8 +106,8 @@ public class InMemoryChatMessageStoreTests
new ChatMessage(ChatRole.Assistant, "B")
};
var jsonElement = await store.SerializeStateAsync();
var newStore = new InMemoryChatMessageStore(jsonElement.Value);
var jsonElement = store.Serialize();
var newStore = new InMemoryChatMessageStore(jsonElement);
Assert.Equal(2, newStore.Count);
Assert.Equal("A", newStore[0].Text);
@@ -2,7 +2,6 @@
using System;
using System.Text.Json;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
@@ -74,13 +73,13 @@ public class ServiceIdAgentThreadTests
#region SerializeAsync Tests
[Fact]
public async Task SerializeAsync_ReturnsCorrectJson_WhenServiceThreadIdIsSetAsync()
public void Serialize_ReturnsCorrectJson_WhenServiceThreadIdIsSet()
{
// Arrange
var thread = new TestServiceIdAgentThread("service-id-789");
// Act
var json = await thread.SerializeAsync();
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
@@ -89,13 +88,13 @@ public class ServiceIdAgentThreadTests
}
[Fact]
public async Task SerializeAsync_ReturnsUndefinedServiceThreadId_WhenNotSetAsync()
public void Serialize_ReturnsUndefinedServiceThreadId_WhenNotSet()
{
// Arrange
var thread = new TestServiceIdAgentThread();
// Act
var json = await thread.SerializeAsync();
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
@@ -216,13 +216,13 @@ public class ChatClientAgentThreadTests
/// Verify thread serialization to JSON when the thread has an id.
/// </summary>
[Fact]
public async Task VerifyThreadSerializationWithIdAsync()
public void VerifyThreadSerializationWithId()
{
// Arrange
var thread = new ChatClientAgentThread { ConversationId = "TestConvId" };
// Act
var json = await thread.SerializeAsync();
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
@@ -237,14 +237,14 @@ public class ChatClientAgentThreadTests
/// Verify thread serialization to JSON when the thread has messages.
/// </summary>
[Fact]
public async Task VerifyThreadSerializationWithMessagesAsync()
public void VerifyThreadSerializationWithMessages()
{
// Arrange
InMemoryChatMessageStore store = [new(ChatRole.User, "TestContent") { AuthorName = "TestAuthor" }];
var thread = new ChatClientAgentThread { MessageStore = store };
// Act
var json = await thread.SerializeAsync();
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
@@ -269,14 +269,13 @@ public class ChatClientAgentThreadTests
}
[Fact]
public async Task VerifyThreadSerializationWithWithAIContextProviderAsync()
public void VerifyThreadSerializationWithWithAIContextProvider()
{
// Arrange
Mock<AIContextProvider> mockProvider = new();
var providerStateElement = JsonSerializer.SerializeToElement(["CP1"], TestJsonSerializerContext.Default.StringArray);
mockProvider
.Setup(m => m.SerializeAsync(It.IsAny<JsonSerializerOptions?>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(providerStateElement);
.Setup(m => m.Serialize(It.IsAny<JsonSerializerOptions?>()))
.Returns(JsonSerializer.SerializeToElement(["CP1"], TestJsonSerializerContext.Default.StringArray));
var thread = new ChatClientAgentThread
{
@@ -284,7 +283,7 @@ public class ChatClientAgentThreadTests
};
// Act
var json = await thread.SerializeAsync();
var json = thread.Serialize();
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
@@ -292,14 +291,14 @@ public class ChatClientAgentThreadTests
Assert.Equal(JsonValueKind.Array, providerStateProperty.ValueKind);
Assert.Single(providerStateProperty.EnumerateArray());
Assert.Equal("CP1", providerStateProperty.EnumerateArray().First().GetString());
mockProvider.Verify(m => m.SerializeAsync(It.IsAny<JsonSerializerOptions?>(), It.IsAny<CancellationToken>()), Times.Once);
mockProvider.Verify(m => m.Serialize(It.IsAny<JsonSerializerOptions?>()), Times.Once);
}
/// <summary>
/// Verify thread serialization to JSON with custom options.
/// </summary>
[Fact]
public async Task VerifyThreadSerializationWithCustomOptionsAsync()
public void VerifyThreadSerializationWithCustomOptions()
{
// Arrange
var thread = new ChatClientAgentThread();
@@ -312,12 +311,12 @@ public class ChatClientAgentThreadTests
var messageStoreMock = new Mock<ChatMessageStore>();
messageStoreMock
.Setup(m => m.SerializeStateAsync(options, It.IsAny<CancellationToken>()))
.ReturnsAsync(storeStateElement);
.Setup(m => m.Serialize(options))
.Returns(storeStateElement);
thread.MessageStore = messageStoreMock.Object;
// Act
var json = await thread.SerializeAsync(options);
var json = thread.Serialize(options);
// Assert
Assert.Equal(JsonValueKind.Object, json.ValueKind);
@@ -330,7 +329,7 @@ public class ChatClientAgentThreadTests
Assert.True(storeStateProperty.TryGetProperty("Key", out var keyProperty));
Assert.Equal("TestValue", keyProperty.GetString());
messageStoreMock.Verify(m => m.SerializeStateAsync(options, It.IsAny<CancellationToken>()), Times.Once);
messageStoreMock.Verify(m => m.Serialize(options), Times.Once);
}
#endregion Serialize Tests