-
Notifications
You must be signed in to change notification settings - Fork 1.9k
.NET: Add AIContextProvider message event and fix internal agent issue #6505
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
XiongHaoTrigger
wants to merge
3
commits into
microsoft:main
Choose a base branch
from
XiongHaoTrigger:fix/WorkflowHostAgent
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+348
−4
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
81140f4
feat(workflows): Add AIContextProvider message event and test, Fix th…
XiongHaoTrigger 876ba93
perf: Capture chat history snapshots on demand, without having to obt…
XiongHaoTrigger 8d3528b
fix: Ensure that the messages can be properly filled in when they are…
XiongHaoTrigger File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
dotnet/src/Microsoft.Agents.AI.Workflows/AgentAIContextProviderMsgEvent.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System.Collections.Generic; | ||
| using Microsoft.Extensions.AI; | ||
|
|
||
| namespace Microsoft.Agents.AI.Workflows; | ||
|
|
||
| internal sealed class AgentAIContextProviderMsgEvent(IReadOnlyList<ChatMessage> messages) : WorkflowEvent(messages) | ||
| { | ||
| public IReadOnlyList<ChatMessage> Messages { get; } = messages; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
185 changes: 185 additions & 0 deletions
185
dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIContextProviderWorkflowTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| // Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Runtime.CompilerServices; | ||
| using System.Text.Json; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using FluentAssertions; | ||
| using Microsoft.Extensions.AI; | ||
|
|
||
| namespace Microsoft.Agents.AI.Workflows.UnitTests; | ||
|
|
||
| /// <summary> | ||
| /// Validates that messages injected by <see cref="AIContextProvider"/> into an inner agent | ||
| /// are correctly persisted into the workflow's chat history, without leaking to downstream agents. | ||
| /// </summary> | ||
| public class AIContextProviderWorkflowTests | ||
| { | ||
| private const string UserText = "Where is Taggia?"; | ||
| private const string ContextText = "Taggia is a city in Liguria."; | ||
| private const string FirstAgentResponseText = "Taggia is in Liguria."; | ||
|
|
||
| /// <summary> | ||
| /// Ensures that AIContextProvider-injected messages appear in the workflow session's | ||
| /// chat history and survive serialization (regression test for the bug where such | ||
| /// messages were lost because WorkflowHostAgent only persisted model outputs). | ||
| /// </summary> | ||
| [Fact] | ||
| public async Task Test_WorkflowAsAgent_SerializesAIContextProviderRequestMessagesAsync() | ||
| { | ||
| // Arrange | ||
| ChatClientAgent innerAgent = CreateContextAwareAgent(); | ||
| AIAgent workflowAgent = AgentWorkflowBuilder.BuildSequential(innerAgent).AsAIAgent(); | ||
| AgentSession session = await workflowAgent.CreateSessionAsync(); | ||
|
|
||
| // Act | ||
| await workflowAgent.RunAsync(new ChatMessage(ChatRole.User, UserText), session); | ||
| JsonElement serializedSession = await workflowAgent.SerializeSessionAsync(session); | ||
|
|
||
| // Assert | ||
| WorkflowSession workflowSession = session.Should().BeOfType<WorkflowSession>().Subject; | ||
| string[] historyTexts = | ||
| [ | ||
| .. workflowSession.ChatHistoryProvider | ||
| .GetAllMessages(workflowSession) | ||
| .Select(message => message.Text) | ||
| ]; | ||
|
|
||
| historyTexts.Should().Contain(UserText); | ||
| historyTexts.Should().Contain(ContextText); | ||
| historyTexts.Should().Contain(FirstAgentResponseText); | ||
| serializedSession.GetRawText().Should().Contain(ContextText); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Ensures that AIContextProvider-injected messages are still persisted when inner chat history is pruned. | ||
| /// </summary> | ||
| [Fact] | ||
| public async Task Test_WorkflowAsAgent_SerializesAIContextProviderRequestMessagesWhenInnerHistoryIsPrunedAsync() | ||
| { | ||
| // Arrange | ||
| RetainingChatHistoryProvider chatHistoryProvider = new(maxStoredMessages: 2); | ||
| chatHistoryProvider.Add(new ChatMessage(ChatRole.User, "Previous question") { MessageId = "previous-user" }); | ||
| chatHistoryProvider.Add(new ChatMessage(ChatRole.Assistant, "Previous answer") { MessageId = "previous-assistant" }); | ||
| ChatClientAgent innerAgent = CreateContextAwareAgent(chatHistoryProvider); | ||
| AIAgent workflowAgent = AgentWorkflowBuilder.BuildSequential(innerAgent).AsAIAgent(); | ||
| AgentSession session = await workflowAgent.CreateSessionAsync(); | ||
|
|
||
| // Act | ||
| await workflowAgent.RunAsync(new ChatMessage(ChatRole.User, UserText), session); | ||
|
|
||
| // Assert | ||
| WorkflowSession workflowSession = session.Should().BeOfType<WorkflowSession>().Subject; | ||
| workflowSession.ChatHistoryProvider | ||
| .GetAllMessages(workflowSession) | ||
| .Select(message => message.Text) | ||
| .Should() | ||
| .Contain(ContextText); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Ensures that AIContextProvider-injected messages are saved to workflow history | ||
| /// but are NOT forwarded as part of the input to subsequent agents in the workflow. | ||
| /// </summary> | ||
| [Fact] | ||
| public async Task Test_WorkflowAsAgent_DoesNotForwardAIContextProviderRequestMessagesToDownstreamAgentAsync() | ||
| { | ||
| // Arrange | ||
| ChatClientAgent innerAgent = CreateContextAwareAgent(); | ||
| RecordingEchoAgent downstreamAgent = new(id: "downstream", name: "downstream", prefix: "downstream:"); | ||
| AIAgent workflowAgent = AgentWorkflowBuilder.BuildSequential(innerAgent, downstreamAgent).AsAIAgent(); | ||
|
|
||
| // Act | ||
| await workflowAgent.RunAsync(new ChatMessage(ChatRole.User, UserText), await workflowAgent.CreateSessionAsync()); | ||
|
|
||
| // Assert | ||
| downstreamAgent.RecordedInputs.Should().ContainSingle(); | ||
| string[] downstreamTexts = [.. downstreamAgent.RecordedInputs[0].Select(message => message.Text)]; | ||
| downstreamTexts.Should().Contain(FirstAgentResponseText); | ||
| downstreamTexts.Should().NotContain(ContextText); | ||
| } | ||
|
|
||
| /// <summary>Builds an agent whose IChatClient always replies with <see cref="FirstAgentResponseText"/>, prepopulated with a <see cref="StaticAIContextProvider"/>.</summary> | ||
| private static ChatClientAgent CreateContextAwareAgent(ChatHistoryProvider? chatHistoryProvider = null) | ||
| { | ||
| return new ChatClientAgent( | ||
| new StubChatClient(_ => new ChatResponse([new ChatMessage(ChatRole.Assistant, FirstAgentResponseText)])), | ||
| new ChatClientAgentOptions | ||
| { | ||
| Name = "inner", | ||
| ChatHistoryProvider = chatHistoryProvider, | ||
| AIContextProviders = [new StaticAIContextProvider(ContextText)] | ||
| }); | ||
| } | ||
|
|
||
| /// <summary>Always injects a single System message containing the configured text.</summary> | ||
| private sealed class StaticAIContextProvider(string text) : AIContextProvider | ||
| { | ||
| protected override ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default) | ||
| { | ||
| return new(new AIContext | ||
| { | ||
| Messages = [new ChatMessage(ChatRole.System, text)] | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| private sealed class RetainingChatHistoryProvider(int maxStoredMessages) : ChatHistoryProvider | ||
| { | ||
| private readonly List<ChatMessage> _messages = []; | ||
|
|
||
| public void Add(ChatMessage message) | ||
| { | ||
| this._messages.Add(message); | ||
| } | ||
|
|
||
| protected override ValueTask<IEnumerable<ChatMessage>> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default) | ||
| { | ||
| return new(this._messages.Concat(context.RequestMessages)); | ||
| } | ||
|
|
||
| protected override ValueTask StoreChatHistoryAsync(InvokedContext context, CancellationToken cancellationToken = default) | ||
| { | ||
| this._messages.AddRange(context.RequestMessages); | ||
| if (context.ResponseMessages is not null) | ||
| { | ||
| this._messages.AddRange(context.ResponseMessages); | ||
| } | ||
|
|
||
| if (this._messages.Count > maxStoredMessages) | ||
| { | ||
| this._messages.RemoveRange(0, this._messages.Count - maxStoredMessages); | ||
| } | ||
|
|
||
| return default; | ||
| } | ||
| } | ||
|
|
||
| /// <summary>Test double for <see cref="IChatClient"/> that returns deterministic responses via the supplied factory.</summary> | ||
| private sealed class StubChatClient(Func<IEnumerable<ChatMessage>, ChatResponse> responseFactory) : IChatClient | ||
| { | ||
| public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default) | ||
| => Task.FromResult(responseFactory(messages)); | ||
|
|
||
| public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync( | ||
| IEnumerable<ChatMessage> messages, | ||
| ChatOptions? options = null, | ||
| [EnumeratorCancellation] CancellationToken cancellationToken = default) | ||
| { | ||
| ChatResponse response = await this.GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); | ||
| foreach (ChatResponseUpdate update in response.ToChatResponseUpdates()) | ||
| { | ||
| yield return update; | ||
| } | ||
| } | ||
|
|
||
| public object? GetService(Type serviceType, object? serviceKey = null) => null; | ||
|
|
||
| public void Dispose() | ||
| { | ||
| } | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.