diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs index e1514f2240..30c887ffe2 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs @@ -17,7 +17,9 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { + Throw.IfNull(this.Model.Message); Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); + string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value; bool isWorkflowConversation = context.IsWorkflowConversation(conversationId, out string? _); @@ -26,7 +28,7 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode // Capture the created message, which includes the assigned ID. newMessage = await agentProvider.CreateMessageAsync(conversationId, newMessage, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(this.Model.Message?.Path, newMessage.ToRecord(), context).ConfigureAwait(false); + await this.AssignAsync(this.Model.Message.Path, newMessage.ToRecord(), context).ConfigureAwait(false); if (isWorkflowConversation) { diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs index 834497bbce..a0171488e0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs @@ -23,7 +23,7 @@ internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, Workflo VariablesToClear.ConversationScopedVariables => WorkflowFormulaState.DefaultScopeName, VariablesToClear.ConversationHistory => null, VariablesToClear.UserScopedVariables => null, - _ => null + _ => null, }; if (scope is not null) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CreateConversationExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CreateConversationExecutor.cs index 5b1459d323..7bfaed1ab0 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CreateConversationExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/CreateConversationExecutor.cs @@ -7,6 +7,7 @@ using Microsoft.Agents.AI.Workflows.Declarative.Interpreter; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; @@ -15,8 +16,10 @@ internal sealed class CreateConversationExecutor(CreateConversation model, Workf { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { + Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); + string conversationId = await agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false); - await this.AssignAsync(this.Model.ConversationId?.Path, FormulaValue.New(conversationId), context).ConfigureAwait(false); + await this.AssignAsync(this.Model.ConversationId.Path, FormulaValue.New(conversationId), context).ConfigureAwait(false); await context.QueueConversationUpdateAsync(conversationId, cancellationToken).ConfigureAwait(false); return default; diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs index fff160c97b..b06a5ebd36 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs @@ -18,12 +18,12 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { - PropertyPath variablePath = Throw.IfNull(this.Model.ItemsVariable?.Path, $"{nameof(this.Model)}.{nameof(this.Model.ItemsVariable)}"); + Throw.IfNull(this.Model.ItemsVariable, $"{nameof(this.Model)}.{nameof(this.Model.ItemsVariable)}"); - FormulaValue table = context.ReadState(variablePath); + FormulaValue table = context.ReadState(this.Model.ItemsVariable); if (table is not TableValue tableValue) { - throw this.Exception($"Require '{variablePath}' to be a table, not: '{table.GetType().Name}'."); + throw this.Exception($"Require '{this.Model.ItemsVariable.Path}' to be a table, not: '{table.GetType().Name}'."); } EditTableOperation? changeType = this.Model.ChangeType; @@ -33,12 +33,12 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat EvaluationResult expressionResult = this.Evaluator.GetValue(addItemValue); RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), expressionResult.Value.ToFormula()); await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(variablePath, newRecord, context).ConfigureAwait(false); + await this.AssignAsync(this.Model.ItemsVariable, newRecord, context).ConfigureAwait(false); } else if (changeType is ClearItemsOperation) { await tableValue.ClearAsync(cancellationToken).ConfigureAwait(false); - await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false); + await this.AssignAsync(this.Model.ItemsVariable, FormulaValue.NewBlank(), context).ConfigureAwait(false); } else if (changeType is RemoveItemOperation removeItemOperation) { @@ -46,8 +46,8 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat EvaluationResult expressionResult = this.Evaluator.GetValue(removeItemValue); if (expressionResult.Value.ToFormula() is TableValue removeItemTable) { - await tableValue.RemoveAsync(removeItemTable?.Rows.Select(row => row.Value), all: true, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false); + await tableValue.RemoveAsync(removeItemTable.Rows.Select(row => row.Value), all: true, cancellationToken).ConfigureAwait(false); + await this.AssignAsync(this.Model.ItemsVariable, FormulaValue.NewBlank(), context).ConfigureAwait(false); } } else if (changeType is TakeLastItemOperation) @@ -56,7 +56,7 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat if (lastRow is not null) { await tableValue.RemoveAsync([lastRow], all: true, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(variablePath, lastRow, context).ConfigureAwait(false); + await this.AssignAsync(this.Model.ItemsVariable, lastRow, context).ConfigureAwait(false); } } else if (changeType is TakeFirstItemOperation) @@ -65,7 +65,7 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat if (firstRow is not null) { await tableValue.RemoveAsync([firstRow], all: true, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(variablePath, firstRow, context).ConfigureAwait(false); + await this.AssignAsync(this.Model.ItemsVariable, firstRow, context).ConfigureAwait(false); } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs index fa201527eb..57fe319aaf 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs @@ -19,24 +19,18 @@ internal sealed class ParseValueExecutor(ParseValue model, WorkflowFormulaState { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { - PropertyPath variablePath = Throw.IfNull(this.Model.Variable?.Path, $"{nameof(this.Model)}.{nameof(model.Variable)}"); + Throw.IfNull(this.Model.ValueType, $"{nameof(this.Model)}.{nameof(model.ValueType)}"); + Throw.IfNull(this.Model.Variable, $"{nameof(this.Model)}.{nameof(model.Variable)}"); ValueExpression valueExpression = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}"); EvaluationResult expressionResult = this.Evaluator.GetValue(valueExpression); FormulaValue parsedValue; - if (this.Model.ValueType is not null) - { - VariableType targetType = new(this.Model.ValueType); - object? parsedResult = expressionResult.Value.ToObject().ConvertType(targetType); - parsedValue = parsedResult.ToFormula(); - } - else - { - parsedValue = expressionResult.Value.ToFormula(); - } + VariableType targetType = new(this.Model.ValueType); + object? parsedResult = expressionResult.Value.ToObject().ConvertType(targetType); + parsedValue = parsedResult.ToFormula(); - await this.AssignAsync(variablePath, parsedValue, context).ConfigureAwait(false); + await this.AssignAsync(this.Model.Variable.Path, parsedValue, context).ConfigureAwait(false); return default; } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs index a2b9ee22af..4c6b4e340b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs @@ -17,6 +17,7 @@ internal sealed class ResetVariableExecutor(ResetVariable model, WorkflowFormula protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { Throw.IfNull(this.Model.Variable, $"{nameof(this.Model)}.{nameof(model.Variable)}"); + await context.QueueStateResetAsync(this.Model.Variable, cancellationToken).ConfigureAwait(false); Debug.WriteLine( $""" diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs index 466e3f2ff4..5ce3869ed1 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs @@ -16,13 +16,15 @@ internal sealed class RetrieveConversationMessageExecutor(RetrieveConversationMe { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { + Throw.IfNull(this.Model.Message); Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); + string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value; string messageId = this.Evaluator.GetValue(Throw.IfNull(this.Model.MessageId, $"{nameof(this.Model)}.{nameof(this.Model.MessageId)}")).Value; ChatMessage message = await agentProvider.GetMessageAsync(conversationId, messageId, cancellationToken).ConfigureAwait(false); - await this.AssignAsync(this.Model.Message?.Path, message.ToRecord(), context).ConfigureAwait(false); + await this.AssignAsync(this.Model.Message.Path, message.ToRecord(), context).ConfigureAwait(false); return default; } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs index 8b41fd451b..96c3ceee1a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs @@ -18,11 +18,13 @@ internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationM { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { + Throw.IfNull(this.Model.Messages); Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); + string conversationId = this.Evaluator.GetValue(this.Model.ConversationId).Value; List messages = []; - await foreach (var m in agentProvider.GetMessagesAsync( + await foreach (ChatMessage message in agentProvider.GetMessagesAsync( conversationId, limit: this.GetLimit(), after: this.GetMessage(this.Model.MessageAfter), @@ -30,21 +32,16 @@ internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationM newestFirst: this.IsDescending(), cancellationToken).ConfigureAwait(false)) { - messages.Add(m); + messages.Add(message); } - await this.AssignAsync(this.Model.Messages?.Path, messages.ToTable(), context).ConfigureAwait(false); + await this.AssignAsync(this.Model.Messages.Path, messages.ToTable(), context).ConfigureAwait(false); return default; } private int? GetLimit() { - if (this.Model.Limit is null) - { - return null; - } - long limit = this.Evaluator.GetValue(this.Model.Limit).Value; return Convert.ToInt32(Math.Min(limit, 100)); } @@ -61,11 +58,6 @@ internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationM private bool IsDescending() { - if (this.Model.SortOrder is null) - { - return false; - } - AgentMessageSortOrderWrapper sortOrderWrapper = this.Evaluator.GetValue(this.Model.SortOrder).Value; return sortOrderWrapper.Value == AgentMessageSortOrder.NewestFirst; diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs index 44cd9baf16..37b8d43e8a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs @@ -7,6 +7,7 @@ using Microsoft.Agents.AI.Workflows.Declarative.Interpreter; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.PowerFx.Types; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; @@ -15,16 +16,12 @@ internal sealed class SetTextVariableExecutor(SetTextVariable model, WorkflowFor { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { - if (this.Model.Value is null) - { - await this.AssignAsync(this.Model.Variable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false); - } - else - { - FormulaValue expressionResult = FormulaValue.New(this.Engine.Format(this.Model.Value)); + Throw.IfNull(this.Model.Variable); + Throw.IfNull(this.Model.Value); - await this.AssignAsync(this.Model.Variable?.Path, expressionResult, context).ConfigureAwait(false); - } + FormulaValue expressionResult = FormulaValue.New(this.Engine.Format(this.Model.Value)); + + await this.AssignAsync(this.Model.Variable.Path, expressionResult, context).ConfigureAwait(false); return default; } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs index 64d01de0eb..6fd4002df5 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs @@ -7,7 +7,7 @@ using Microsoft.Agents.AI.Workflows.Declarative.Interpreter; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.Agents.ObjectModel.Abstractions; -using Microsoft.PowerFx.Types; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; @@ -16,16 +16,12 @@ internal sealed class SetVariableExecutor(SetVariable model, WorkflowFormulaStat { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { - if (this.Model.Value is null) - { - await this.AssignAsync(this.Model.Variable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false); - } - else - { - EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Value); + Throw.IfNull(this.Model.Variable); + Throw.IfNull(this.Model.Value); - await this.AssignAsync(this.Model.Variable?.Path, expressionResult.Value.ToFormula(), context).ConfigureAwait(false); - } + EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Value); + + await this.AssignAsync(this.Model.Variable.Path, expressionResult.Value.ToFormula(), context).ConfigureAwait(false); return default; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/MockAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/MockAgentProvider.cs index 5a55dd297a..1c496c3936 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/MockAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/MockAgentProvider.cs @@ -17,7 +17,7 @@ internal sealed class MockAgentProvider : Mock { public IList ExistingConversationIds { get; } = []; - public List? TestMessages { get; set; } + public List TestMessages { get; set; } = []; public MockAgentProvider() { @@ -45,7 +45,7 @@ internal sealed class MockAgentProvider : Mock It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.FromResult(testMessages.First())); + .Returns((conversationId, message, cancellationToken) => Task.FromResult(this.CaptureChatMessage(message))); } private string CreateConversationId() @@ -56,6 +56,13 @@ internal sealed class MockAgentProvider : Mock return newConversationId; } + private ChatMessage CaptureChatMessage(ChatMessage message) + { + this.TestMessages.Add(message); + + return message; + } + private List CreateMessages() { // Create test messages diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs index b5cdeadaeb..a7f2ba48f6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/AddConversationMessageExecutorTest.cs @@ -1,11 +1,14 @@ // 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; @@ -28,20 +31,64 @@ public sealed class AddConversationMessageExecutorTest(ITestOutputHelper output) messageText: $"Hello from {role}"); } + [Theory] + [InlineData(AgentMessageRole.User)] + [InlineData(AgentMessageRole.Agent)] + public async Task AddMessageToWorkflowAsync(AgentMessageRole role) + { + // Arrange + this.State.Set(SystemScope.Names.ConversationId, FormulaValue.New("WorkflowConversationId"), VariableScopeNames.System); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(AddMessageToWorkflowAsync), + variableName: "TestMessage", + role: AgentMessageRoleWrapper.Get(role), + conversationId: "WorkflowConversationId", + messageText: $"Hello from {role}"); + } + + [Theory] + [InlineData(AgentMessageRole.User)] + [InlineData(AgentMessageRole.Agent)] + public async Task AddMessageWithMetadataAsync(AgentMessageRole role) + { + // Arrange + Dictionary metadataValues = + new() + { + ["Key1"] = "Value1", + ["Key2"] = "Value2", + }; + RecordDataValue metadataRecord = metadataValues.ToRecordValue(); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(AddMessageWithMetadataAsync), + variableName: "TestMessage", + role: AgentMessageRoleWrapper.Get(role), + messageText: $"Hello from {role}", + metadata: metadataRecord); + } + private async Task ExecuteTestAsync( string displayName, string variableName, AgentMessageRoleWrapper role, - string messageText) + string messageText, + string? conversationId = null, + RecordDataValue? metadata = null) { // Arrange MockAgentProvider mockAgentProvider = new(); - AddConversationMessage model = this.CreateModel( - this.FormatDisplayName(displayName), - FormatVariablePath(variableName), - "TestConversationId", - role, - messageText); + AddConversationMessage model = + this.CreateModel( + this.FormatDisplayName(displayName), + FormatVariablePath(variableName), + conversationId ?? "TestConversationId", + role, + messageText, + metadata); AddConversationMessageExecutor action = new(model, mockAgentProvider.Object, this.State); @@ -49,10 +96,15 @@ public sealed class AddConversationMessageExecutorTest(ITestOutputHelper output) await this.ExecuteAsync(action); // Assert - ChatMessage? testMessage = mockAgentProvider.TestMessages?.FirstOrDefault(); + ChatMessage? testMessage = mockAgentProvider.TestMessages?.LastOrDefault(); Assert.NotNull(testMessage); VerifyModel(model, action); this.VerifyState(variableName, testMessage.ToRecord()); + if (metadata is not null) + { + Assert.NotNull(testMessage.AdditionalProperties); + Assert.NotEmpty(testMessage.AdditionalProperties); + } } private AddConversationMessage CreateModel( @@ -60,8 +112,15 @@ public sealed class AddConversationMessageExecutorTest(ITestOutputHelper output) string messageVariable, string conversationId, AgentMessageRoleWrapper role, - string messageText) + string messageText, + RecordDataValue? metadata) { + ObjectExpression.Builder? metadataExpression = null; + if (metadata is not null) + { + metadataExpression = ObjectExpression.Literal(metadata).ToBuilder(); + } + AddConversationMessage.Builder actionBuilder = new() { @@ -70,6 +129,7 @@ public sealed class AddConversationMessageExecutorTest(ITestOutputHelper output) Message = PropertyPath.Create(messageVariable), ConversationId = StringExpression.Literal(conversationId), Role = role, + Metadata = metadataExpression, }; actionBuilder.Content.Add(new AddConversationMessageContent.Builder diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs index 5ccea000e1..70e4ac0a02 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs @@ -13,47 +13,91 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; /// public sealed class ClearAllVariablesExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output) { + [Fact] + public async Task ClearGlobalScopeAsync() + { + // Arrange + this.State.Set("GlobalVar", FormulaValue.New("Old value"), VariableScopeNames.Global); + + // Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ClearGlobalScopeAsync)), + VariablesToClear.AllGlobalVariables, + "GlobalVar", + VariableScopeNames.Global); + } + [Fact] public async Task ClearWorkflowScopeAsync() { // Arrange - this.State.Set("NoVar", FormulaValue.New("Old value")); + this.State.Set("LocalVar", FormulaValue.New("Old value")); + + // Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ClearWorkflowScopeAsync)), + VariablesToClear.ConversationScopedVariables, + "LocalVar"); + } + + [Fact] + public async Task ClearUserScopeAsync() + { + // Arrange + this.State.Set("LocalVar", FormulaValue.New("Old value")); + + // Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ClearUserScopeAsync)), + VariablesToClear.UserScopedVariables, + "LocalVar", + expectedValue: FormulaValue.New("Old value")); + } + + [Fact] + public async Task ClearWorkflowHistoryAsync() + { + // Arrange + this.State.Set("LocalVar", FormulaValue.New("Old value")); + + // Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ClearWorkflowHistoryAsync)), + VariablesToClear.ConversationHistory, + "LocalVar", + expectedValue: FormulaValue.New("Old value")); + } + + private async Task ExecuteTestAsync( + string displayName, + VariablesToClear scope, + string variableName, + string variableScope = VariableScopeNames.Local, + FormulaValue? expectedValue = null) + { + // Arrange + ClearAllVariables model = this.CreateModel( + this.FormatDisplayName(displayName), + scope); + + ClearAllVariablesExecutor action = new(model, this.State); + this.State.Bind(); - ClearAllVariables model = - this.CreateModel( - this.FormatDisplayName(nameof(ClearWorkflowScopeAsync)), - VariablesToClear.ConversationScopedVariables); - // Act - ClearAllVariablesExecutor action = new(model, this.State); await this.ExecuteAsync(action); // Assert VerifyModel(model, action); this.VerifyUndefined("NoVar"); - } - - [Fact] - public async Task ClearUndefinedScopeAsync() - { - // Arrange - this.State.Set("NoVar", FormulaValue.New("Old value")); - this.State.Bind(); - - // Arrange - ClearAllVariables model = - this.CreateModel( - this.FormatDisplayName(nameof(ClearUndefinedScopeAsync)), - VariablesToClear.UserScopedVariables); - - // Act - ClearAllVariablesExecutor action = new(model, this.State); - await this.ExecuteAsync(action); - - // Assert - VerifyModel(model, action); - this.VerifyState("NoVar", FormulaValue.New("Old value")); + if (expectedValue is null) + { + this.VerifyUndefined(variableName, variableScope); + } + else + { + this.VerifyState(variableName, variableScope, expectedValue); + } } private ClearAllVariables CreateModel(string displayName, VariablesToClear variableTarget) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs new file mode 100644 index 0000000000..5eb723ae0e --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/EditTableV2ExecutorTest.cs @@ -0,0 +1,345 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; +using Microsoft.Agents.ObjectModel; +using Microsoft.PowerFx.Types; +using Xunit.Abstractions; + +namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel; + +/// +/// Tests for . +/// +public sealed class EditTableV2ExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output) +{ + [Fact] + public void InvalidModelNullItemsVariable() + { + // Arrange + EditTableV2 model = new EditTableV2.Builder + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(nameof(InvalidModelNullItemsVariable)), + ItemsVariable = null, + ChangeType = new AddItemOperation.Builder + { + Value = new ValueExpression.Builder(ValueExpression.Literal(new StringDataValue("test"))) + }.Build() + }.Build(); + + // Act, Assert + DeclarativeModelException exception = Assert.Throws(() => new EditTableV2Executor(model, this.State)); + Assert.Contains("required", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task InvalidModelVariableNotTableAsync() + { + // Arrange + this.State.Set("NotATable", FormulaValue.New("I am a string")); + + EditTableV2 model = this.CreateModel( + nameof(InvalidModelVariableNotTableAsync), + "NotATable", + new AddItemOperation.Builder + { + Value = new ValueExpression.Builder(ValueExpression.Literal(new StringDataValue("test"))) + }.Build()); + + EditTableV2Executor action = new(model, this.State); + + // Act & Assert + await Assert.ThrowsAsync(async () => await this.ExecuteAsync(action)); + } + + [Fact] + public async Task InvalidModelAddItemOperationNullValueAsync() + { + // Arrange + EditTableV2 model = new EditTableV2.Builder + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(nameof(InvalidModelAddItemOperationNullValueAsync)), + ItemsVariable = PropertyPath.Create(FormatVariablePath("TestTable")), + ChangeType = new AddItemOperation.Builder + { + Value = null + }.Build() + }.Build(); + + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + TableValue tableValue = FormulaValue.NewTable(recordType); + this.State.Set("TestTable", tableValue); + + // Act, Assert + EditTableV2Executor action = new(model, this.State); + await Assert.ThrowsAsync(async () => await this.ExecuteAsync(action)); + } + + [Fact] + public async Task InvalidModelRemoveItemOperationNullValueAsync() + { + // Arrange + EditTableV2 model = new EditTableV2.Builder + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(nameof(InvalidModelRemoveItemOperationNullValueAsync)), + ItemsVariable = PropertyPath.Create(FormatVariablePath("TestTable")), + ChangeType = new RemoveItemOperation.Builder + { + Value = null + }.Build() + }.Build(); + + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + TableValue tableValue = FormulaValue.NewTable(recordType); + this.State.Set("TestTable", tableValue); + + // Act, Assert + EditTableV2Executor action = new(model, this.State); + await Assert.ThrowsAsync(async () => await this.ExecuteAsync(action)); + } + + [Fact] + public async Task RemoveItemOperationNonTableValueAsync() + { + // Arrange + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1"))); + TableValue tableValue = FormulaValue.NewTable(recordType, record1); + this.State.Set("TestTable", tableValue); + + // Set a string value instead of a table for removal + this.State.Set("RemoveItems", FormulaValue.New("NotATable")); + + EditTableV2 model = new EditTableV2.Builder + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(nameof(RemoveItemOperationNonTableValueAsync)), + ItemsVariable = PropertyPath.Create(FormatVariablePath("TestTable")), + ChangeType = new RemoveItemOperation.Builder + { + Value = new ValueExpression.Builder(ValueExpression.Variable(PropertyPath.TopicVariable("RemoveItems"))) + }.Build() + }.Build(); + + // Act + EditTableV2Executor action = new(model, this.State); + await this.ExecuteAsync(action); + + // Assert: When the remove value is not a table, no removal occurs, so the table should be unchanged + FormulaValue value = this.State.Get("TestTable"); + Assert.IsAssignableFrom(value); + TableValue resultTable = (TableValue)value; + Assert.Single(resultTable.Rows); + } + + [Fact] + public async Task AddItemOperationWithSingleFieldRecordAsync() + { + // Arrange: Create an empty table with single field + RecordType recordType = RecordType.Empty().Add("Name", FormulaType.String); + TableValue tableValue = FormulaValue.NewTable(recordType); + this.State.Set("TestTable", tableValue); + + // Arrange, Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(AddItemOperationWithSingleFieldRecordAsync), + variableName: "TestTable", + changeType: this.CreateAddItemOperation(new RecordDataValue.Builder + { + Properties = + { + ["Name"] = new StringDataValue("John") + } + }.Build()), + verifyAction: (variableName, recordValue) => + Assert.Equal("John", recordValue.GetField("Name").ToObject()) + ); + } + + [Fact] + public async Task AddItemOperationWithScalarValueAsync() + { + // Arrange: Create an empty table with single field + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + TableValue tableValue = FormulaValue.NewTable(recordType); + this.State.Set("TestTable", tableValue); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(AddItemOperationWithScalarValueAsync), + variableName: "TestTable", + changeType: this.CreateAddItemOperation(new StringDataValue("TestValue")), + verifyAction: (variableName, recordValue) => + Assert.Equal("TestValue", recordValue.GetField("Value").ToObject()) + ); + } + + [Fact] + public async Task ClearItemsOperationAsync() + { + // Arrange: Create a table with some items + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1"))); + RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2"))); + TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2); + this.State.Set("TestTable", tableValue); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(ClearItemsOperationAsync), + variableName: "TestTable", + changeType: new ClearItemsOperation.Builder().Build()); + } + + [Fact] + public async Task RemoveItemOperationAsync() + { + // Arrange: Create a table with some items + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1"))); + RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2"))); + TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2); + this.State.Set("TestTable", tableValue); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(RemoveItemOperationAsync), + variableName: "TestTable", + changeType: this.CreateRemoveItemOperation("Item1")); + } + + [Fact] + public async Task TakeLastItemOperationWithItemsAsync() + { + // Arrange: Create a table with some items + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1"))); + RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2"))); + RecordValue record3 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item3"))); + TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2, record3); + this.State.Set("TestTable", tableValue); + + // Arrange, Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(TakeLastItemOperationWithItemsAsync), + variableName: "TestTable", + changeType: new TakeLastItemOperation.Builder().Build(), + verifyAction: (variableName, recordValue) => + Assert.Equal("Item3", recordValue.GetField("Value").ToObject()) + ); + } + + [Fact] + public async Task TakeLastItemOperationEmptyTableAsync() + { + // Arrange: Create an empty table + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + TableValue tableValue = FormulaValue.NewTable(recordType); + this.State.Set("TestTable", tableValue); + + // Arrange, Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(TakeLastItemOperationEmptyTableAsync), + variableName: "TestTable", + changeType: new TakeLastItemOperation.Builder().Build()); + } + + [Fact] + public async Task TakeFirstItemOperationWithItemsAsync() + { + // Arrange: Create a table with some items + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1"))); + RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2"))); + RecordValue record3 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item3"))); + TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2, record3); + this.State.Set("TestTable", tableValue); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(TakeFirstItemOperationWithItemsAsync), + variableName: "TestTable", + changeType: new TakeFirstItemOperation.Builder().Build(), + verifyAction: (variableName, recordValue) => + Assert.Equal("Item1", recordValue.GetField("Value").ToObject()) + ); + } + + [Fact] + public async Task TakeFirstItemOperationEmptyTableAsync() + { + // Arrange: Create an empty table + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + TableValue tableValue = FormulaValue.NewTable(recordType); + this.State.Set("TestTable", tableValue); + + // Act & Assert + await this.ExecuteTestAsync( + displayName: nameof(TakeFirstItemOperationEmptyTableAsync), + variableName: "TestTable", + changeType: new TakeFirstItemOperation.Builder().Build()); + } + + private async Task ExecuteTestAsync( + string displayName, + string variableName, + EditTableOperation changeType, + Action? verifyAction = null) where TValue : FormulaValue + { + // Arrange + EditTableV2 model = this.CreateModel(displayName, variableName, changeType); + + EditTableV2Executor action = new(model, this.State); + + // Act + await this.ExecuteAsync(action); + + // Assert + VerifyModel(model, action); + FormulaValue value = this.State.Get(variableName); + TValue typedValue = Assert.IsAssignableFrom(value); + verifyAction?.Invoke(variableName, typedValue); + } + + private EditTableV2 CreateModel(string displayName, string variableName, EditTableOperation changeType) + { + EditTableV2.Builder actionBuilder = new() + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(displayName), + ItemsVariable = PropertyPath.Create(FormatVariablePath(variableName)), + ChangeType = changeType + }; + + return AssignParent(actionBuilder); + } + + private AddItemOperation CreateAddItemOperation(DataValue value) + { + return new AddItemOperation.Builder + { + Value = new ValueExpression.Builder(ValueExpression.Literal(value)) + }.Build(); + } + + private RemoveItemOperation CreateRemoveItemOperation(string itemValue) + { + // Create a table with the item to remove + RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String); + RecordValue recordToRemove = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New(itemValue))); + TableValue tableToRemove = FormulaValue.NewTable(recordType, recordToRemove); + + // Store in state for expression evaluation + this.State.Set("RemoveItems", tableToRemove); + this.State.Bind(); + + return new RemoveItemOperation.Builder + { + Value = new ValueExpression.Builder(ValueExpression.Variable(PropertyPath.TopicVariable("RemoveItems"))) + }.Build(); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs index 0591066471..22854c90e8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ParseValueExecutorTest.cs @@ -25,95 +25,71 @@ public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowA {"key1", new PropertyInfo.Builder() { Type = DataType.String } }, } }; - ParseValue model = - this.CreateModel( - this.FormatDisplayName(nameof(ParseRecordAsync)), - recordBuilder, - @"{ ""key1"": ""val1"" }"); - // Act - ParseValueExecutor action = new(model, this.State); - await this.ExecuteAsync(action); - - // Assert - VerifyModel(model, action); - this.VerifyState("Target", FormulaValue.NewRecordFromFields(new NamedValue("key1", FormulaValue.New("val1")))); + // Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ParseRecordAsync)), + recordBuilder, + @"{ ""key1"": ""val1"" }", + FormulaValue.NewRecordFromFields(new NamedValue("key1", FormulaValue.New("val1")))); } [Fact] public async Task ParseTableAsync() { - // Arrange - RecordDataType.Builder recordBuilder = - new() - { - Properties = - { - {"key1", new PropertyInfo.Builder() { Type = DataType.String } }, - } - }; - ParseValue model = - this.CreateModel( - this.FormatDisplayName(nameof(ParseTableAsync)), - DataType.EmptyTable, - @"[""apple"",""banana"",""cat""]"); - - // Act - ParseValueExecutor action = new(model, this.State); - await this.ExecuteAsync(action); - - // Assert - VerifyModel(model, action); - this.VerifyState("Target", FormulaValue.NewSingleColumnTable(FormulaValue.New("apple"), FormulaValue.New("banana"), FormulaValue.New("cat"))); + // Arrange, Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ParseTableAsync)), + DataType.EmptyTable, + @"[""apple"",""banana"",""cat""]", + FormulaValue.NewSingleColumnTable(FormulaValue.New("apple"), FormulaValue.New("banana"), FormulaValue.New("cat"))); } [Fact] public async Task ParseBooleanAsync() { - // Arrange - ParseValue model = - this.CreateModel( - this.FormatDisplayName(nameof(ParseTableAsync)), - new BooleanDataType.Builder(), - "True"); - - // Act - ParseValueExecutor action = new(model, this.State); - await this.ExecuteAsync(action); - - // Assert - VerifyModel(model, action); - this.VerifyState("Target", FormulaValue.New(true)); + // Arrange, Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ParseBooleanAsync)), + new BooleanDataType.Builder(), + "True", + FormulaValue.New(true)); } [Fact] public async Task ParseNumberAsync() { - // Arrange - ParseValue model = - this.CreateModel( - this.FormatDisplayName(nameof(ParseNumberAsync)), - new NumberDataType.Builder(), - "42"); - - // Act - ParseValueExecutor action = new(model, this.State); - await this.ExecuteAsync(action); - - // Assert - VerifyModel(model, action); - this.VerifyState("Target", FormulaValue.New(42)); + // Arrange, Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ParseNumberAsync)), + new NumberDataType.Builder(), + "42", + FormulaValue.New(42)); } [Fact] public async Task ParseStringAsync() { - // Arrange + // Arrange, Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(ParseStringAsync)), + new StringDataType.Builder(), + "Hello, World!", + FormulaValue.New("Hello, World!")); + } + + private async Task ExecuteTestAsync( + string displayName, + DataType.Builder dataBuilder, + string sourceText, + FormulaValue expectedValue) + { ParseValue model = this.CreateModel( - this.FormatDisplayName(nameof(ParseStringAsync)), - new StringDataType.Builder(), - "Hello, World!"); + displayName, + "Target", + dataBuilder, + sourceText); // Act ParseValueExecutor action = new(model, this.State); @@ -121,10 +97,14 @@ public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowA // Assert VerifyModel(model, action); - this.VerifyState("Target", FormulaValue.New("Hello, World!")); + this.VerifyState("Target", expectedValue); } - private ParseValue CreateModel(string displayName, DataType.Builder typeBuilder, string sourceText) + private ParseValue CreateModel( + string displayName, + string variableName, + DataType.Builder typeBuilder, + string sourceText) { ParseValue.Builder actionBuilder = new() @@ -132,7 +112,7 @@ public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowA Id = this.CreateActionId(), DisplayName = this.FormatDisplayName(displayName), ValueType = typeBuilder, - Variable = PropertyPath.TopicVariable("Target"), + Variable = PropertyPath.TopicVariable(variableName), Value = new ValueExpression.Builder(ValueExpression.Literal(StringDataValue.Create(sourceText))), }; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs index ad77cb602e..9059780751 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ResetVariableExecutorTest.cs @@ -63,7 +63,7 @@ public sealed class ResetVariableExecutorTest(ITestOutputHelper output) : Workfl { Id = this.CreateActionId(), DisplayName = this.FormatDisplayName(displayName), - Variable = InitializablePropertyPath.Create(variablePath), + Variable = PropertyPath.Create(variablePath), }; return AssignParent(actionBuilder); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetMultipleVariablesExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetMultipleVariablesExecutorTest.cs index b25fff59b8..037ee5b94a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetMultipleVariablesExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetMultipleVariablesExecutorTest.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Linq; using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; using Microsoft.Agents.ObjectModel; @@ -32,7 +33,6 @@ public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : // Arrange this.State.Set("SourceNumber", FormulaValue.New(10)); this.State.Set("SourceText", FormulaValue.New("Hello")); - this.State.Bind(); // Act, Assert await this.ExecuteTestAsync( @@ -50,7 +50,6 @@ public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : // Arrange this.State.Set("Source1", FormulaValue.New(123)); this.State.Set("Source2", FormulaValue.New("Reference")); - this.State.Bind(); // Act, Assert await this.ExecuteTestAsync( @@ -74,6 +73,19 @@ public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : ]); } + [Fact] + public async Task SetMultipleVariablesWithNullVariableAsync() + { + // Arrange, Act, Assert + await this.ExecuteTestAsync( + displayName: nameof(SetMultipleVariablesWithNullVariableAsync), + assignments: [ + new AssignmentCase("NullVar1", null, FormulaValue.NewBlank()), + new AssignmentCase(null, new StringDataValue("NotNull"), FormulaValue.New("NotNull")), + new AssignmentCase("NullVar2", null, FormulaValue.NewBlank()) + ]); + } + [Fact] public async Task SetMultipleVariablesUpdateExistingAsync() { @@ -116,9 +128,9 @@ public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : // Assert VerifyModel(model, action); - foreach (AssignmentCase assignment in assignments) + foreach (AssignmentCase assignment in assignments.Where(a => a.VariableName != null)) { - this.VerifyState(assignment.VariableName, assignment.ExpectedValue); + this.VerifyState(assignment.VariableName!, assignment.ExpectedValue); } } @@ -140,9 +152,15 @@ public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : _ => throw new System.ArgumentException($"Unsupported value type: {assignment.ValueExpression?.GetType().Name}") }; + InitializablePropertyPath? variablePath = null; + if (assignment.VariableName != null) + { + variablePath = PropertyPath.Create(FormatVariablePath(assignment.VariableName)); + } + actionBuilder.Assignments.Add(new VariableAssignment.Builder() { - Variable = PropertyPath.Create(FormatVariablePath(assignment.VariableName)), + Variable = variablePath, Value = valueExpressionBuilder, }); } @@ -150,5 +168,5 @@ public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : return AssignParent(actionBuilder); } - private sealed record AssignmentCase(string VariableName, object? ValueExpression, FormulaValue ExpectedValue); + private sealed record AssignmentCase(string? VariableName, object? ValueExpression, FormulaValue ExpectedValue); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs index 930e5cb570..0bc850e9ce 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetTextVariableExecutorTest.cs @@ -16,20 +16,11 @@ public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : Work [Fact] public async Task SetLiteralValueAsync() { - // Arrange - SetTextVariable model = - this.CreateModel( + // Arrange, Act & Assert + await this.ExecuteTestAsync( this.FormatDisplayName(nameof(SetLiteralValueAsync)), - FormatVariablePath("TextVar"), - "Text variable value"); - - // Act - SetTextVariableExecutor action = new(model, this.State); - await this.ExecuteAsync(action); - - // Assert - VerifyModel(model, action); - this.VerifyState("TextVar", FormulaValue.New("Text variable value")); + "TextVar", + "New value"); } [Fact] @@ -38,11 +29,24 @@ public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : Work // Arrange this.State.Set("TextVar", FormulaValue.New("Old value")); + // Act & Assert + await this.ExecuteTestAsync( + this.FormatDisplayName(nameof(UpdateExistingValueAsync)), + "TextVar", + "New value"); + } + + private async Task ExecuteTestAsync( + string displayName, + string variableName, + string textValue) + { + // Arrange SetTextVariable model = this.CreateModel( - this.FormatDisplayName(nameof(UpdateExistingValueAsync)), - FormatVariablePath("TextVar"), - "New value"); + displayName, + variableName, + textValue); // Act SetTextVariableExecutor action = new(model, this.State); @@ -50,7 +54,7 @@ public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : Work // Assert VerifyModel(model, action); - this.VerifyState("TextVar", FormulaValue.New("New value")); + this.VerifyState(variableName, FormulaValue.New(textValue)); } private SetTextVariable CreateModel(string displayName, string variablePath, string textValue) @@ -60,7 +64,7 @@ public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : Work { Id = this.CreateActionId(), DisplayName = this.FormatDisplayName(displayName), - Variable = InitializablePropertyPath.Create(variablePath), + Variable = PropertyPath.Create(FormatVariablePath(variablePath)), Value = TemplateLine.Parse(textValue), }; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs index f45ccffa78..dddfab6365 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs @@ -92,7 +92,6 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow { // Arrange this.State.Set("Source", FormulaValue.New(true)); - this.State.Bind(); ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); @@ -109,7 +108,6 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow { // Arrange this.State.Set("Source", FormulaValue.New(321)); - this.State.Bind(); ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); @@ -126,7 +124,6 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow { // Arrange this.State.Set("Source", FormulaValue.New("Test")); - this.State.Bind(); ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); @@ -196,7 +193,7 @@ public sealed class SetVariableExecutorTest(ITestOutputHelper output) : Workflow { Id = this.CreateActionId(), DisplayName = this.FormatDisplayName(displayName), - Variable = InitializablePropertyPath.Create(variablePath), + Variable = PropertyPath.Create(variablePath), Value = valueExpression, }; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs index 19758aa372..f7c074f1f5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/WorkflowActionExecutorTest.cs @@ -27,6 +27,8 @@ public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : Wor internal async Task ExecuteAsync(DeclarativeActionExecutor executor) { + this.State.Bind(); + TestWorkflowExecutor workflowExecutor = new(); WorkflowBuilder workflowBuilder = new(workflowExecutor); workflowBuilder.AddEdge(workflowExecutor, executor);