mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.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:
@@ -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/>
|
||||
|
||||
+2
-3
@@ -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
-2
@@ -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
-15
@@ -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();
|
||||
|
||||
+1
-1
@@ -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());
|
||||
|
||||
|
||||
+17
-8
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
-10
@@ -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());
|
||||
}
|
||||
+10
-18
@@ -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
|
||||
|
||||
+97
-37
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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();
|
||||
}
|
||||
|
||||
+20
-37
@@ -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}
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+13
-7
@@ -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
-3
@@ -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}]
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
|
||||
+4
-4
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
+3
-6
@@ -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;
|
||||
|
||||
+1
-1
@@ -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
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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}"
|
||||
|
||||
+1
-1
@@ -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");
|
||||
|
||||
+6
-6
@@ -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));
|
||||
|
||||
+6
-1
@@ -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)
|
||||
|
||||
+6
@@ -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
|
||||
|
||||
-47
@@ -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()
|
||||
{
|
||||
|
||||
+2
@@ -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]);
|
||||
|
||||
|
||||
+2
@@ -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(
|
||||
|
||||
+84
@@ -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);
|
||||
}
|
||||
}
|
||||
-173
@@ -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}";
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,5 +7,5 @@ trigger:
|
||||
|
||||
- kind: ClearAllVariables
|
||||
id: clear_all
|
||||
variables: [ConversationScopedVariables]
|
||||
variables: ConversationScopedVariables
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user