diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 01f2fbe735..3e2529a0b7 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -37,11 +37,12 @@
+
+
+
-
-
diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs
index 940c6afddf..19c3c0321a 100644
--- a/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs
+++ b/dotnet/samples/GettingStarted/Workflows/Declarative/Program.cs
@@ -36,7 +36,7 @@ internal sealed class Program
string? workflowFile = ParseWorkflowFile(args);
if (workflowFile is null)
{
- Notify("\nUsage: DeclarativeWorkflow []");
+ Notify("\nUsage: DeclarativeWorkflow []\n");
return;
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs
index 5c8fd35192..b3a0d044db 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/AzureAgentProvider.cs
@@ -48,10 +48,7 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
await this.GetAgentsClient().Messages.CreateMessageAsync(
conversationId,
role: s_roleMap[conversationMessage.Role.Value.ToUpperInvariant()],
- // TODO: PersistentAgent bug blocks supporting multiple content types:
- // https://github.com/Azure/azure-sdk-for-net/issues/52571
- //contentBlocks: GetContent(),
- content: conversationMessage.Text,
+ contentBlocks: GetContent(),
attachments: null,
metadata: GetMetadata(),
cancellationToken).ConfigureAwait(false);
@@ -66,27 +63,25 @@ public sealed class AzureAgentProvider(string projectEndpoint, TokenCredential p
return conversationMessage.AdditionalProperties.ToDictionary(prop => prop.Key, prop => prop.Value?.ToString() ?? string.Empty);
}
- // TODO: PersistentAgent bug blocks supporting multiple content types:
- // https://github.com/Azure/azure-sdk-for-net/issues/52571
- //IEnumerable GetContent()
- //{
- // foreach (AIContent content in conversationMessage.Contents)
- // {
- // MessageInputContentBlock? contentBlock =
- // content switch
- // {
- // TextContent textContent => new MessageInputTextBlock(textContent.Text),
- // HostedFileContent fileContent => new MessageInputImageFileBlock(new MessageImageFileParam(fileContent.FileId)),
- // UriContent uriContent when uriContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(uriContent.Uri.ToString())),
- // _ => null // Unsupported content type
- // };
+ IEnumerable GetContent()
+ {
+ foreach (AIContent content in conversationMessage.Contents)
+ {
+ MessageInputContentBlock? contentBlock =
+ content switch
+ {
+ TextContent textContent => new MessageInputTextBlock(textContent.Text),
+ HostedFileContent fileContent => new MessageInputImageFileBlock(new MessageImageFileParam(fileContent.FileId)),
+ UriContent uriContent when uriContent.Uri is not null => new MessageInputImageUriBlock(new MessageImageUriParam(uriContent.Uri.ToString())),
+ _ => null // Unsupported content type
+ };
- // if (contentBlock is not null)
- // {
- // yield return contentBlock;
- // }
- // }
- //}
+ if (contentBlock is not null)
+ {
+ yield return contentBlock;
+ }
+ }
+ }
}
///
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompletedEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompletedEvent.cs
index 349028c96b..9638d1649f 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompletedEvent.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionCompletedEvent.cs
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.Workflows.Declarative;
///
/// Event that indicates a declarative action has been invoked.
///
-public sealed class DeclarativeActionInvokedEvent : WorkflowEvent
+public sealed class DeclarativeActionCompletedEvent : WorkflowEvent
{
///
/// The declarative action id.
@@ -30,11 +30,10 @@ public sealed class DeclarativeActionInvokedEvent : WorkflowEvent
///
public string? PriorActionId { get; }
- internal DeclarativeActionInvokedEvent(DialogAction action, string? priorActionId) : base(action)
+ internal DeclarativeActionCompletedEvent(DialogAction action) : base(action)
{
this.ActionId = action.GetId();
this.ActionType = action.GetType().Name;
this.ParentActionId = action.GetParentId();
- this.PriorActionId = priorActionId;
}
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokedEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokedEvent.cs
index 16845124b6..7d92db985a 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokedEvent.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Events/DeclarativeActionInvokedEvent.cs
@@ -8,7 +8,7 @@ namespace Microsoft.Agents.Workflows.Declarative;
///
/// Event that indicates a declarative action has completed.
///
-public sealed class DeclarativeActionCompletedEvent : WorkflowEvent
+public sealed class DeclarativeActionInvokedEvent : WorkflowEvent
{
///
/// The declarative action identifier.
@@ -25,10 +25,16 @@ public sealed class DeclarativeActionCompletedEvent : WorkflowEvent
///
public string? ParentActionId { get; }
- internal DeclarativeActionCompletedEvent(DialogAction action) : base(action)
+ ///
+ /// Identifier of the previous action.
+ ///
+ public string? PriorActionId { get; }
+
+ internal DeclarativeActionInvokedEvent(DialogAction action, string? priorActionId) : base(action)
{
this.ActionId = action.GetId();
this.ActionType = action.GetType().Name;
this.ParentActionId = action.GetParentId();
+ this.PriorActionId = priorActionId;
}
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs
index e0dab7116a..e883609141 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
-using System.Dynamic;
using System.Linq;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
@@ -66,7 +65,7 @@ internal static class DataValueExtensions
DateTimeDataValue dateTimeValue => dateTimeValue.Value.DateTime,
DateDataValue dateValue => dateValue.Value,
TimeDataValue timeValue => timeValue.Value,
- TableDataValue tableValue => tableValue.Values.Select(value => value.ToObject()).ToArray(),
+ TableDataValue tableValue => tableValue.Values.Select(value => value.ToDictionary()).ToArray(),
RecordDataValue recordValue => recordValue.ToDictionary(),
OptionDataValue optionValue => optionValue.Value.Value,
_ => throw new DeclarativeModelException($"Unsupported {nameof(DataValue)} type: {value.GetType().Name}"),
@@ -89,19 +88,6 @@ internal static class DataValueExtensions
return recordType;
}
- public static ExpandoObject ToObject(this RecordDataValue recordDataValue)
- {
- ExpandoObject expandoObject = new();
-
- IDictionary dictionary = expandoObject;
- foreach (KeyValuePair field in recordDataValue.Properties)
- {
- dictionary[field.Key] = field.Value?.ToObject();
- }
-
- return expandoObject;
- }
-
private static RecordType ParseRecordType(this RecordDataValue record)
{
RecordType recordType = RecordType.Empty();
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
index e9d393c1ff..cc4761bf07 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
@@ -146,7 +146,7 @@ internal static class FormulaValueExtensions
public static RecordDataValue ToRecord(this RecordValue value) =>
DataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()));
- private static RecordValue ToRecord(this IDictionary value)
+ public static RecordValue ToRecord(this IDictionary value)
{
return FormulaValue.NewRecordFromFields(GetFields());
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
index dce481d5d0..b370d6bae0 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Microsoft.Agents.Workflows.Declarative.Interpreter;
using Microsoft.Agents.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
+using Microsoft.PowerFx.Types;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Workflows.Declarative.Extensions;
@@ -20,20 +21,28 @@ internal static class IWorkflowContextExtensions
public static ValueTask SendResultMessageAsync(this IWorkflowContext context, string id, object? result = null, CancellationToken cancellationToken = default) =>
context.SendMessageAsync(new ExecutorResultMessage(id, result));
+ public static ValueTask QueueStateResetAsync(this IWorkflowContext context, PropertyPath variablePath) =>
+ context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), UnassignedValue.Instance, Throw.IfNull(variablePath.VariableScopeName));
+
public static ValueTask QueueStateUpdateAsync(this IWorkflowContext context, PropertyPath variablePath, TValue? value) =>
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.VariableScopeName));
- public static async Task GetStateAsync(this IWorkflowContext context, CancellationToken cancellationToken)
+ public static ValueTask QueueSystemUpdateAsync(this IWorkflowContext context, string key, TValue? value) =>
+ DeclarativeContext(context).QueueSystemUpdateAsync(key, value);
+
+ public static FormulaValue ReadState(this IWorkflowContext context, PropertyPath variablePath) =>
+ context.ReadState(Throw.IfNull(variablePath.VariableName), Throw.IfNull(variablePath.VariableScopeName));
+
+ public static FormulaValue ReadState(this IWorkflowContext context, string key, string? scopeName = null) =>
+ DeclarativeContext(context).State.Get(key, scopeName);
+
+ private static DeclarativeWorkflowContext DeclarativeContext(IWorkflowContext context)
{
- if (context is DeclarativeWorkflowContext declarativeContext)
+ if (context is not DeclarativeWorkflowContext declarativeContext)
{
- return declarativeContext.State;
+ throw new DeclarativeActionException($"Invalid workflow context: {context.GetType().Name}.");
}
- WorkflowFormulaState state = new(RecalcEngineFactory.Create());
-
- await state.RestoreAsync(context, cancellationToken).ConfigureAwait(false);
-
- return state;
+ return declarativeContext;
}
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/PropertyPathExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/PropertyPathExtensions.cs
deleted file mode 100644
index 2fbc5c28ef..0000000000
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/PropertyPathExtensions.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Microsoft.Bot.ObjectModel;
-
-namespace Microsoft.Agents.Workflows.Declarative.Extensions;
-
-internal static class PropertyPathExtensions
-{
- public static string Format(this PropertyPath path) => string.Join(".", path.Segments());
-}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
index 44e845e63c..c870b77138 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
-using System.Collections.Frozen;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
@@ -10,8 +9,8 @@ using Microsoft.Agents.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.PowerFx;
using Microsoft.PowerFx.Types;
-using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
@@ -24,13 +23,8 @@ internal abstract class DeclarativeActionExecutor(TAction model, Workfl
internal abstract class DeclarativeActionExecutor : Executor
{
- private static readonly FrozenSet s_mutableScopes =
- [
- VariableScopeNames.Topic,
- VariableScopeNames.Global
- ];
-
private string? _parentId;
+ private readonly WorkflowFormulaState _state;
protected DeclarativeActionExecutor(DialogAction model, WorkflowFormulaState state)
: base(model.Id.Value)
@@ -40,17 +34,20 @@ internal abstract class DeclarativeActionExecutor : Executor this._parentId ??= this.Model.GetParentId() ?? WorkflowActionVisitor.Steps.Root();
- internal ILogger Logger { get; set; } = NullLogger.Instance;
+ public RecalcEngine Engine => this._state.Engine;
- protected WorkflowFormulaState State { get; }
+ public WorkflowExpressionEngine Evaluator => this._state.Evaluator;
+
+ internal ILogger Logger { get; set; } = NullLogger.Instance;
protected virtual bool IsDiscreteAction => true;
@@ -71,7 +68,7 @@ internal abstract class DeclarativeActionExecutor : Executor
protected override ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellation = default) =>
- this.State.RestoreAsync(context, cancellation);
+ this._state.RestoreAsync(context, cancellation);
protected async ValueTask AssignAsync(PropertyPath? targetPath, FormulaValue result, IWorkflowContext context)
{
@@ -113,11 +110,6 @@ internal abstract class DeclarativeActionExecutor : Executor ManagedScopes =
+ [
+ VariableScopeNames.Topic,
+ VariableScopeNames.Global,
+ ];
+
public DeclarativeWorkflowContext(IWorkflowContext source, WorkflowFormulaState state)
{
this.Source = source;
@@ -24,50 +33,38 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
public ValueTask AddEventAsync(WorkflowEvent workflowEvent) => this.Source.AddEventAsync(workflowEvent);
///
- public ValueTask QueueClearScopeAsync(string? scopeName = null)
+ public async ValueTask QueueClearScopeAsync(string? scopeName = null)
{
- this.State.ResetAll(scopeName);
- return this.Source.QueueClearScopeAsync(scopeName);
+ if (scopeName is not null)
+ {
+ if (ManagedScopes.Contains(scopeName))
+ {
+ // Copy keys to array to avoid modifying collection during enumeration.
+ foreach (string key in this.State.Keys(scopeName).ToArray())
+ {
+ await this.UpdateStateAsync(key, UnassignedValue.Instance, scopeName).ConfigureAwait(false);
+ }
+ }
+ else
+ {
+ await this.Source.QueueClearScopeAsync(scopeName).ConfigureAwait(false);
+ }
+
+ this.State.Bind();
+ }
}
///
public async ValueTask QueueStateUpdateAsync(string key, T? value, string? scopeName = null)
{
- ValueTask task = value switch
- {
- null => QueueEmptyStateAsync(),
- FormulaValue formulaValue => QueueFormulaStateAsync(formulaValue),
- DataValue dataValue => QueueDataValueStateAsync(dataValue),
- _ => QueueNativeStateAsync(value),
- };
+ await this.UpdateStateAsync(key, value, scopeName).ConfigureAwait(false);
+ this.State.Bind();
+ }
- await task.ConfigureAwait(false);
-
- ValueTask QueueEmptyStateAsync()
- {
- this.State.Set(key, FormulaValue.NewBlank(), scopeName);
- return this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName);
- }
-
- ValueTask QueueFormulaStateAsync(FormulaValue formulaValue)
- {
- this.State.Set(key, formulaValue, scopeName);
- return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName);
- }
-
- ValueTask QueueDataValueStateAsync(DataValue dataValue)
- {
- FormulaValue formulaValue = dataValue.ToFormula();
- this.State.Set(key, formulaValue, scopeName);
- return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName);
- }
-
- ValueTask QueueNativeStateAsync(object? rawValue)
- {
- FormulaValue formulaValue = rawValue.ToFormula();
- this.State.Set(key, formulaValue, scopeName);
- return this.Source.QueueStateUpdateAsync(key, rawValue, scopeName);
- }
+ public async ValueTask QueueSystemUpdateAsync(string key, TValue? value)
+ {
+ await this.UpdateStateAsync(key, value, VariableScopeNames.System, allowSystem: true).ConfigureAwait(false);
+ this.State.Bind();
}
///
@@ -78,4 +75,67 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
///
public ValueTask SendMessageAsync(object message, string? targetId = null) => this.Source.SendMessageAsync(message, targetId);
+
+ private ValueTask UpdateStateAsync(string key, T? value, string? scopeName, bool allowSystem = true)
+ {
+ bool isManagedScope =
+ scopeName != null && // null scope cannot be managed
+ (ManagedScopes.Contains(scopeName) ||
+ (allowSystem && VariableScopeNames.System.Equals(scopeName, StringComparison.Ordinal)));
+
+ if (!isManagedScope)
+ {
+ // Not a managed scope, just pass through. This is valid when a declarative
+ // workflow has been ejected to code (where DeclarativeWorkflowContext is also utilized).
+ return this.Source.QueueStateUpdateAsync(key, value, scopeName);
+ }
+
+ return value switch
+ {
+ null => QueueEmptyStateAsync(),
+ UnassignedValue => QueueEmptyStateAsync(),
+ BlankValue => QueueEmptyStateAsync(),
+ FormulaValue formulaValue => QueueFormulaStateAsync(formulaValue),
+ DataValue dataValue => QueueDataValueStateAsync(dataValue),
+ _ => QueueNativeStateAsync(value),
+ };
+
+ ValueTask QueueEmptyStateAsync()
+ {
+ if (isManagedScope)
+ {
+ this.State.Set(key, FormulaValue.NewBlank(), scopeName);
+ }
+ return this.Source.QueueStateUpdateAsync(key, UnassignedValue.Instance, scopeName);
+ }
+
+ ValueTask QueueFormulaStateAsync(FormulaValue formulaValue)
+ {
+ if (isManagedScope)
+ {
+ this.State.Set(key, formulaValue, scopeName);
+ }
+ return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName);
+ }
+
+ ValueTask QueueDataValueStateAsync(DataValue dataValue)
+ {
+ FormulaValue formulaValue = dataValue.ToFormula();
+ if (isManagedScope)
+ {
+ this.State.Set(key, formulaValue, scopeName);
+ }
+ return this.Source.QueueStateUpdateAsync(key, formulaValue.ToObject(), scopeName);
+ }
+
+ ValueTask QueueNativeStateAsync(object? rawValue)
+ {
+ FormulaValue formulaValue = rawValue.ToFormula();
+ if (isManagedScope)
+ {
+ this.State.Set(key, formulaValue, scopeName);
+ }
+ return this.Source.QueueStateUpdateAsync(key, rawValue, scopeName);
+ }
+ }
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs
index 45a38373f8..224ccfcf40 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs
@@ -197,7 +197,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
{
DefaultActionExecutor continueLoopExecutor = new(item, this._workflowState);
this.ContinueWith(continueLoopExecutor);
- this._workflowModel.AddLink(continueLoopExecutor.Id, Steps.Post(loopExecutor.Id));
+ this._workflowModel.AddLink(continueLoopExecutor.Id, ForeachExecutor.Steps.Next(loopExecutor.Id));
this.RestartAfter(continueLoopExecutor.Id, continueLoopExecutor.ParentId);
}
}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs
index a202a70cea..2cf6ffd6ac 100644
--- a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ObjectModel/AddConversationMessageExecutor.cs
@@ -18,7 +18,7 @@ internal sealed class AddConversationMessageExecutor(AddConversationMessage mode
protected override async ValueTask