.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 <taochen@microsoft.com>

* Namespace

* Optimize bind

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Tao Chen <taochen@microsoft.com>
This commit is contained in:
Chris
2025-09-23 14:08:07 -07:00
committed by GitHub
Unverified
parent b28e1db478
commit 06827e9f1a
46 changed files with 368 additions and 545 deletions
+3 -2
View File
@@ -37,11 +37,12 @@
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.9" />
<PackageVersion Include="System.Text.Json" Version="9.0.9" />
<PackageVersion Include="System.CodeDom" Version="9.0.8" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.9" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.9" />
<PackageVersion Include="System.Threading.Channels" Version="9.0.9" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.9" />
<!-- OpenTelemetry -->
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
@@ -36,7 +36,7 @@ internal sealed class Program
string? workflowFile = ParseWorkflowFile(args);
if (workflowFile is null)
{
Notify("\nUsage: DeclarativeWorkflow <workflow-file> [<input>]");
Notify("\nUsage: DeclarativeWorkflow <workflow-file> [<input>]\n");
return;
}
@@ -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<MessageInputContentBlock> 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<MessageInputContentBlock> 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;
}
}
}
}
/// <inheritdoc/>
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.Workflows.Declarative;
/// <summary>
/// Event that indicates a declarative action has been invoked.
/// </summary>
public sealed class DeclarativeActionInvokedEvent : WorkflowEvent
public sealed class DeclarativeActionCompletedEvent : WorkflowEvent
{
/// <summary>
/// The declarative action id.
@@ -30,11 +30,10 @@ public sealed class DeclarativeActionInvokedEvent : WorkflowEvent
/// </summary>
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;
}
}
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.Workflows.Declarative;
/// <summary>
/// Event that indicates a declarative action has completed.
/// </summary>
public sealed class DeclarativeActionCompletedEvent : WorkflowEvent
public sealed class DeclarativeActionInvokedEvent : WorkflowEvent
{
/// <summary>
/// The declarative action identifier.
@@ -25,10 +25,16 @@ public sealed class DeclarativeActionCompletedEvent : WorkflowEvent
/// </summary>
public string? ParentActionId { get; }
internal DeclarativeActionCompletedEvent(DialogAction action) : base(action)
/// <summary>
/// Identifier of the previous action.
/// </summary>
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;
}
}
@@ -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<string, object?> dictionary = expandoObject;
foreach (KeyValuePair<string, DataValue> field in recordDataValue.Properties)
{
dictionary[field.Key] = field.Value?.ToObject();
}
return expandoObject;
}
private static RecordType ParseRecordType(this RecordDataValue record)
{
RecordType recordType = RecordType.Empty();
@@ -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());
@@ -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<TValue>(this IWorkflowContext context, PropertyPath variablePath, TValue? value) =>
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.VariableScopeName));
public static async Task<WorkflowFormulaState> GetStateAsync(this IWorkflowContext context, CancellationToken cancellationToken)
public static ValueTask QueueSystemUpdateAsync<TValue>(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;
}
}
@@ -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());
}
@@ -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>(TAction model, Workfl
internal abstract class DeclarativeActionExecutor : Executor<ExecutorResultMessage>
{
private static readonly FrozenSet<string> 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<ExecutorResultMessa
throw new DeclarativeModelException($"Missing required properties for element: {model.GetId()} ({model.GetType().Name}).");
}
this._state = state;
this.Model = model;
this.State = state;
}
public DialogAction Model { get; }
public string ParentId => this._parentId ??= this.Model.GetParentId() ?? WorkflowActionVisitor.Steps.Root();
internal ILogger Logger { get; set; } = NullLogger<DeclarativeActionExecutor>.Instance;
public RecalcEngine Engine => this._state.Engine;
protected WorkflowFormulaState State { get; }
public WorkflowExpressionEngine Evaluator => this._state.Evaluator;
internal ILogger Logger { get; set; } = NullLogger<DeclarativeActionExecutor>.Instance;
protected virtual bool IsDiscreteAction => true;
@@ -71,7 +68,7 @@ internal abstract class DeclarativeActionExecutor : Executor<ExecutorResultMessa
try
{
object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this.State), cancellationToken: default).ConfigureAwait(false);
object? result = await this.ExecuteAsync(new DeclarativeWorkflowContext(context, this._state), cancellationToken: default).ConfigureAwait(false);
if (this.EmitResultEvent)
{
@@ -104,7 +101,7 @@ internal abstract class DeclarativeActionExecutor : Executor<ExecutorResultMessa
/// This must be overridden to restore any state that was saved during checkpointing.
/// </summary>
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<ExecutorResultMessa
return;
}
if (!s_mutableScopes.Contains(Throw.IfNull(targetPath.VariableScopeName)))
{
throw new DeclarativeModelException($"Invalid scope: {targetPath.VariableScopeName}");
}
await context.QueueStateUpdateAsync(targetPath, result).ConfigureAwait(false);
#if DEBUG
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.Workflows.Declarative.Extensions;
using Microsoft.Agents.Workflows.Declarative.PowerFx;
@@ -11,6 +14,12 @@ namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
internal sealed class DeclarativeWorkflowContext : IWorkflowContext
{
public static readonly FrozenSet<string> 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);
/// <inheritdoc/>
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();
}
}
/// <inheritdoc/>
public async ValueTask QueueStateUpdateAsync<T>(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<TValue>(string key, TValue? value)
{
await this.UpdateStateAsync(key, value, VariableScopeNames.System, allowSystem: true).ConfigureAwait(false);
this.State.Bind();
}
/// <inheritdoc/>
@@ -78,4 +75,67 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
/// <inheritdoc/>
public ValueTask SendMessageAsync(object message, string? targetId = null) => this.Source.SendMessageAsync(message, targetId);
private ValueTask UpdateStateAsync<T>(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);
}
}
}
@@ -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);
}
}
@@ -18,7 +18,7 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode
protected override async ValueTask<object?> 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();
}
@@ -13,46 +13,29 @@ namespace Microsoft.Agents.Workflows.Declarative.ObjectModel;
internal sealed class ClearAllVariablesExecutor(ClearAllVariables model, WorkflowFormulaState state)
: DeclarativeActionExecutor<ClearAllVariables>(model, state)
{
protected override ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
EvaluationResult<VariablesToClearWrapper> variablesResult = this.State.Evaluator.GetValue(this.Model.Variables);
EvaluationResult<VariablesToClearWrapper> 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}
""");
}
}
}
@@ -57,7 +57,7 @@ internal sealed class ConditionGroupExecutor : DeclarativeActionExecutor<Conditi
continue; // Skip if no condition is defined
}
EvaluationResult<bool> expressionResult = this.State.Evaluator.GetValue(conditionItem.Condition);
EvaluationResult<bool> expressionResult = this.Evaluator.GetValue(conditionItem.Condition);
if (expressionResult.Value)
{
return Steps.Item(this.Model, conditionItem);
@@ -17,7 +17,7 @@ internal sealed class CopyConversationMessagesExecutor(CopyConversationMessages
{
protected override async ValueTask<object?> 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<DataValue> expressionResult = this.State.Evaluator.GetValue(this.Model.Messages);
EvaluationResult<DataValue> expressionResult = this.Evaluator.GetValue(this.Model.Messages);
messages = expressionResult.Value;
}
@@ -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<DataValue> addResult = this.State.Evaluator.GetValue(addItemValue);
EvaluationResult<DataValue> 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<DataValue> removeResult = this.State.Evaluator.GetValue(removeItemValue);
EvaluationResult<DataValue> 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);
@@ -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<DataValue> expressionResult = this.State.Evaluator.GetValue(addItemValue);
EvaluationResult<DataValue> 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<DataValue> expressionResult = this.State.Evaluator.GetValue(removeItemValue);
EvaluationResult<DataValue> 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);
@@ -46,7 +46,7 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
}
else
{
EvaluationResult<DataValue> expressionResult = this.State.Evaluator.GetValue(this.Model.Items);
EvaluationResult<DataValue> 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<Foreach>
{
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
@@ -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<DataValue> expressionResult = this.State.Evaluator.GetValue(this.AgentInput.Messages);
EvaluationResult<DataValue> expressionResult = this.Evaluator.GetValue(this.AgentInput.Messages);
userInput = expressionResult.Value;
}
@@ -102,12 +108,12 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA
return null;
}
EvaluationResult<string> conversationIdResult = this.State.Evaluator.GetValue(this.Model.ConversationId);
EvaluationResult<string> 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<bool> autoSendResult = this.State.Evaluator.GetValue(this.AgentOutput.AutoSend);
EvaluationResult<bool> autoSendResult = this.Evaluator.GetValue(this.AgentOutput.AutoSend);
return autoSendResult.Value;
}
@@ -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<DataValue> expressionResult = this.State.Evaluator.GetValue(valueExpression);
EvaluationResult<DataValue> expressionResult = this.Evaluator.GetValue(valueExpression);
FormulaValue? parsedResult = null;
@@ -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();
}
}
@@ -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<ResetVariable>(model, state)
{
protected override ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
protected override async ValueTask<object?> 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}]
@@ -16,8 +16,8 @@ internal sealed class RetrieveConversationMessageExecutor(RetrieveConversationMe
{
protected override async ValueTask<object?> 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);
@@ -18,7 +18,7 @@ internal sealed class RetrieveConversationMessagesExecutor(RetrieveConversationM
{
protected override async ValueTask<object?> 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;
}
@@ -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);
}
@@ -29,7 +29,7 @@ internal sealed class SetMultipleVariablesExecutor(SetMultipleVariables model, W
}
else
{
EvaluationResult<DataValue> expressionResult = this.State.Evaluator.GetValue(assignment.Value);
EvaluationResult<DataValue> expressionResult = this.Evaluator.GetValue(assignment.Value);
await this.AssignAsync(assignment.Variable, expressionResult.Value.ToFormula(), context).ConfigureAwait(false);
}
@@ -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<object?> 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;
@@ -25,7 +25,7 @@ internal sealed class SetVariableExecutor(SetVariable model, WorkflowFormulaStat
}
else
{
EvaluationResult<DataValue> expressionResult = this.State.Evaluator.GetValue(this.Model.Value);
EvaluationResult<DataValue> expressionResult = this.Evaluator.GetValue(this.Model.Value);
await this.AssignAsync(variablePath, expressionResult.Value.ToFormula(), context).ConfigureAwait(false);
}
@@ -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();
}
}
@@ -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<string> s_mutableScopes =
public static readonly FrozenSet<string> RestorableScopes =
[
VariableScopeNames.Topic,
VariableScopeNames.Global,
@@ -29,6 +28,7 @@ internal sealed class WorkflowFormulaState
];
private readonly Dictionary<string, WorkflowScope> _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<string> 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
/// <summary>
/// The set of variables for a specific action scope.
/// </summary>
private sealed class WorkflowScope(string scopeName) : Dictionary<string, FormulaValue>
{
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<NamedValue> GetFields()
{
foreach (KeyValuePair<string, FormulaValue> kvp in this)
{
yield return new NamedValue(kvp.Key, kvp.Value);
}
}
}
}
private sealed class WorkflowScope : Dictionary<string, FormulaValue>;
}
@@ -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<string, string?>? s_agentMap;
@@ -18,9 +17,4 @@ public sealed class AgentFixture : IDisposable
return s_agentMap;
}
public void Dispose()
{
GC.SuppressFinalize(this);
}
}
@@ -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}"
@@ -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");
@@ -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<DeclarativeModelException>(() => 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<DeclarativeModelException>(() => model.AddNode(CreateExecutor("next"), "missing"));
}
[Fact]
public async Task LinkFromMissingSourceAsync()
public void LinkFromMissingSource()
{
DeclarativeWorkflowModel model = new(CreateExecutor("root"));
Assert.Throws<DeclarativeModelException>(() => model.AddLink("missing", "anything"));
}
[Fact]
public async Task LocateMissingParentAsync()
public void LocateMissingParent()
{
DeclarativeWorkflowModel model = new(CreateExecutor("root"));
Assert.Null(model.LocateParent<TestExecutor>(null));
@@ -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)
@@ -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
@@ -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()
{
@@ -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]);
@@ -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(
@@ -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);
}
}
@@ -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<BlankValue>(resultBlank);
}
}
@@ -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}";
}
@@ -7,5 +7,5 @@ trigger:
- kind: ClearAllVariables
id: clear_all
variables: [ConversationScopedVariables]
variables: ConversationScopedVariables
@@ -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
@@ -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