diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs index 6a51ce5805..bf5be5310f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs @@ -42,14 +42,11 @@ internal sealed class CopyConversationMessagesExecutor(CopyConversationMessages private IEnumerable? GetInputMessages() { - DataValue? messages = null; + Throw.IfNull(this.Model.Messages, $"{nameof(this.Model)}.{nameof(this.Model.Messages)}"); - if (this.Model.Messages is not null) - { - EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Messages); - messages = expressionResult.Value; - } + EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Messages); + DataValue messages = expressionResult.Value; - return messages?.ToChatMessages(); + return messages.ToChatMessages(); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CopyConversationMessagesExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CopyConversationMessagesExecutorTest.cs new file mode 100644 index 0000000000..cb818fec15 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/CopyConversationMessagesExecutorTest.cs @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Workflows.Declarative.Extensions; +using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; +using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; +using Microsoft.Agents.ObjectModel; +using Microsoft.Extensions.AI; +using Microsoft.PowerFx.Types; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; + +/// +/// Tests for . +/// +public sealed class CopyConversationMessagesExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output) +{ + [Fact] + public async Task CopyMessagesWithSingleStringMessageAsync() + { + // Arrange, Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(CopyMessagesWithSingleStringMessageAsync), + conversationId: "TestConversationId", + messages: ValueExpression.Literal(StringDataValue.Create("Hello, how can I help you?")), + expectedMessageCount: 1); + } + + [Fact] + public async Task CopyMessagesWithSingleRecordMessageAsync() + { + // Arrange + ChatMessage testMessage = new(ChatRole.User, "Test message content"); + DataValue messageDataValue = testMessage.ToRecord().ToDataValue(); + Assert.IsType(messageDataValue); + RecordDataValue messageRecord = (RecordDataValue)messageDataValue; + + // Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(CopyMessagesWithSingleRecordMessageAsync), + conversationId: "TestConversationId", + messages: ValueExpression.Literal(messageRecord), + expectedMessageCount: 1); + } + + [Fact] + public async Task CopyMessagesWithMultipleMessagesAsync() + { + // Arrange + List testMessages = + [ + new ChatMessage(ChatRole.User, "First message"), + new ChatMessage(ChatRole.Assistant, "Second message"), + new ChatMessage(ChatRole.User, "Third message") + ]; + DataValue messagesDataValue = testMessages.ToTable().ToDataValue(); + Assert.IsType(messagesDataValue); + TableDataValue messagesTable = (TableDataValue)messagesDataValue; + + // Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(CopyMessagesWithMultipleMessagesAsync), + conversationId: "TestConversationId", + messages: ValueExpression.Literal(messagesTable), + expectedMessageCount: 3); + } + + [Fact] + public async Task CopyMessagesWithVariableExpressionAsync() + { + // Arrange + List testMessages = + [ + new ChatMessage(ChatRole.User, "Message from variable") + ]; + TableValue messagesTable = testMessages.ToTable(); + this.State.Set("SourceMessages", messagesTable); + + // Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(CopyMessagesWithVariableExpressionAsync), + conversationId: "TestConversationId", + messages: ValueExpression.Variable(PropertyPath.TopicVariable("SourceMessages")), + expectedMessageCount: 1); + } + + [Fact] + public async Task CopyMessagesToWorkflowConversationAsync() + { + // Arrange + this.State.Set(SystemScope.Names.ConversationId, FormulaValue.New("WorkflowConversationId"), VariableScopeNames.System); + + List testMessages = + [ + new ChatMessage(ChatRole.User, "Message to workflow conversation") + ]; + DataValue messagesDataValue = testMessages.ToTable().ToDataValue(); + Assert.IsType(messagesDataValue); + TableDataValue messagesTable = (TableDataValue)messagesDataValue; + + // Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(CopyMessagesToWorkflowConversationAsync), + conversationId: "WorkflowConversationId", + messages: ValueExpression.Literal(messagesTable), + expectedMessageCount: 1, + expectWorkflowEvent: true); + } + + [Fact] + public async Task CopyMessagesToNonWorkflowConversationAsync() + { + // Arrange + this.State.Set(SystemScope.Names.ConversationId, FormulaValue.New("WorkflowConversationId"), VariableScopeNames.System); + + List testMessages = + [ + new ChatMessage(ChatRole.User, "Message to non-workflow conversation") + ]; + DataValue messagesDataValue = testMessages.ToTable().ToDataValue(); + Assert.IsType(messagesDataValue); + TableDataValue messagesTable = (TableDataValue)messagesDataValue; + + // Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(CopyMessagesToNonWorkflowConversationAsync), + conversationId: "DifferentConversationId", + messages: ValueExpression.Literal(messagesTable), + expectedMessageCount: 1, + expectWorkflowEvent: false); + } + + [Fact] + public async Task CopyMessagesWithBlankDataValueAsync() + { + // Arrange, Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(CopyMessagesWithBlankDataValueAsync), + conversationId: "TestConversationId", + messages: ValueExpression.Literal(DataValue.Blank()), + expectedMessageCount: 0); + } + + private async Task ExecuteTestAsync( + string displayName, + string conversationId, + ValueExpression messages, + int expectedMessageCount, + bool expectWorkflowEvent = false) + { + // Arrange + MockAgentProvider mockAgentProvider = new(); + mockAgentProvider.TestMessages.Clear(); + + CopyConversationMessages model = this.CreateModel( + this.FormatDisplayName(displayName), + conversationId, + messages); + + CopyConversationMessagesExecutor action = new(model, mockAgentProvider.Object, this.State); + + // Act + WorkflowEvent[] events = await this.ExecuteAsync(action); + + // Assert + Assert.Equal(expectedMessageCount, mockAgentProvider.TestMessages.Count); + VerifyModel(model, action); + + AgentResponseEvent[] responseEvents = events.OfType().ToArray(); + if (expectWorkflowEvent && expectedMessageCount > 0) + { + Assert.NotEmpty(responseEvents); + AgentResponseEvent responseEvent = responseEvents.First(); + Assert.Equal(action.Id, responseEvent.ExecutorId); + Assert.NotNull(responseEvent.Response); + Assert.Equal(expectedMessageCount, responseEvent.Response.Messages.Count); + } + else + { + Assert.Empty(responseEvents); + } + } + + private CopyConversationMessages CreateModel( + string displayName, + string conversationId, + ValueExpression messages) + { + CopyConversationMessages.Builder actionBuilder = new() + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(displayName), + ConversationId = StringExpression.Literal(conversationId), + Messages = messages + }; + + return AssignParent(actionBuilder); + } +}