mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[BREAKING] Remove NotifyThreadOfNewMessagesAsync AIAgent helper (#2450)
* Remove NotifyThreadOfNewMessagesAsync helper from AIAgent * Fix bug * Update comment
This commit is contained in:
committed by
GitHub
Unverified
parent
1dde57981e
commit
b6c3b8d3f2
+12
-2
@@ -39,11 +39,16 @@ namespace SampleApp
|
||||
// Create a thread if the user didn't supply one.
|
||||
thread ??= this.GetNewThread();
|
||||
|
||||
if (thread is not CustomAgentThread typedThread)
|
||||
{
|
||||
throw new ArgumentException($"The provided thread is not of type {nameof(CustomAgentThread)}.", nameof(thread));
|
||||
}
|
||||
|
||||
// Clone the input messages and turn them into response messages with upper case text.
|
||||
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
|
||||
|
||||
// Notify the thread of the input and output messages.
|
||||
await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken);
|
||||
await typedThread.MessageStore.AddMessagesAsync(messages.Concat(responseMessages), cancellationToken);
|
||||
|
||||
return new AgentRunResponse
|
||||
{
|
||||
@@ -58,11 +63,16 @@ namespace SampleApp
|
||||
// Create a thread if the user didn't supply one.
|
||||
thread ??= this.GetNewThread();
|
||||
|
||||
if (thread is not CustomAgentThread typedThread)
|
||||
{
|
||||
throw new ArgumentException($"The provided thread is not of type {nameof(CustomAgentThread)}.", nameof(thread));
|
||||
}
|
||||
|
||||
// Clone the input messages and turn them into response messages with upper case text.
|
||||
List<ChatMessage> responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList();
|
||||
|
||||
// Notify the thread of the input and output messages.
|
||||
await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken);
|
||||
await typedThread.MessageStore.AddMessagesAsync(messages.Concat(responseMessages), cancellationToken);
|
||||
|
||||
foreach (var message in responseMessages)
|
||||
{
|
||||
|
||||
@@ -328,28 +328,4 @@ public abstract class AIAgent
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the specified thread about new messages that have been added to the conversation.
|
||||
/// </summary>
|
||||
/// <param name="thread">The conversation thread to notify about the new messages.</param>
|
||||
/// <param name="messages">The collection of new messages to report to the thread.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous notification operation.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="thread"/> or <paramref name="messages"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This method ensures that conversation threads are kept informed about message additions, which
|
||||
/// is important for threads that manage their own state, memory components, or derived context.
|
||||
/// While all agent implementations should notify their threads, the specific actions taken by
|
||||
/// each thread type may vary.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected static async Task NotifyThreadOfNewMessagesAsync(AgentThread thread, IEnumerable<ChatMessage> messages, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = Throw.IfNull(thread);
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
await thread.MessagesReceivedAsync(messages, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
@@ -65,19 +61,6 @@ public abstract class AgentThread
|
||||
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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Inheritors can use this method to update their context based on the new message.
|
||||
/// </remarks>
|
||||
/// <param name="newMessages">The new messages.</param>
|
||||
/// <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 updated.</returns>
|
||||
/// <exception cref="InvalidOperationException">The thread has been deleted.</exception>
|
||||
protected internal virtual Task MessagesReceivedAsync(IEnumerable<ChatMessage> newMessages, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <summary>Asks the <see cref="AgentThread"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
|
||||
/// <param name="serviceType">The type of object being requested.</param>
|
||||
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
|
||||
|
||||
@@ -4,8 +4,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
@@ -116,10 +114,6 @@ public abstract class InMemoryAgentThread : AgentThread
|
||||
public override object? GetService(Type serviceType, object? serviceKey = null) =>
|
||||
base.GetService(serviceType, serviceKey) ?? this.MessageStore?.GetService(serviceType, serviceKey);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override Task MessagesReceivedAsync(IEnumerable<ChatMessage> newMessages, CancellationToken cancellationToken = default)
|
||||
=> this.MessageStore.AddMessagesAsync(newMessages, cancellationToken);
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private string DebuggerDisplay => $"Count = {this.MessageStore.Count}";
|
||||
|
||||
|
||||
@@ -68,9 +68,6 @@ internal sealed class WorkflowThread : AgentThread
|
||||
|
||||
public CheckpointInfo? LastCheckpoint { get; set; }
|
||||
|
||||
protected override Task MessagesReceivedAsync(IEnumerable<ChatMessage> newMessages, CancellationToken cancellationToken = default)
|
||||
=> this.MessageStore.AddMessagesAsync(newMessages, cancellationToken);
|
||||
|
||||
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
{
|
||||
JsonMarshaller marshaller = new(jsonSerializerOptions);
|
||||
|
||||
@@ -270,7 +270,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
this.UpdateThreadWithTypeAndConversationId(safeThread, chatResponse.ConversationId);
|
||||
|
||||
// To avoid inconsistent state we only notify the thread of the input messages if no error occurs after the initial request.
|
||||
await NotifyThreadOfNewMessagesAsync(safeThread, inputMessages.Concat(aiContextProviderMessages ?? []).Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false);
|
||||
await NotifyMessageStoreOfNewMessagesAsync(safeThread, inputMessages.Concat(aiContextProviderMessages ?? []).Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Notify the AIContextProvider of all new messages.
|
||||
await NotifyAIContextProviderOfSuccessAsync(safeThread, inputMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
|
||||
@@ -413,7 +413,7 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
}
|
||||
|
||||
// Only notify the thread of new messages if the chatResponse was successful to avoid inconsistent message state in the thread.
|
||||
await NotifyThreadOfNewMessagesAsync(safeThread, inputMessages.Concat(aiContextProviderMessages ?? []).Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false);
|
||||
await NotifyMessageStoreOfNewMessagesAsync(safeThread, inputMessages.Concat(aiContextProviderMessages ?? []).Concat(chatResponse.Messages), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Notify the AIContextProvider of all new messages.
|
||||
await NotifyAIContextProviderOfSuccessAsync(safeThread, inputMessages, aiContextProviderMessages, chatResponse.Messages, cancellationToken).ConfigureAwait(false);
|
||||
@@ -711,12 +711,26 @@ public sealed partial class ChatClientAgent : AIAgent
|
||||
else
|
||||
{
|
||||
// If the service doesn't use service side thread storage (i.e. we got no id back from invocation), and
|
||||
// the thread has no MessageStore yet, and we have a custom messages store, we should update the thread
|
||||
// with the custom MessageStore so that it has somewhere to store the chat history.
|
||||
thread.MessageStore ??= this._agentOptions?.ChatMessageStoreFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null });
|
||||
// the thread has no MessageStore yet, we should update the thread with the custom MessageStore or
|
||||
// default InMemoryMessageStore so that it has somewhere to store the chat history.
|
||||
thread.MessageStore ??= this._agentOptions?.ChatMessageStoreFactory?.Invoke(new() { SerializedState = default, JsonSerializerOptions = null }) ?? new InMemoryChatMessageStore();
|
||||
}
|
||||
}
|
||||
|
||||
private static Task NotifyMessageStoreOfNewMessagesAsync(ChatClientAgentThread thread, IEnumerable<ChatMessage> newMessages, CancellationToken cancellationToken)
|
||||
{
|
||||
var messageStore = thread.MessageStore;
|
||||
|
||||
// Only notify the message store if we have one.
|
||||
// If we don't have one, it means that the chat history is service managed and the underlying service is responsible for storing messages.
|
||||
if (messageStore is not null)
|
||||
{
|
||||
return messageStore.AddMessagesAsync(newMessages, cancellationToken);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private string GetLoggingAgentName() => this.Name ?? "UnnamedAgent";
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI;
|
||||
@@ -181,33 +177,6 @@ public class ChatClientAgentThread : AgentThread
|
||||
?? this.AIContextProvider?.GetService(serviceType, serviceKey)
|
||||
?? this.MessageStore?.GetService(serviceType, serviceKey);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task MessagesReceivedAsync(IEnumerable<ChatMessage> newMessages, CancellationToken cancellationToken = default)
|
||||
{
|
||||
switch (this)
|
||||
{
|
||||
case { ConversationId: not null }:
|
||||
// If the thread messages are stored in the service
|
||||
// there is nothing to do here, since invoking the
|
||||
// service should already update the thread.
|
||||
break;
|
||||
|
||||
case { MessageStore: null }:
|
||||
// If there is no conversation id, and no store we can createa a default in memory store and add messages to it.
|
||||
this._messageStore = new InMemoryChatMessageStore();
|
||||
await this._messageStore!.AddMessagesAsync(newMessages, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case { MessageStore: not null }:
|
||||
// If a store has been provided, we need to add the messages to the store.
|
||||
await this._messageStore!.AddMessagesAsync(newMessages, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnreachableException();
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
private string DebuggerDisplay =>
|
||||
this.ConversationId is { } conversationId ? $"ConversationId = {conversationId}" :
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
|
||||
namespace Microsoft.Agents.AI.Abstractions.UnitTests;
|
||||
|
||||
@@ -222,21 +221,6 @@ public class AIAgentTests
|
||||
Assert.Equal(id, agent.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotifyThreadOfNewMessagesNotifiesThreadAsync()
|
||||
{
|
||||
var cancellationToken = default(CancellationToken);
|
||||
|
||||
var messages = new[] { new ChatMessage(ChatRole.User, "msg1"), new ChatMessage(ChatRole.User, "msg2") };
|
||||
|
||||
var threadMock = new Mock<TestAgentThread> { CallBase = true };
|
||||
threadMock.SetupAllProperties();
|
||||
|
||||
await MockAgent.NotifyThreadOfNewMessagesAsync(threadMock.Object, messages, cancellationToken);
|
||||
|
||||
threadMock.Protected().Verify("MessagesReceivedAsync", Times.Once(), messages, cancellationToken);
|
||||
}
|
||||
|
||||
#region GetService Method Tests
|
||||
|
||||
/// <summary>
|
||||
@@ -360,9 +344,6 @@ public class AIAgentTests
|
||||
|
||||
private sealed class MockAgent : AIAgent
|
||||
{
|
||||
public static new Task NotifyThreadOfNewMessagesAsync(AgentThread thread, IEnumerable<ChatMessage> messages, CancellationToken cancellationToken) =>
|
||||
AIAgent.NotifyThreadOfNewMessagesAsync(thread, messages, cancellationToken);
|
||||
|
||||
public override AgentThread GetNewThread()
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
#pragma warning disable CA1861 // Avoid constant arrays as arguments
|
||||
|
||||
@@ -21,15 +19,6 @@ public class AgentThreadTests
|
||||
Assert.Equal(default, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MessagesReceivedAsync_ReturnsCompletedTask()
|
||||
{
|
||||
var thread = new TestAgentThread();
|
||||
var messages = new List<ChatMessage> { new(ChatRole.User, "hello") };
|
||||
var result = thread.MessagesReceivedAsync(messages);
|
||||
Assert.True(result.IsCompleted);
|
||||
}
|
||||
|
||||
#region GetService Method Tests
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -4,7 +4,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
@@ -91,50 +90,6 @@ public class ChatClientAgentThreadTests
|
||||
|
||||
#endregion Constructor and Property Tests
|
||||
|
||||
#region OnNewMessagesAsync Tests
|
||||
|
||||
[Fact]
|
||||
public async Task OnNewMessagesAsyncDoesNothingWhenAgentServiceIdAsync()
|
||||
{
|
||||
// Arrange
|
||||
var thread = new ChatClientAgentThread { ConversationId = "thread-123" };
|
||||
var messages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!")
|
||||
};
|
||||
var agent = new MessageSendingAgent();
|
||||
|
||||
// Act
|
||||
await agent.SendMessagesAsync(thread, messages, CancellationToken.None);
|
||||
Assert.Equal("thread-123", thread.ConversationId);
|
||||
Assert.Null(thread.MessageStore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnNewMessagesAsyncAddsMessagesToStoreAsync()
|
||||
{
|
||||
// Arrange
|
||||
var store = new InMemoryChatMessageStore();
|
||||
var thread = new ChatClientAgentThread { MessageStore = store };
|
||||
var messages = new List<ChatMessage>
|
||||
{
|
||||
new(ChatRole.User, "Hello"),
|
||||
new(ChatRole.Assistant, "Hi there!")
|
||||
};
|
||||
var agent = new MessageSendingAgent();
|
||||
|
||||
// Act
|
||||
await agent.SendMessagesAsync(thread, messages, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, store.Count);
|
||||
Assert.Equal("Hello", store[0].Text);
|
||||
Assert.Equal("Hi there!", store[1].Text);
|
||||
}
|
||||
|
||||
#endregion OnNewMessagesAsync Tests
|
||||
|
||||
#region Deserialize Tests
|
||||
|
||||
[Fact]
|
||||
@@ -372,22 +327,4 @@ public class ChatClientAgentThreadTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private sealed class MessageSendingAgent : AIAgent
|
||||
{
|
||||
public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public override AgentThread GetNewThread()
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public override Task<AgentRunResponse> RunAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public override IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task SendMessagesAsync(AgentThread thread, IEnumerable<ChatMessage> messages, CancellationToken cancellationToken = default)
|
||||
=> NotifyThreadOfNewMessagesAsync(thread, messages, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user