From 06827e9f1a108ff767732fcea5737a386e48233c Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:08:07 -0700 Subject: [PATCH] .NET Workflows - Improvements to Declarative Framework (#838) * Closure * Verified * Update dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Sample folder * System scope fix * Fix link * Integrate Foundry SDK fix * File naming fix * Update dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeWorkflowContext.cs Co-authored-by: Tao Chen * Namespace * Optimize bind --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Tao Chen --- dotnet/Directory.Packages.props | 5 +- .../Workflows/Declarative/Program.cs | 2 +- .../AzureAgentProvider.cs | 43 ++--- .../Events/DeclarativeActionCompletedEvent.cs | 5 +- .../Events/DeclarativeActionInvokedEvent.cs | 10 +- .../Extensions/DataValueExtensions.cs | 16 +- .../Extensions/FormulaValueExtensions.cs | 2 +- .../Extensions/IWorkflowContextExtensions.cs | 25 ++- .../Extensions/PropertyPathExtensions.cs | 10 - .../Interpreter/DeclarativeActionExecutor.cs | 28 +-- .../Interpreter/DeclarativeWorkflowContext.cs | 134 ++++++++++---- .../Interpreter/WorkflowActionVisitor.cs | 2 +- .../AddConversationMessageExecutor.cs | 6 +- .../ObjectModel/ClearAllVariablesExecutor.cs | 57 ++---- .../ObjectModel/ConditionGroupExecutor.cs | 2 +- .../CopyConversationMessagesExecutor.cs | 4 +- .../ObjectModel/EditTableExecutor.cs | 6 +- .../ObjectModel/EditTableV2Executor.cs | 6 +- .../ObjectModel/ForeachExecutor.cs | 6 +- .../ObjectModel/InvokeAzureAgentExecutor.cs | 20 +- .../ObjectModel/ParseValueExecutor.cs | 2 +- .../ObjectModel/QuestionExecutor.cs | 12 +- .../ObjectModel/ResetVariableExecutor.cs | 6 +- .../RetrieveConversationMessageExecutor.cs | 4 +- .../RetrieveConversationMessagesExecutor.cs | 8 +- .../ObjectModel/SendActivityExecutor.cs | 2 +- .../SetMultipleVariablesExecutor.cs | 2 +- .../ObjectModel/SetTextVariableExecutor.cs | 9 +- .../ObjectModel/SetVariableExecutor.cs | 2 +- .../PowerFx/SystemScope.cs | 37 ++-- .../PowerFx/WorkflowFormulaState.cs | 82 +-------- .../Framework/AgentFixture.cs | 8 +- .../Workflows/SendActivity.yaml | 6 +- .../DeclarativeWorkflowTest.cs | 2 +- .../Interpreter/WorkflowModelTest.cs | 12 +- .../ClearAllVariablesExecutorTest.cs | 7 +- .../ObjectModel/SetVariableExecutorTest.cs | 6 + .../PowerFx/RecalcEngineFactoryTests.cs | 47 ----- .../PowerFx/TemplateExtensionsTests.cs | 2 + .../PowerFx/WorkflowExpressionEngineTests.cs | 2 + .../PowerFx/WorkflowFormulaStateTests.cs | 84 +++++++++ .../PowerFx/WorkflowScopesTests.cs | 173 ------------------ .../WorkflowTest.cs | 3 + .../Workflows/ClearAllVariables.yaml | 2 +- .../Workflows/Condition.yaml | 2 +- .../Workflows/ConditionElse.yaml | 2 +- 46 files changed, 368 insertions(+), 545 deletions(-) delete mode 100644 dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/PropertyPathExtensions.cs create mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowFormulaStateTests.cs delete mode 100644 dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowScopesTests.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 01f2fbe735..3e2529a0b7 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -37,11 +37,12 @@ + + + - - diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs index 940c6afddf..19c3c0321a 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs @@ -36,7 +36,7 @@ internal sealed class Program string? workflowFile = ParseWorkflowFile(args); if (workflowFile is null) { - Notify("\nUsage: DeclarativeWorkflow []"); + Notify("\nUsage: DeclarativeWorkflow []\n"); return; } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs index 5c8fd35192..b3a0d044db 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs @@ -48,10 +48,7 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p await this.GetAgentsClient().Messages.CreateMessageAsync( conversationId, role: s_roleMap[conversationMessage.Role.Value.ToUpperInvariant()], - // TODO: PersistentAgent bug blocks supporting multiple content types: - // https://github.com/Azure/azure-sdk-for-net/issues/52571 - //contentBlocks: GetContent(), - content: conversationMessage.Text, + contentBlocks: GetContent(), attachments: null, metadata: GetMetadata(), cancellationToken).ConfigureAwait(false); @@ -66,27 +63,25 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p return conversationMessage.AdditionalProperties.ToDictionary(prop => prop.Key, prop => prop.Value?.ToString() ?? string.Empty); } - // TODO: PersistentAgent bug blocks supporting multiple content types: - // https://github.com/Azure/azure-sdk-for-net/issues/52571 - //IEnumerable GetContent() - //{ - // foreach (AIContent content in conversationMessage.Contents) - // { - // MessageInputContentBlock? contentBlock = - // content switch - // { - // TextContent textContent => new MessageInputTextBlock(textContent.Text), - // HostedFileContent fileContent => new MessageInputImageFileBlock(new MessageImageFileParam(fileContent.FileId)), - // UriContent uriContent when uriContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(uriContent.Uri.ToString())), - // _ => null // Unsupported content type - // }; + IEnumerable GetContent() + { + foreach (AIContent content in conversationMessage.Contents) + { + MessageInputContentBlock? contentBlock = + content switch + { + TextContent textContent => new MessageInputTextBlock(textContent.Text), + HostedFileContent fileContent => new MessageInputImageFileBlock(new MessageImageFileParam(fileContent.FileId)), + UriContent uriContent when uriContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(uriContent.Uri.ToString())), + _ => null // Unsupported content type + }; - // if (contentBlock is not null) - // { - // yield return contentBlock; - // } - // } - //} + if (contentBlock is not null) + { + yield return contentBlock; + } + } + } } /// diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompletedEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompletedEvent.cs index 349028c96b..9638d1649f 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompletedEvent.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompletedEvent.cs @@ -8,7 +8,7 @@ namespace Microsoft.Agents.Workflows.Declarative; /// /// Event that indicates a declarative action has been invoked. /// -public sealed class DeclarativeActionInvokedEvent : WorkflowEvent +public sealed class DeclarativeActionCompletedEvent : WorkflowEvent { /// /// The declarative action id. @@ -30,11 +30,10 @@ public sealed class DeclarativeActionInvokedEvent : WorkflowEvent /// public string? PriorActionId { get; } - internal DeclarativeActionInvokedEvent(DialogAction action, string? priorActionId) : base(action) + internal DeclarativeActionCompletedEvent(DialogAction action) : base(action) { this.ActionId = action.GetId(); this.ActionType = action.GetType().Name; this.ParentActionId = action.GetParentId(); - this.PriorActionId = priorActionId; } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokedEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokedEvent.cs index 16845124b6..7d92db985a 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokedEvent.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokedEvent.cs @@ -8,7 +8,7 @@ namespace Microsoft.Agents.Workflows.Declarative; /// /// Event that indicates a declarative action has completed. /// -public sealed class DeclarativeActionCompletedEvent : WorkflowEvent +public sealed class DeclarativeActionInvokedEvent : WorkflowEvent { /// /// The declarative action identifier. @@ -25,10 +25,16 @@ public sealed class DeclarativeActionCompletedEvent : WorkflowEvent /// public string? ParentActionId { get; } - internal DeclarativeActionCompletedEvent(DialogAction action) : base(action) + /// + /// Identifier of the previous action. + /// + public string? PriorActionId { get; } + + internal DeclarativeActionInvokedEvent(DialogAction action, string? priorActionId) : base(action) { this.ActionId = action.GetId(); this.ActionType = action.GetType().Name; this.ParentActionId = action.GetParentId(); + this.PriorActionId = priorActionId; } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs index e0dab7116a..e883609141 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Dynamic; using System.Linq; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; @@ -66,7 +65,7 @@ internal static class DataValueExtensions DateTimeDataValue dateTimeValue => dateTimeValue.Value.DateTime, DateDataValue dateValue => dateValue.Value, TimeDataValue timeValue => timeValue.Value, - TableDataValue tableValue => tableValue.Values.Select(value => value.ToObject()).ToArray(), + TableDataValue tableValue => tableValue.Values.Select(value => value.ToDictionary()).ToArray(), RecordDataValue recordValue => recordValue.ToDictionary(), OptionDataValue optionValue => optionValue.Value.Value, _ => throw new DeclarativeModelException($"Unsupported {nameof(DataValue)} type: {value.GetType().Name}"), @@ -89,19 +88,6 @@ internal static class DataValueExtensions return recordType; } - public static ExpandoObject ToObject(this RecordDataValue recordDataValue) - { - ExpandoObject expandoObject = new(); - - IDictionary dictionary = expandoObject; - foreach (KeyValuePair field in recordDataValue.Properties) - { - dictionary[field.Key] = field.Value?.ToObject(); - } - - return expandoObject; - } - private static RecordType ParseRecordType(this RecordDataValue record) { RecordType recordType = RecordType.Empty(); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs index e9d393c1ff..cc4761bf07 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs @@ -146,7 +146,7 @@ internal static class FormulaValueExtensions public static RecordDataValue ToRecord(this RecordValue value) => DataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair())); - private static RecordValue ToRecord(this IDictionary value) + public static RecordValue ToRecord(this IDictionary value) { return FormulaValue.NewRecordFromFields(GetFields()); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs index dce481d5d0..b370d6bae0 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.Agents.Workflows.Declarative.Interpreter; using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows.Declarative.Extensions; @@ -20,20 +21,28 @@ internal static class IWorkflowContextExtensions public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, object? result = null, CancellationToken cancellationToken = default) => context.SendMessageAsync(new ExecutorResultMessage(id, result)); + public static ValueTask QueueStateResetAsync(this IWorkflowContext context, PropertyPath variablePath) => + context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), UnassignedValue.Instance, Throw.IfNull(variablePath.VariableScopeName)); + public static ValueTask QueueStateUpdateAsync(this IWorkflowContext context, PropertyPath variablePath, TValue? value) => context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.VariableScopeName)); - public static async Task GetStateAsync(this IWorkflowContext context, CancellationToken cancellationToken) + public static ValueTask QueueSystemUpdateAsync(this IWorkflowContext context, string key, TValue? value) => + DeclarativeContext(context).QueueSystemUpdateAsync(key, value); + + public static FormulaValue ReadState(this IWorkflowContext context, PropertyPath variablePath) => + context.ReadState(Throw.IfNull(variablePath.VariableName), Throw.IfNull(variablePath.VariableScopeName)); + + public static FormulaValue ReadState(this IWorkflowContext context, string key, string? scopeName = null) => + DeclarativeContext(context).State.Get(key, scopeName); + + private static DeclarativeWorkflowContext DeclarativeContext(IWorkflowContext context) { - if (context is DeclarativeWorkflowContext declarativeContext) + if (context is not DeclarativeWorkflowContext declarativeContext) { - return declarativeContext.State; + throw new DeclarativeActionException($"Invalid workflow context: {context.GetType().Name}."); } - WorkflowFormulaState state = new(RecalcEngineFactory.Create()); - - await state.RestoreAsync(context, cancellationToken).ConfigureAwait(false); - - return state; + return declarativeContext; } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/PropertyPathExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/PropertyPathExtensions.cs deleted file mode 100644 index 2fbc5c28ef..0000000000 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/PropertyPathExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Bot.ObjectModel; - -namespace Microsoft.Agents.Workflows.Declarative.Extensions; - -internal static class PropertyPathExtensions -{ - public static string Format(this PropertyPath path) => string.Join(".", path.Segments()); -} diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs index 44e845e63c..c870b77138 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Frozen; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -10,8 +9,8 @@ using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.PowerFx; using Microsoft.PowerFx.Types; -using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows.Declarative.Interpreter; @@ -24,13 +23,8 @@ internal abstract class DeclarativeActionExecutor(TAction model, Workfl internal abstract class DeclarativeActionExecutor : Executor { - private static readonly FrozenSet s_mutableScopes = - [ - VariableScopeNames.Topic, - VariableScopeNames.Global - ]; - private string? _parentId; + private readonly WorkflowFormulaState _state; protected DeclarativeActionExecutor(DialogAction model, WorkflowFormulaState state) : base(model.Id.Value) @@ -40,17 +34,20 @@ internal abstract class DeclarativeActionExecutor : Executor this._parentId ??= this.Model.GetParentId() ?? WorkflowActionVisitor.Steps.Root(); - internal ILogger Logger { get; set; } = NullLogger.Instance; + public RecalcEngine Engine => this._state.Engine; - protected WorkflowFormulaState State { get; } + public WorkflowExpressionEngine Evaluator => this._state.Evaluator; + + internal ILogger Logger { get; set; } = NullLogger.Instance; protected virtual bool IsDiscreteAction => true; @@ -71,7 +68,7 @@ internal abstract class DeclarativeActionExecutor : Executor protected override ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default) => - this.State.RestoreAsync(context, cancellation); + this._state.RestoreAsync(context, cancellation); protected async ValueTask AssignAsync(PropertyPath? targetPath, FormulaValue result, IWorkflowContext context) { @@ -113,11 +110,6 @@ internal abstract class DeclarativeActionExecutor : Executor ManagedScopes = + [ + VariableScopeNames.Topic, + VariableScopeNames.Global, + ]; + public DeclarativeWorkflowContext(IWorkflowContext source, WorkflowFormulaState state) { this.Source = source; @@ -24,50 +33,38 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext public ValueTask AddEventAsync(WorkflowEvent workflowEvent) => this.Source.AddEventAsync(workflowEvent); /// - public ValueTask QueueClearScopeAsync(string? scopeName = null) + public async ValueTask QueueClearScopeAsync(string? scopeName = null) { - this.State.ResetAll(scopeName); - return this.Source.QueueClearScopeAsync(scopeName); + if (scopeName is not null) + { + if (ManagedScopes.Contains(scopeName)) + { + // Copy keys to array to avoid modifying collection during enumeration. + foreach (string key in this.State.Keys(scopeName).ToArray()) + { + await this.UpdateStateAsync(key, UnassignedValue.Instance, scopeName).ConfigureAwait(false); + } + } + else + { + await this.Source.QueueClearScopeAsync(scopeName).ConfigureAwait(false); + } + + this.State.Bind(); + } } /// public async ValueTask QueueStateUpdateAsync(string key, T? value, string? scopeName = null) { - ValueTask task = value switch - { - null => QueueEmptyStateAsync(), - FormulaValue formulaValue => QueueFormulaStateAsync(formulaValue), - DataValue dataValue => QueueDataValueStateAsync(dataValue), - _ => QueueNativeStateAsync(value), - }; + await this.UpdateStateAsync(key, value, scopeName).ConfigureAwait(false); + this.State.Bind(); + } - await task.ConfigureAwait(false); - - ValueTask QueueEmptyStateAsync() - { - this.State.Set(key, FormulaValue.NewBlank(), scopeName); - return this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName); - } - - ValueTask QueueFormulaStateAsync(FormulaValue formulaValue) - { - this.State.Set(key, formulaValue, scopeName); - return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName); - } - - ValueTask QueueDataValueStateAsync(DataValue dataValue) - { - FormulaValue formulaValue = dataValue.ToFormula(); - this.State.Set(key, formulaValue, scopeName); - return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName); - } - - ValueTask QueueNativeStateAsync(object? rawValue) - { - FormulaValue formulaValue = rawValue.ToFormula(); - this.State.Set(key, formulaValue, scopeName); - return this.Source.QueueStateUpdateAsync(key, rawValue, scopeName); - } + public async ValueTask QueueSystemUpdateAsync(string key, TValue? value) + { + await this.UpdateStateAsync(key, value, VariableScopeNames.System, allowSystem: true).ConfigureAwait(false); + this.State.Bind(); } /// @@ -78,4 +75,67 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext /// public ValueTask SendMessageAsync(object message, string? targetId = null) => this.Source.SendMessageAsync(message, targetId); + + private ValueTask UpdateStateAsync(string key, T? value, string? scopeName, bool allowSystem = true) + { + bool isManagedScope = + scopeName != null && // null scope cannot be managed + (ManagedScopes.Contains(scopeName) || + (allowSystem && VariableScopeNames.System.Equals(scopeName, StringComparison.Ordinal))); + + if (!isManagedScope) + { + // Not a managed scope, just pass through. This is valid when a declarative + // workflow has been ejected to code (where DeclarativeWorkflowContext is also utilized). + return this.Source.QueueStateUpdateAsync(key, value, scopeName); + } + + return value switch + { + null => QueueEmptyStateAsync(), + UnassignedValue => QueueEmptyStateAsync(), + BlankValue => QueueEmptyStateAsync(), + FormulaValue formulaValue => QueueFormulaStateAsync(formulaValue), + DataValue dataValue => QueueDataValueStateAsync(dataValue), + _ => QueueNativeStateAsync(value), + }; + + ValueTask QueueEmptyStateAsync() + { + if (isManagedScope) + { + this.State.Set(key, FormulaValue.NewBlank(), scopeName); + } + return this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName); + } + + ValueTask QueueFormulaStateAsync(FormulaValue formulaValue) + { + if (isManagedScope) + { + this.State.Set(key, formulaValue, scopeName); + } + return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName); + } + + ValueTask QueueDataValueStateAsync(DataValue dataValue) + { + FormulaValue formulaValue = dataValue.ToFormula(); + if (isManagedScope) + { + this.State.Set(key, formulaValue, scopeName); + } + return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName); + } + + ValueTask QueueNativeStateAsync(object? rawValue) + { + FormulaValue formulaValue = rawValue.ToFormula(); + if (isManagedScope) + { + this.State.Set(key, formulaValue, scopeName); + } + return this.Source.QueueStateUpdateAsync(key, rawValue, scopeName); + } + } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs index 45a38373f8..224ccfcf40 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs @@ -197,7 +197,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor { DefaultActionExecutor continueLoopExecutor = new(item, this._workflowState); this.ContinueWith(continueLoopExecutor); - this._workflowModel.AddLink(continueLoopExecutor.Id, Steps.Post(loopExecutor.Id)); + this._workflowModel.AddLink(continueLoopExecutor.Id, ForeachExecutor.Steps.Next(loopExecutor.Id)); this.RestartAfter(continueLoopExecutor.Id, continueLoopExecutor.ParentId); } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs index a202a70cea..2cf6ffd6ac 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs @@ -18,7 +18,7 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { StringExpression conversationExpression = Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}"); - string conversationId = this.State.Evaluator.GetValue(conversationExpression).Value; + string conversationId = this.Evaluator.GetValue(conversationExpression).Value; ChatMessage newMessage = new(this.Model.Role.Value.ToChatRole(), [.. this.GetContent()]) { AdditionalProperties = this.GetMetadata() }; @@ -33,7 +33,7 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode { foreach (AddConversationMessageContent content in this.Model.Content) { - AIContent? messageContent = content.Type.Value.ToContent(this.State.Engine.Format(content.Value)); + AIContent? messageContent = content.Type.Value.ToContent(this.Engine.Format(content.Value)); if (messageContent is not null) { yield return messageContent; @@ -48,7 +48,7 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode return null; } - RecordDataValue? metadataValue = this.State.Evaluator.GetValue(this.Model.Metadata).Value; + RecordDataValue? metadataValue = this.Evaluator.GetValue(this.Model.Metadata).Value; return metadataValue.ToMetadata(); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs index d80479186e..8c494da54d 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ClearAllVariablesExecutor.cs @@ -13,46 +13,29 @@ namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, WorkflowFormulaState state) : DeclarativeActionExecutor(model, state) { - protected override ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - EvaluationResult variablesResult = this.State.Evaluator.GetValue(this.Model.Variables); + EvaluationResult variablesResult = this.Evaluator.GetValue(this.Model.Variables); - variablesResult.Value.Handle(new ScopeHandler(this.Id, this.State)); + string? scope = variablesResult.Value.Value switch + { + VariablesToClear.AllGlobalVariables => VariableScopeNames.Global, + VariablesToClear.ConversationScopedVariables => WorkflowFormulaState.DefaultScopeName, + VariablesToClear.ConversationHistory => null, + VariablesToClear.UserScopedVariables => null, + _ => null + }; + + if (scope is not null) + { + await context.QueueClearScopeAsync(scope).ConfigureAwait(false); + Debug.WriteLine( + $""" + STATE: {this.GetType().Name} [{this.Id}] + SCOPE: {scope} + """); + } return default; } - - private sealed class ScopeHandler(string executorId, WorkflowFormulaState state) : IEnumVariablesToClearHandler - { - public void HandleAllGlobalVariables() => - this.ClearAll(VariableScopeNames.Global); - - public void HandleConversationHistory() - { - // Not supported.... - } - - public void HandleConversationScopedVariables() => - this.ClearAll(WorkflowFormulaState.DefaultScopeName); - - public void HandleUnknownValue() - { - // No scope to clear for unknown values. - } - - public void HandleUserScopedVariables() - { - // Not supported.... - } - - private void ClearAll(string scope) - { - state.ResetAll(scope); - Debug.WriteLine( - $""" - STATE: {this.GetType().Name} [{executorId}] - SCOPE: {scope} - """); - } - } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs index 8321046a0d..7d1bb7c68a 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ConditionGroupExecutor.cs @@ -57,7 +57,7 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor expressionResult = this.State.Evaluator.GetValue(conditionItem.Condition); + EvaluationResult expressionResult = this.Evaluator.GetValue(conditionItem.Condition); if (expressionResult.Value) { return Steps.Item(this.Model, conditionItem); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs index b15b712e04..1628b00199 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/CopyConversationMessagesExecutor.cs @@ -17,7 +17,7 @@ internal sealed class CopyConversationMessagesExecutor(CopyConversationMessages { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - string conversationId = this.State.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value; + string conversationId = this.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value; DataValue? inputMessages = this.GetInputMessages(); if (inputMessages is not null) @@ -37,7 +37,7 @@ internal sealed class CopyConversationMessagesExecutor(CopyConversationMessages if (this.Model.Messages is not null) { - EvaluationResult expressionResult = this.State.Evaluator.GetValue(this.Model.Messages); + EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Messages); messages = expressionResult.Value; } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableExecutor.cs index ade179dbdb..90e42f9fad 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableExecutor.cs @@ -20,7 +20,7 @@ internal sealed class EditTableExecutor(EditTable model, WorkflowFormulaState st { PropertyPath variablePath = Throw.IfNull(this.Model.ItemsVariable?.Path, $"{nameof(this.Model)}.{nameof(this.Model.ItemsVariable)}"); - FormulaValue table = this.State.Get(variablePath); + FormulaValue table = context.ReadState(variablePath); if (table is not TableValue tableValue) { throw this.Exception($"Require '{variablePath}' to be a table, not: '{table.GetType().Name}'."); @@ -31,14 +31,14 @@ internal sealed class EditTableExecutor(EditTable model, WorkflowFormulaState st { case TableChangeType.Add: ValueExpression addItemValue = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}"); - EvaluationResult addResult = this.State.Evaluator.GetValue(addItemValue); + EvaluationResult addResult = this.Evaluator.GetValue(addItemValue); RecordValue newRecord = BuildRecord(tableValue.Type.ToRecord(), addResult.Value.ToFormula()); await tableValue.AppendAsync(newRecord, cancellationToken).ConfigureAwait(false); await this.AssignAsync(variablePath, newRecord, context).ConfigureAwait(false); break; case TableChangeType.Remove: ValueExpression removeItemValue = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}"); - EvaluationResult removeResult = this.State.Evaluator.GetValue(removeItemValue); + EvaluationResult removeResult = this.Evaluator.GetValue(removeItemValue); if (removeResult.Value is TableDataValue removeItemTable) { await tableValue.RemoveAsync(removeItemTable?.Values.Select(row => row.ToRecordValue()), all: true, cancellationToken).ConfigureAwait(false); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs index 3ad45a32c2..0158e2e55e 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/EditTableV2Executor.cs @@ -20,7 +20,7 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat { PropertyPath variablePath = Throw.IfNull(this.Model.ItemsVariable?.Path, $"{nameof(this.Model)}.{nameof(this.Model.ItemsVariable)}"); - FormulaValue table = this.State.Get(variablePath); + FormulaValue table = context.ReadState(variablePath); if (table is not TableValue tableValue) { throw this.Exception($"Require '{variablePath}' to be a table, not: '{table.GetType().Name}'."); @@ -30,7 +30,7 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat if (changeType is AddItemOperation addItemOperation) { ValueExpression addItemValue = Throw.IfNull(addItemOperation.Value, $"{nameof(this.Model)}.{nameof(this.Model.ChangeType)}"); - EvaluationResult expressionResult = this.State.Evaluator.GetValue(addItemValue); + 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); @@ -43,7 +43,7 @@ internal sealed class EditTableV2Executor(EditTableV2 model, WorkflowFormulaStat else if (changeType is RemoveItemOperation removeItemOperation) { ValueExpression removeItemValue = Throw.IfNull(removeItemOperation.Value, $"{nameof(this.Model)}.{nameof(this.Model.ChangeType)}"); - EvaluationResult expressionResult = this.State.Evaluator.GetValue(removeItemValue); + 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); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ForeachExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ForeachExecutor.cs index 91d2abf2f4..b31b8d44de 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ForeachExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ForeachExecutor.cs @@ -46,7 +46,7 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor } else { - EvaluationResult expressionResult = this.State.Evaluator.GetValue(this.Model.Items); + EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Items); if (expressionResult.Value is TableDataValue tableValue) { this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormula())]; @@ -83,10 +83,10 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor { try { - this.State.Reset(Throw.IfNull(this.Model.Value)); + await context.QueueStateResetAsync(Throw.IfNull(this.Model.Value)).ConfigureAwait(false); if (this.Model.Index is not null) { - this.State.Reset(this.Model.Index); + await context.QueueStateResetAsync(this.Model.Index).ConfigureAwait(false); } } finally diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs index f696cda9d5..af250cd5fe 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs @@ -11,6 +11,7 @@ using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.Extensions.AI; using Microsoft.Extensions.AI.Agents; +using Microsoft.PowerFx.Types; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; @@ -37,7 +38,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA await context.AddEventAsync(new AgentRunResponseEvent(this.Id, agentResponse)).ConfigureAwait(false); } - ChatMessage response = agentResponse.Messages.Last(); + ChatMessage response = agentResponse.Messages[agentResponse.Messages.Count - 1]; await this.AssignAsync(this.AgentOutput?.Messages?.Path, response.ToRecord(), context).ConfigureAwait(false); return default; @@ -77,7 +78,12 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA if (assignValue is not null && conversationId is null) { conversationId = assignValue; - this.State.SetConversationId(conversationId); + + RecordValue conversation = (RecordValue)context.ReadState(SystemScope.Names.Conversation, VariableScopeNames.System); + conversation.UpdateField("Id", FormulaValue.New(conversationId)); + await context.QueueSystemUpdateAsync(SystemScope.Names.Conversation, conversation).ConfigureAwait(false); + await context.QueueSystemUpdateAsync(SystemScope.Names.ConversationId, FormulaValue.New(conversationId)).ConfigureAwait(false); + await context.AddEventAsync(new ConversationUpdateEvent(conversationId)).ConfigureAwait(false); } } @@ -88,7 +94,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA DataValue? userInput = null; if (this.AgentInput?.Messages is not null) { - EvaluationResult expressionResult = this.State.Evaluator.GetValue(this.AgentInput.Messages); + EvaluationResult expressionResult = this.Evaluator.GetValue(this.AgentInput.Messages); userInput = expressionResult.Value; } @@ -102,12 +108,12 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA return null; } - EvaluationResult conversationIdResult = this.State.Evaluator.GetValue(this.Model.ConversationId); + EvaluationResult conversationIdResult = this.Evaluator.GetValue(this.Model.ConversationId); return conversationIdResult.Value.Length == 0 ? null : conversationIdResult.Value; } private string GetAgentName() => - this.State.Evaluator.GetValue( + this.Evaluator.GetValue( Throw.IfNull( this.AgentUsage.Name, $"{nameof(this.Model)}.{nameof(this.Model.Agent)}.{nameof(this.Model.Agent.Name)}")).Value; @@ -118,7 +124,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA if (this.AgentInput?.AdditionalInstructions is not null) { - additionalInstructions = this.State.Engine.Format(this.AgentInput.AdditionalInstructions); + additionalInstructions = this.Engine.Format(this.AgentInput.AdditionalInstructions); } return additionalInstructions; @@ -131,7 +137,7 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA return true; } - EvaluationResult autoSendResult = this.State.Evaluator.GetValue(this.AgentOutput.AutoSend); + EvaluationResult autoSendResult = this.Evaluator.GetValue(this.AgentOutput.AutoSend); return autoSendResult.Value; } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs index e7efc2071c..4c3e49d40e 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ParseValueExecutor.cs @@ -23,7 +23,7 @@ internal sealed class ParseValueExecutor(ParseValue model, WorkflowFormulaState PropertyPath variablePath = Throw.IfNull(this.Model.Variable?.Path, $"{nameof(this.Model)}.{nameof(model.Variable)}"); ValueExpression valueExpression = Throw.IfNull(this.Model.Value, $"{nameof(this.Model)}.{nameof(this.Model.Value)}"); - EvaluationResult expressionResult = this.State.Evaluator.GetValue(valueExpression); + EvaluationResult expressionResult = this.Evaluator.GetValue(valueExpression); FormulaValue? parsedResult = null; diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/QuestionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/QuestionExecutor.cs index 906c723a48..5da764f76a 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/QuestionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/QuestionExecutor.cs @@ -40,13 +40,13 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat await this._promptCount.WriteAsync(context, 0).ConfigureAwait(false); InitializablePropertyPath variable = Throw.IfNull(this.Model.Variable); - bool hasValue = this.State.Get(variable.Path) is BlankValue; - bool alwaysPrompt = this.State.Evaluator.GetValue(this.Model.AlwaysPrompt).Value; + bool hasValue = context.ReadState(variable.Path) is BlankValue; + bool alwaysPrompt = this.Evaluator.GetValue(this.Model.AlwaysPrompt).Value; bool proceed = !alwaysPrompt || hasValue; if (proceed) { - SkipQuestionMode mode = this.State.Evaluator.GetValue(this.Model.SkipQuestionMode).Value; + SkipQuestionMode mode = this.Evaluator.GetValue(this.Model.SkipQuestionMode).Value; proceed = mode switch { @@ -118,12 +118,12 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat private async ValueTask PromptAsync(IWorkflowContext context, CancellationToken cancellationToken) { - long repeatCount = this.State.Evaluator.GetValue(this.Model.RepeatCount).Value; + long repeatCount = this.Evaluator.GetValue(this.Model.RepeatCount).Value; int actualCount = await this._promptCount.ReadAsync(context).ConfigureAwait(false); if (actualCount >= repeatCount) { ValueExpression defaultValueExpression = Throw.IfNull(this.Model.DefaultValue); - DataValue defaultValue = this.State.Evaluator.GetValue(defaultValueExpression).Value; + DataValue defaultValue = this.Evaluator.GetValue(defaultValueExpression).Value; await this.AssignAsync(this.Model.Variable?.Path, defaultValue.ToFormula(), context).ConfigureAwait(false); string defaultValueResponse = this.FormatPrompt(this.Model.DefaultValueResponse); await context.AddEventAsync(new MessageActivityEvent(defaultValueResponse.Trim())).ConfigureAwait(false); @@ -142,6 +142,6 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat return string.Empty; } - return this.State.Engine.Format(messageActivity.Text).Trim(); + return this.Engine.Format(messageActivity.Text).Trim(); } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs index 5bbb64e23e..08c7e24138 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/ResetVariableExecutor.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.Interpreter; using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; @@ -13,11 +14,10 @@ namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; internal sealed class ResetVariableExecutor(ResetVariable model, WorkflowFormulaState state) : DeclarativeActionExecutor(model, state) { - protected override ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) + protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { Throw.IfNull(this.Model.Variable, $"{nameof(this.Model)}.{nameof(model.Variable)}"); - - this.State.Reset(this.Model.Variable); + await context.QueueStateResetAsync(this.Model.Variable).ConfigureAwait(false); Debug.WriteLine( $""" STATE: {this.GetType().Name} [{this.Id}] diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs index 782e3aaef6..a723417cba 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessageExecutor.cs @@ -16,8 +16,8 @@ internal sealed class RetrieveConversationMessageExecutor(RetrieveConversationMe { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - string conversationId = this.State.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value; - string messageId = this.State.Evaluator.GetValue(Throw.IfNull(this.Model.MessageId, $"{nameof(this.Model)}.{nameof(this.Model.MessageId)}")).Value; + string conversationId = this.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(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); diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs index 588f244f08..e5a55721ef 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/RetrieveConversationMessagesExecutor.cs @@ -18,7 +18,7 @@ internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationM { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - string conversationId = this.State.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value; + string conversationId = this.Evaluator.GetValue(Throw.IfNull(this.Model.ConversationId, $"{nameof(this.Model)}.{nameof(this.Model.ConversationId)}")).Value; ChatMessage[] messages = await agentProvider.GetMessagesAsync( conversationId, @@ -40,7 +40,7 @@ internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationM return null; } - long limit = this.State.Evaluator.GetValue(this.Model.Limit).Value; + long limit = this.Evaluator.GetValue(this.Model.Limit).Value; return Convert.ToInt32(Math.Min(limit, 100)); } @@ -51,7 +51,7 @@ internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationM return null; } - return this.State.Evaluator.GetValue(messagExpression).Value; + return this.Evaluator.GetValue(messagExpression).Value; } private bool IsDescending() @@ -61,7 +61,7 @@ internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationM return false; } - AgentMessageSortOrderWrapper sortOrderWrapper = this.State.Evaluator.GetValue(this.Model.SortOrder).Value; + AgentMessageSortOrderWrapper sortOrderWrapper = this.Evaluator.GetValue(this.Model.SortOrder).Value; return sortOrderWrapper.Value == AgentMessageSortOrder.NewestFirst; } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SendActivityExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SendActivityExecutor.cs index 8c78036069..12831a1588 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SendActivityExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SendActivityExecutor.cs @@ -16,7 +16,7 @@ internal sealed class SendActivityExecutor(SendActivity model, WorkflowFormulaSt { if (this.Model.Activity is MessageActivityTemplate messageActivity) { - string activityText = this.State.Engine.Format(messageActivity.Text).Trim(); + string activityText = this.Engine.Format(messageActivity.Text).Trim(); await context.AddEventAsync(new MessageActivityEvent(activityText.Trim())).ConfigureAwait(false); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetMultipleVariablesExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetMultipleVariablesExecutor.cs index 3e77c6d76b..e59f33edaa 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetMultipleVariablesExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetMultipleVariablesExecutor.cs @@ -29,7 +29,7 @@ internal sealed class SetMultipleVariablesExecutor(SetMultipleVariables model, W } else { - EvaluationResult expressionResult = this.State.Evaluator.GetValue(assignment.Value); + EvaluationResult expressionResult = this.Evaluator.GetValue(assignment.Value); await this.AssignAsync(assignment.Variable, expressionResult.Value.ToFormula(), context).ConfigureAwait(false); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs index 6778c8074d..980d27c062 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetTextVariableExecutor.cs @@ -7,7 +7,6 @@ using Microsoft.Agents.Workflows.Declarative.Interpreter; using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx.Types; -using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows.Declarative.ObjectModel; @@ -16,17 +15,15 @@ internal sealed class SetTextVariableExecutor(SetTextVariable model, WorkflowFor { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken) { - PropertyPath variablePath = Throw.IfNull(this.Model.Variable?.Path, $"{nameof(this.Model)}.{nameof(model.Variable)}"); - if (this.Model.Value is null) { - await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false); + await this.AssignAsync(this.Model.Variable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false); } else { - FormulaValue expressionResult = FormulaValue.New(this.State.Engine.Format(this.Model.Value)); + FormulaValue expressionResult = FormulaValue.New(this.Engine.Format(this.Model.Value)); - await this.AssignAsync(variablePath, expressionResult, context).ConfigureAwait(false); + await this.AssignAsync(this.Model.Variable?.Path, expressionResult, context).ConfigureAwait(false); } return default; diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs index 07d00eaa22..24f15d19b8 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs @@ -25,7 +25,7 @@ internal sealed class SetVariableExecutor(SetVariable model, WorkflowFormulaStat } else { - EvaluationResult expressionResult = this.State.Evaluator.GetValue(this.Model.Value); + EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Value); await this.AssignAsync(variablePath, expressionResult.Value.ToFormula(), context).ConfigureAwait(false); } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/SystemScope.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/SystemScope.cs index d11588a9a0..94ce5215f9 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/SystemScope.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/SystemScope.cs @@ -21,7 +21,6 @@ internal static class SystemScope public const string Bot = nameof(Bot); public const string Conversation = nameof(Conversation); public const string ConversationId = nameof(SystemVariables.ConversationId); - public const string InternalId = nameof(InternalId); public const string LastMessage = nameof(LastMessage); public const string LastMessageId = nameof(SystemVariables.LastMessageId); public const string LastMessageText = nameof(SystemVariables.LastMessageText); @@ -36,7 +35,6 @@ internal static class SystemScope Names.Bot, Names.Conversation, Names.ConversationId, - Names.InternalId, Names.LastMessage, Names.LastMessageId, Names.LastMessageText, @@ -45,16 +43,16 @@ internal static class SystemScope Names.UserLanguage, ]; - public static void InitializeSystem(this WorkflowFormulaState scopes) + public static void InitializeSystem(this WorkflowFormulaState state) { - scopes.Set(Names.Activity, RecordValue.Empty(), VariableScopeNames.System); - scopes.Set(Names.Bot, RecordValue.Empty(), VariableScopeNames.System); + state.Set(Names.Activity, RecordValue.Empty(), VariableScopeNames.System); + state.Set(Names.Bot, RecordValue.Empty(), VariableScopeNames.System); - scopes.Set(Names.LastMessage, s_emptyMessage, VariableScopeNames.System); + state.Set(Names.LastMessage, s_emptyMessage, VariableScopeNames.System); Set(Names.LastMessageId); Set(Names.LastMessageText); - scopes.Set( + state.Set( Names.Conversation, FormulaValue.NewRecordFromFields( new NamedValue("Id", FormulaType.String.NewBlank()), @@ -62,51 +60,40 @@ internal static class SystemScope new NamedValue("LocalTimeZoneOffset", FormulaValue.New(TimeZoneInfo.Local.GetUtcOffset(DateTime.UtcNow))), new NamedValue("InTestMode", FormulaValue.New(false))), VariableScopeNames.System); - scopes.Set(Names.ConversationId, FormulaType.String.NewBlank(), VariableScopeNames.System); - scopes.Set(Names.InternalId, FormulaType.String.NewBlank(), VariableScopeNames.System); + state.Set(Names.ConversationId, FormulaType.String.NewBlank(), VariableScopeNames.System); - scopes.Set( + state.Set( Names.Recognizer, FormulaValue.NewRecordFromFields( new NamedValue("Id", FormulaType.String.NewBlank()), new NamedValue("Text", FormulaType.String.NewBlank())), VariableScopeNames.System); - scopes.Set( + state.Set( Names.User, FormulaValue.NewRecordFromFields( new NamedValue("Language", FormulaValue.New(CultureInfo.CurrentCulture.TwoLetterISOLanguageName))), VariableScopeNames.System); - scopes.Set(Names.UserLanguage, FormulaValue.New(CultureInfo.CurrentCulture.TwoLetterISOLanguageName), VariableScopeNames.System); + state.Set(Names.UserLanguage, FormulaValue.New(CultureInfo.CurrentCulture.TwoLetterISOLanguageName), VariableScopeNames.System); void Set(string key, string? value = null) { if (string.IsNullOrEmpty(value)) { - scopes.Set(key, FormulaType.String.NewBlank(), VariableScopeNames.System); + state.Set(key, FormulaType.String.NewBlank(), VariableScopeNames.System); } else { - scopes.Set(key, FormulaValue.New(value), VariableScopeNames.System); + state.Set(key, FormulaValue.New(value), VariableScopeNames.System); } } } - public static FormulaValue GetConversationId(this WorkflowFormulaState state) => - state.Get(Names.ConversationId, VariableScopeNames.System); - - public static void SetConversationId(this WorkflowFormulaState state, string conversationId) - { - RecordValue conversation = (RecordValue)state.Get(Names.Conversation, VariableScopeNames.System); - conversation.UpdateField("Id", FormulaValue.New(conversationId)); - state.Set(Names.Conversation, conversation, VariableScopeNames.System); - state.Set(Names.ConversationId, FormulaValue.New(conversationId), VariableScopeNames.System); - } - public static void SetLastMessage(this WorkflowFormulaState state, ChatMessage message) { state.Set(Names.LastMessage, message.ToRecord(), VariableScopeNames.System); state.Set(Names.LastMessageId, message.MessageId is null ? FormulaValue.NewBlank(FormulaType.String) : FormulaValue.New(message.MessageId), VariableScopeNames.System); state.Set(Names.LastMessageText, FormulaValue.New(message.Text), VariableScopeNames.System); + state.Bind(); } } diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs index cbe213a8dd..3fa2b7950d 100644 --- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs +++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/PowerFx/WorkflowFormulaState.cs @@ -9,7 +9,6 @@ using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Bot.ObjectModel; using Microsoft.PowerFx; using Microsoft.PowerFx.Types; -using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.Workflows.Declarative.PowerFx; @@ -21,7 +20,7 @@ internal sealed class WorkflowFormulaState // ISSUE #488 - Update default scope for workflows to `Workflow` (instead of `Topic`) public const string DefaultScopeName = VariableScopeNames.Topic; - private static readonly FrozenSet s_mutableScopes = + public static readonly FrozenSet RestorableScopes = [ VariableScopeNames.Topic, VariableScopeNames.Global, @@ -29,6 +28,7 @@ internal sealed class WorkflowFormulaState ]; private readonly Dictionary _scopes; + private int _isInitialized; public RecalcEngine Engine { get; } @@ -37,13 +37,13 @@ internal sealed class WorkflowFormulaState public WorkflowFormulaState(RecalcEngine engine) { + this._scopes = VariableScopeNames.AllScopes.ToDictionary(scopeName => scopeName, scopeName => new WorkflowScope()); this.Engine = engine; this.Evaluator = new WorkflowExpressionEngine(engine); - this._scopes = VariableScopeNames.AllScopes.ToDictionary(scopeName => scopeName, scopeName => new WorkflowScope(scopeName)); this.Bind(); } - public FormulaValue Get(PropertyPath variablePath) => this.Get(Throw.IfNull(variablePath.VariableName), variablePath.VariableScopeName); + public IEnumerable Keys(string scopeName) => this.GetScope(scopeName).Keys; public FormulaValue Get(string variableName, string? scopeName = null) { @@ -55,38 +55,8 @@ internal sealed class WorkflowFormulaState return FormulaValue.NewBlank(); } - public void ResetAll(string? scopeName = null) - { - if (scopeName is not null) - { - this.GetScope(scopeName).ResetAll(); - } - else - { - foreach (string targetScope in VariableScopeNames.AllScopes) - { - this.GetScope(targetScope).ResetAll(); - } - } - - this.Bind(); - } - - public void Reset(PropertyPath variablePath) => this.Reset(Throw.IfNull(variablePath.VariableName), variablePath.VariableScopeName); - - public void Reset(string variableName, string? scopeName = null) - { - this.GetScope(scopeName).Reset(variableName); - this.Bind(); - } - - public void Set(PropertyPath variablePath, FormulaValue value) => this.Set(Throw.IfNull(variablePath.VariableName), value, variablePath.VariableScopeName); - - public void Set(string variableName, FormulaValue value, string? scopeName = null) - { - this.GetScope(scopeName)[variableName] = value; - this.Bind(); - } + public void Set(string variableName, FormulaValue value, string? scopeName = null) => + this.GetScope(scopeName ?? DefaultScopeName)[variableName] = value; public bool SetInitialized() => Interlocked.CompareExchange(ref this._isInitialized, 1, 0) == 0; @@ -97,7 +67,7 @@ internal sealed class WorkflowFormulaState return; } - await Task.WhenAll(s_mutableScopes.Select(scopeName => ReadScopeAsync(scopeName))).ConfigureAwait(false); + await Task.WhenAll(RestorableScopes.Select(scopeName => ReadScopeAsync(scopeName))).ConfigureAwait(false); async Task ReadScopeAsync(string scopeName) { @@ -117,8 +87,6 @@ internal sealed class WorkflowFormulaState } } - public RecordValue BuildRecord(string scopeName) => this.GetScope(scopeName).BuildRecord(); - public void Bind(string? targetScope = null) { if (targetScope is not null) @@ -135,7 +103,7 @@ internal sealed class WorkflowFormulaState void Bind(string scopeName) { - RecordValue scopeRecord = this.BuildRecord(scopeName); + RecordValue scopeRecord = this.GetScope(scopeName).ToRecord(); this.Engine.DeleteFormula(scopeName); this.Engine.UpdateVariable(scopeName, scopeRecord); } @@ -156,37 +124,5 @@ internal sealed class WorkflowFormulaState /// /// The set of variables for a specific action scope. /// - private sealed class WorkflowScope(string scopeName) : Dictionary - { - public string Name => scopeName; - - public void ResetAll() - { - foreach (string variableName in this.Keys.ToArray()) - { - this.Reset(variableName); - } - } - - public void Reset(string variableName) - { - if (this.TryGetValue(variableName, out FormulaValue? value)) - { - this[variableName] = value.Type.NewBlank(); - } - } - - public RecordValue BuildRecord() - { - return FormulaValue.NewRecordFromFields(GetFields()); - - IEnumerable GetFields() - { - foreach (KeyValuePair kvp in this) - { - yield return new NamedValue(kvp.Key, kvp.Value); - } - } - } - } + private sealed class WorkflowScope : Dictionary; } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs index 4e746d87fb..d8836cfa94 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Framework/AgentFixture.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -8,7 +7,7 @@ using Shared.IntegrationTests; namespace Microsoft.Agents.Workflows.Declarative.IntegrationTests.Framework; -public sealed class AgentFixture : IDisposable +public static class AgentFixture { private static IReadOnlyDictionary? s_agentMap; @@ -18,9 +17,4 @@ public sealed class AgentFixture : IDisposable return s_agentMap; } - - public void Dispose() - { - GC.SuppressFinalize(this); - } } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml index 69095e2a3c..88d5ee97f5 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.IntegrationTests/Workflows/SendActivity.yaml @@ -7,19 +7,19 @@ trigger: # Capture input - kind: SetVariable - id: setvar_userinput + id: set_user_input variable: Topic.UserInput value: =System.LastMessage.Text # Capture environment variable - kind: SetVariable - id: setvar_username + id: set_user_name variable: Global.UserName value: =Env.USERNAME # Respond with input - kind: SendActivity - id: sendActivity_demo + id: send_result activity: |- Hello {Global.UserName}, You said, "{Topic.UserInput}" diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs index d65b33319b..5c1b7ac554 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/DeclarativeWorkflowTest.cs @@ -58,7 +58,7 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow public async Task LoopContinueActionAsync() { await this.RunWorkflowAsync("LoopContinue.yaml"); - this.AssertExecutionCount(expectedCount: 7); + this.AssertExecutionCount(expectedCount: 23); this.AssertExecuted("foreach_loop"); this.AssertExecuted("continueLoop_now"); this.AssertExecuted("end_all"); diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Interpreter/WorkflowModelTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Interpreter/WorkflowModelTest.cs index 35c5553454..ad6757e5df 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Interpreter/WorkflowModelTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Interpreter/WorkflowModelTest.cs @@ -14,21 +14,21 @@ namespace Microsoft.Agents.Workflows.Declarative.UnitTests.Interpreter; public sealed class DeclarativeWorkflowModelTest(ITestOutputHelper output) : WorkflowTest(output) { [Fact] - public async Task GetDepthForDefaultAsync() + public void GetDepthForDefault() { DeclarativeWorkflowModel model = new(CreateExecutor("root")); Assert.Equal(0, model.GetDepth(null)); } [Fact] - public async Task GetDepthForMissingNodeAsync() + public void GetDepthForMissingNode() { DeclarativeWorkflowModel model = new(CreateExecutor("root")); Assert.Throws(() => model.GetDepth("missing")); } [Fact] - public async Task ConnectMissingNodeAsync() + public void ConnectMissingNode() { TestExecutor rootExecutor = CreateExecutor("root"); DeclarativeWorkflowModel model = new(rootExecutor); @@ -38,21 +38,21 @@ public sealed class DeclarativeWorkflowModelTest(ITestOutputHelper output) : Wor } [Fact] - public async Task AddToMissingParentAsync() + public void AddToMissingParent() { DeclarativeWorkflowModel model = new(CreateExecutor("root")); Assert.Throws(() => model.AddNode(CreateExecutor("next"), "missing")); } [Fact] - public async Task LinkFromMissingSourceAsync() + public void LinkFromMissingSource() { DeclarativeWorkflowModel model = new(CreateExecutor("root")); Assert.Throws(() => model.AddLink("missing", "anything")); } [Fact] - public async Task LocateMissingParentAsync() + public void LocateMissingParent() { DeclarativeWorkflowModel model = new(CreateExecutor("root")); Assert.Null(model.LocateParent(null)); diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs index 9a48de3621..5838d4be7e 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/ClearAllVariablesExecutorTest.cs @@ -18,6 +18,7 @@ public sealed class ClearAllVariablesExecutorTest(ITestOutputHelper output) : Wo { // Arrange this.State.Set("NoVar", FormulaValue.New("Old value")); + this.State.Bind(); ClearAllVariables model = this.CreateModel( @@ -36,6 +37,10 @@ public sealed class ClearAllVariablesExecutorTest(ITestOutputHelper output) : Wo [Fact] public async Task ClearUndefinedScopeAsync() { + // Arrange + this.State.Set("NoVar", FormulaValue.New("Old value")); + this.State.Bind(); + // Arrange ClearAllVariables model = this.CreateModel( @@ -48,7 +53,7 @@ public sealed class ClearAllVariablesExecutorTest(ITestOutputHelper output) : Wo // Assert VerifyModel(model, action); - this.VerifyUndefined("NoVar"); + this.VerifyState("NoVar", FormulaValue.New("Old value")); } private ClearAllVariables CreateModel(string displayName, VariablesToClear variableTarget) diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs index 12cbe68fb3..2ffa4120dc 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/ObjectModel/SetVariableExecutorTest.cs @@ -92,6 +92,8 @@ 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"))); // Act, Assert @@ -107,6 +109,8 @@ 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"))); // Act, Assert @@ -122,6 +126,8 @@ 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"))); // Act, Assert diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs index e247f0b9bb..6998a19de8 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/RecalcEngineFactoryTests.cs @@ -1,61 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; -using Microsoft.Agents.Workflows.Declarative.Extensions; using Microsoft.Agents.Workflows.Declarative.PowerFx; using Microsoft.PowerFx; -using Microsoft.PowerFx.Types; using Xunit.Abstractions; namespace Microsoft.Agents.Workflows.Declarative.UnitTests.PowerFx; public class RecalcEngineFactoryTests(ITestOutputHelper output) : WorkflowTest(output) { - [Fact] - public void VariableUpdateTest() - { - RecalcEngine engine = RecalcEngineFactory.Create(); - - FormulaValue evalResult; - - engine.UpdateVariable("single", FormulaValue.New(1)); - evalResult = engine.Eval("single"); - Console.WriteLine($"# {evalResult.Format()}"); - - RecordValue recordSub = - FormulaValue.NewRecordFromFields( - new NamedValue("sub", FormulaValue.New(3.14))); - RecordValue recordRoot = - FormulaValue.NewRecordFromFields( - new NamedValue("another", FormulaValue.NewBlank()), - new NamedValue("val", FormulaValue.New(2.82)), - new NamedValue("root", recordSub)); - engine.DeleteFormula("Topic"); - engine.UpdateVariable("Topic", recordRoot); - evalResult = engine.Eval("Topic"); - Console.WriteLine($"# {evalResult.Format()}"); - evalResult = engine.Eval("Topic.val"); - Console.WriteLine($"# {evalResult.Format()}"); - evalResult = engine.Eval("Topic.root"); - Console.WriteLine($"# {evalResult.Format()}"); - evalResult = engine.Eval("Topic.root.sub"); - Console.WriteLine($"# {evalResult.Format()}"); - //recordRoot.UpdateField("another", FormulaValue.New("abc")); - RecordValue recordRoot2 = - FormulaValue.NewRecordFromFields( - new NamedValue("another", FormulaValue.New("abc")), - new NamedValue("val", FormulaValue.New(2.82)), - new NamedValue("root", recordSub)); - engine.DeleteFormula("Topic"); - engine.UpdateVariable("Topic", recordRoot2); - evalResult = engine.Eval("Topic.another"); - Console.WriteLine($"# {evalResult.Format()}"); - engine.UpdateVariable("Topic.another", FormulaValue.New(-1)); - evalResult = engine.Eval("Topic.another"); - Console.WriteLine($"# {evalResult.Format()}"); - } - [Fact] public void DefaultNotNull() { diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/TemplateExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/TemplateExtensionsTests.cs index ca3a8905d9..3a45d8bbb5 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/TemplateExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/TemplateExtensionsTests.cs @@ -100,6 +100,8 @@ public class TemplateExtensionsTests(ITestOutputHelper output) : RecalcEngineTes { // Arrange this.State.Set("Source", FormulaValue.New("Hello World")); + this.State.Bind(); + ExpressionSegment expressionSegment = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source"))); TemplateLine line = new([expressionSegment]); diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs index cd991cf613..9d612b3dc5 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs @@ -42,6 +42,7 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest this.State.Set(Variables.ObjectValue, ObjectData, VariableScopeNames.Topic); this.State.Set(Variables.ArrayValue, TableData, VariableScopeNames.Topic); this.State.Set(Variables.BlankValue, FormulaValue.NewBlank(), VariableScopeNames.Topic); + this.State.Bind(); } #region BoolExpression Tests @@ -136,6 +137,7 @@ public class WorkflowExpressionEngineTests : RecalcEngineTest // Arrange RecordValue state = FormulaValue.NewRecordFromFields([new NamedValue("test", FormulaValue.New("value"))]); this.State.Set("TestRecord", state, VariableScopeNames.Global); + this.State.Bind(); // Arrange, Act & Assert this.EvaluateExpression( diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowFormulaStateTests.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowFormulaStateTests.cs new file mode 100644 index 0000000000..bf026747ed --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowFormulaStateTests.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.Workflows.Declarative.PowerFx; +using Microsoft.Bot.ObjectModel; +using Microsoft.PowerFx.Types; + +namespace Microsoft.Agents.Workflows.Declarative.UnitTests.PowerFx; + +public class WorkflowFormulaStateTests +{ + internal WorkflowFormulaState State { get; } = new(RecalcEngineFactory.Create()); + + [Fact] + public void GetWithImplicitScope() + { + // Arrange + FormulaValue testValue = FormulaValue.New("test"); + this.State.Set("key1", testValue, VariableScopeNames.Topic); + + // Act + FormulaValue result = this.State.Get("key1"); + + // Assert + Assert.Equal(testValue, result); + } + + [Fact] + public void GetWithSpecifiedScope() + { + // Arrange + FormulaValue testValue = FormulaValue.New("test"); + this.State.Set("key1", testValue, VariableScopeNames.Global); + + // Act + FormulaValue result = this.State.Get("key1", VariableScopeNames.Global); + + // Assert + Assert.Equal(testValue, result); + } + + [Fact] + public void SetDefaultScope() + { + // Arrange + FormulaValue testValue = FormulaValue.New("test"); + + // Act + this.State.Set("key1", testValue); + + // Assert + FormulaValue result = this.State.Get("key1", VariableScopeNames.Topic); + Assert.Equal(testValue, result); + } + + [Fact] + public void SetSpecifiedScope() + { + // Arrange + FormulaValue testValue = FormulaValue.New("test"); + + // Act + this.State.Set("key1", testValue, VariableScopeNames.System); + + // Assert + FormulaValue result = this.State.Get("key1", VariableScopeNames.System); + Assert.Equal(testValue, result); + } + + [Fact] + public void SetOverwritesExistingValue() + { + // Arrange + FormulaValue initialValue = FormulaValue.New("initial"); + FormulaValue newValue = FormulaValue.New("new"); + + // Act + this.State.Set("key1", initialValue, VariableScopeNames.Topic); + this.State.Set("key1", newValue, VariableScopeNames.Topic); + + // Assert + FormulaValue result = this.State.Get("key1", VariableScopeNames.Topic); + Assert.Equal(newValue, result); + } +} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowScopesTests.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowScopesTests.cs deleted file mode 100644 index 606e21c327..0000000000 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/PowerFx/WorkflowScopesTests.cs +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Linq; -using Microsoft.Agents.Workflows.Declarative.PowerFx; -using Microsoft.Bot.ObjectModel; -using Microsoft.PowerFx.Types; - -namespace Microsoft.Agents.Workflows.Declarative.UnitTests.PowerFx; - -public class WorkflowScopesTests -{ - internal WorkflowFormulaState State { get; } = new(RecalcEngineFactory.Create()); - - [Fact] - public void ConstructorInitializesAllScopes() - { - // Act & Assert - RecordValue envRecord = this.State.BuildRecord(VariableScopeNames.Environment); - RecordValue topicRecord = this.State.BuildRecord(VariableScopeNames.Topic); - RecordValue globalRecord = this.State.BuildRecord(VariableScopeNames.Global); - RecordValue systemRecord = this.State.BuildRecord(VariableScopeNames.System); - - Assert.NotNull(envRecord); - Assert.NotNull(topicRecord); - Assert.NotNull(globalRecord); - Assert.NotNull(systemRecord); - } - - [Fact] - public void BuildRecordWhenEmpty() - { - // Act - RecordValue record = this.State.BuildRecord(VariableScopeNames.Topic); - - // Assert - Assert.NotNull(record); - Assert.Empty(record.Fields); - } - - [Fact] - public void BuildRecordContainsSetValues() - { - // Arrange - FormulaValue testValue = FormulaValue.New("test"); - this.State.Set("key1", testValue, VariableScopeNames.Topic); - - // Act - RecordValue record = this.State.BuildRecord(VariableScopeNames.Topic); - - // Assert - Assert.NotNull(record); - Assert.Single(record.Fields); - Assert.Equal("key1", record.Fields.First().Name); - Assert.Equal(testValue, record.Fields.First().Value); - } - - [Fact] - public void BuildRecordForAllScopeTypes() - { - // Arrange - FormulaValue testValue = FormulaValue.New("test"); - - // Act & Assert - this.State.Set("envKey", testValue, VariableScopeNames.Environment); - RecordValue envRecord = this.State.BuildRecord(VariableScopeNames.Environment); - Assert.Single(envRecord.Fields); - - this.State.Set("topicKey", testValue, VariableScopeNames.Topic); - RecordValue topicRecord = this.State.BuildRecord(VariableScopeNames.Topic); - Assert.Single(topicRecord.Fields); - - this.State.Set("globalKey", testValue, VariableScopeNames.Global); - RecordValue globalRecord = this.State.BuildRecord(VariableScopeNames.Global); - Assert.Single(globalRecord.Fields); - - this.State.Set("systemKey", testValue, VariableScopeNames.System); - RecordValue systemRecord = this.State.BuildRecord(VariableScopeNames.System); - Assert.Single(systemRecord.Fields); - } - - [Fact] - public void GetWithImplicitScope() - { - // Arrange - FormulaValue testValue = FormulaValue.New("test"); - this.State.Set("key1", testValue, VariableScopeNames.Topic); - - // Act - FormulaValue result = this.State.Get("key1"); - - // Assert - Assert.Equal(testValue, result); - } - - [Fact] - public void GetWithSpecifiedScope() - { - // Arrange - FormulaValue testValue = FormulaValue.New("test"); - this.State.Set("key1", testValue, VariableScopeNames.Global); - - // Act - FormulaValue result = this.State.Get("key1", VariableScopeNames.Global); - - // Assert - Assert.Equal(testValue, result); - } - - [Fact] - public void SetDefaultScope() - { - // Arrange - FormulaValue testValue = FormulaValue.New("test"); - - // Act - this.State.Set("key1", testValue); - - // Assert - FormulaValue result = this.State.Get("key1", VariableScopeNames.Topic); - Assert.Equal(testValue, result); - } - - [Fact] - public void SetSpecifiedScope() - { - // Arrange - FormulaValue testValue = FormulaValue.New("test"); - - // Act - this.State.Set("key1", testValue, VariableScopeNames.System); - - // Assert - FormulaValue result = this.State.Get("key1", VariableScopeNames.System); - Assert.Equal(testValue, result); - } - - [Fact] - public void SetOverwritesExistingValue() - { - // Arrange - FormulaValue initialValue = FormulaValue.New("initial"); - FormulaValue newValue = FormulaValue.New("new"); - - // Act - this.State.Set("key1", initialValue, VariableScopeNames.Topic); - this.State.Set("key1", newValue, VariableScopeNames.Topic); - - // Assert - FormulaValue result = this.State.Get("key1", VariableScopeNames.Topic); - Assert.Equal(newValue, result); - } - - [Fact] - public void RemoveSpecifiedScope() - { - // Arrange - FormulaValue testValue = FormulaValue.New("test"); - - // Act - this.State.Set("key1", testValue); - - // Assert - FormulaValue result = this.State.Get("key1"); - Assert.Equal(testValue, result); - - // Act - this.State.Reset("key1"); - - // Assert - FormulaValue resultBlank = this.State.Get("key1"); - Assert.IsType(resultBlank); - } -} diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/WorkflowTest.cs b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/WorkflowTest.cs index f82107e08f..809f05cbbc 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/WorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/WorkflowTest.cs @@ -33,5 +33,8 @@ public abstract class WorkflowTest : IDisposable } } + internal static string? FormatOptionalPath(string? variableName, string? scope = null) => + variableName is null ? null : FormatVariablePath(variableName, scope); + internal static string FormatVariablePath(string variableName, string? scope = null) => $"{scope ?? VariableScopeNames.Topic}.{variableName}"; } diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/ClearAllVariables.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/ClearAllVariables.yaml index b2785c70e1..83644115fb 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/ClearAllVariables.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/ClearAllVariables.yaml @@ -7,5 +7,5 @@ trigger: - kind: ClearAllVariables id: clear_all - variables: [ConversationScopedVariables] + variables: ConversationScopedVariables diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/Condition.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/Condition.yaml index 34fdd190a0..9505c5dc6d 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/Condition.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/Condition.yaml @@ -8,7 +8,7 @@ trigger: - kind: SetVariable id: setVariable_test variable: Topic.TestValue - value: =System.LastMessageText + value: =Value(System.LastMessageText) - kind: ConditionGroup id: conditionGroup_test diff --git a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/ConditionElse.yaml b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/ConditionElse.yaml index b69e814ca5..6431e83be6 100644 --- a/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/ConditionElse.yaml +++ b/dotnet/tests/Microsoft.Agents.Workflows.Declarative.UnitTests/Workflows/ConditionElse.yaml @@ -8,7 +8,7 @@ trigger: - kind: SetVariable id: setVariable_test variable: Topic.TestValue - value: =System.LastMessageText + value: =Value(System.LastMessageText) - kind: ConditionGroup id: conditionGroup_test