.NET Workflows - Introduce support for Local variable scope (#944)

* Prepare for update

* Checkpoint

* Comments

* All fixed

* Mixed casre test added

* Rollback nuget

* Remove redundant restorable

* Namespace
This commit is contained in:
Chris
2025-09-26 21:44:19 +00:00
committed by GitHub
parent a9608465d9
commit 4d63899812
38 changed files with 242 additions and 207 deletions
@@ -17,7 +17,7 @@ namespace Microsoft.Agents.Workflows.Declarative;
public static class DeclarativeWorkflowBuilder
{
/// <summary>
/// Builds a process from the provided YAML definition of a CPS Topic ObjectModel.
/// Builds a workflow from the provided YAML definition.
/// </summary>
/// <typeparam name="TInput">The type of the input message</typeparam>
/// <param name="workflowFile">The path to the workflow.</param>
@@ -35,7 +35,7 @@ public static class DeclarativeWorkflowBuilder
}
/// <summary>
/// Builds a process from the provided YAML definition of a CPS Topic ObjectModel.
/// Builds a workflow from the provided YAML definition.
/// </summary>
/// <typeparam name="TInput">The type of the input message</typeparam>
/// <param name="yamlReader">The reader that provides the workflow object model YAML.</param>
@@ -71,7 +71,7 @@ public static class DeclarativeWorkflowBuilder
return visitor.Complete();
}
private static ChatMessage DefaultTransform(object message) =>
internal static ChatMessage DefaultTransform(object message) =>
message switch
{
ChatMessage chatMessage => chatMessage,
@@ -23,16 +23,16 @@ internal static class IWorkflowContextExtensions
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));
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), UnassignedValue.Instance, Throw.IfNull(variablePath.NamespaceAlias));
public static ValueTask QueueStateUpdateAsync<TValue>(this IWorkflowContext context, PropertyPath variablePath, TValue? value) =>
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.VariableScopeName));
context.QueueStateUpdateAsync(Throw.IfNull(variablePath.VariableName), value, Throw.IfNull(variablePath.NamespaceAlias));
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));
context.ReadState(Throw.IfNull(variablePath.VariableName), Throw.IfNull(variablePath.NamespaceAlias));
public static FormulaValue ReadState(this IWorkflowContext context, string key, string? scopeName = null) =>
DeclarativeContext(context).State.Get(key, scopeName);
@@ -51,7 +51,7 @@ internal static class IWorkflowContextExtensions
public static async ValueTask EnsureWorkflowConversationAsync(this IWorkflowContext context, WorkflowAgentProvider agentProvider, StringExpression expression, CancellationToken cancellationToken)
{
if (expression.IsVariableReference &&
expression.VariableReference.IsVariableReferenceWithScope(VariableScopeNames.System, out string? variableName))
expression.VariableReference.IsVariableReferenceWithScope(VariableNamespace.System, out string? variableName))
{
if (string.Equals(variableName, SystemScope.Names.Conversation, StringComparison.Ordinal) ||
string.Equals(variableName, SystemScope.Names.ConversationId, StringComparison.Ordinal))
@@ -16,6 +16,7 @@ internal sealed class DeclarativeWorkflowContext : IWorkflowContext
{
public static readonly FrozenSet<string> ManagedScopes =
[
VariableScopeNames.Local,
VariableScopeNames.Topic,
VariableScopeNames.Global,
];
@@ -211,6 +211,15 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
this.RestartAfter(item.Id.Value, endExecutor.ParentId);
}
protected override void Visit(EndDialog item)
{
this.Trace(item);
DefaultActionExecutor endExecutor = new(item, this._workflowState);
this.ContinueWith(endExecutor);
this.RestartAfter(item.Id.Value, endExecutor.ParentId);
}
protected override void Visit(Question item)
{
this.Trace(item);
@@ -388,8 +397,6 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
protected override void Visit(UnknownDialogAction item) => this.NotSupported(item);
protected override void Visit(EndDialog item) => this.NotSupported(item);
protected override void Visit(RepeatDialog item) => this.NotSupported(item);
protected override void Visit(ReplaceDialog item) => this.NotSupported(item);
@@ -6,11 +6,6 @@ namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
internal sealed class WorkflowElementWalker : BotElementWalker
{
static WorkflowElementWalker()
{
ProductContext.SetContext(Product.Foundry);
}
private readonly DialogActionVisitor _visitor;
public WorkflowElementWalker(DialogActionVisitor visitor)
@@ -37,6 +37,7 @@
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.Workflows.Declarative.UnitTests" />
<InternalsVisibleTo Include="Microsoft.Agents.Workflows.Declarative.IntegrationTests" />
</ItemGroup>
</Project>
@@ -17,8 +17,9 @@ internal static class RecalcEngineFactory
foreach (string scopeName in VariableScopeNames.AllScopes)
{
engine.UpdateVariable(scopeName, RecordValue.Empty());
engine.UpdateVariable(WorkflowFormulaState.GetScopeName(scopeName), RecordValue.Empty());
}
engine.UpdateVariable(VariableScopeNames.Topic, RecordValue.Empty());
return engine;
@@ -59,13 +59,13 @@ internal static class WorkflowDiagnostics
FormulaValue defaultValue = variableDiagnostic.ConstantValue?.ToFormula() ?? variableDiagnostic.Type.NewBlank();
if (variableDiagnostic.Path.VariableScopeName?.Equals(VariableScopeNames.System, StringComparison.OrdinalIgnoreCase) is true &&
if (variableDiagnostic.Path.NamespaceAlias?.Equals(VariableScopeNames.System, StringComparison.OrdinalIgnoreCase) is true &&
!SystemScope.AllNames.Contains(variableDiagnostic.Path.VariableName))
{
throw new DeclarativeModelException($"Variable '{variableDiagnostic.Path.VariableName}' is not a supported system variable.");
}
scopes.Set(variableDiagnostic.Path.VariableName, defaultValue, variableDiagnostic.Path.VariableScopeName ?? WorkflowFormulaState.DefaultScopeName);
scopes.Set(variableDiagnostic.Path.VariableName, defaultValue, variableDiagnostic.Path.NamespaceAlias ?? WorkflowFormulaState.DefaultScopeName);
}
}
@@ -17,12 +17,11 @@ namespace Microsoft.Agents.Workflows.Declarative.PowerFx;
/// </summary>
internal sealed class WorkflowFormulaState
{
// ISSUE #488 - Update default scope for workflows to `Workflow` (instead of `Topic`)
public const string DefaultScopeName = VariableScopeNames.Topic;
public const string DefaultScopeName = VariableScopeNames.Local;
public static readonly FrozenSet<string> RestorableScopes =
[
VariableScopeNames.Topic,
VariableScopeNames.Local,
VariableScopeNames.Global,
VariableScopeNames.System,
];
@@ -37,7 +36,8 @@ internal sealed class WorkflowFormulaState
public WorkflowFormulaState(RecalcEngine engine)
{
this._scopes = VariableScopeNames.AllScopes.ToDictionary(scopeName => scopeName, scopeName => new WorkflowScope());
this._scopes = VariableScopeNames.AllScopes.ToDictionary(scopeName => GetScopeName(scopeName), _ => new WorkflowScope());
this.Engine = engine;
this.Evaluator = new WorkflowExpressionEngine(engine);
this.Bind();
@@ -87,11 +87,15 @@ internal sealed class WorkflowFormulaState
}
}
public void Bind(string? targetScope = null)
public void Bind(string? scopeNameToBind = null)
{
if (targetScope is not null)
if (scopeNameToBind is not null)
{
Bind(targetScope);
Bind(scopeNameToBind);
if (VariableScopeNames.GetNamespaceFromName(scopeNameToBind) == VariableNamespace.Component)
{
Bind(scopeNameToBind, VariableScopeNames.Topic);
}
}
else
{
@@ -99,26 +103,38 @@ internal sealed class WorkflowFormulaState
{
Bind(scopeName);
}
Bind(DefaultScopeName, VariableScopeNames.Topic);
}
void Bind(string scopeName)
void Bind(string scopeName, string? targetScope = null)
{
targetScope = GetScopeName(targetScope ?? scopeName);
RecordValue scopeRecord = this.GetScope(scopeName).ToRecord();
this.Engine.DeleteFormula(scopeName);
this.Engine.UpdateVariable(scopeName, scopeRecord);
this.Engine.DeleteFormula(targetScope);
this.Engine.UpdateVariable(targetScope, scopeRecord);
}
}
private WorkflowScope GetScope(string? scopeName)
{
scopeName ??= DefaultScopeName;
private WorkflowScope GetScope(string? scopeName) => this._scopes[GetScopeName(scopeName)];
if (!VariableScopeNames.IsValidName(scopeName))
public static string GetScopeName(string? scopeName)
{
if (!ProductContext.IsLocalScopeSupported())
{
throw new DeclarativeActionException($"Invalid variable scope name: '{scopeName}'.");
ProductContext.SetContext(Product.Foundry);
}
return this._scopes[scopeName];
scopeName ??= DefaultScopeName;
return
VariableScopeNames.GetNamespaceFromName(scopeName) switch
{
// Always alias component level scope as "Local"
VariableNamespace.Component => DefaultScopeName,
VariableNamespace.Unknown => throw new DeclarativeActionException($"Invalid variable scope name: '{scopeName}'."),
_ => scopeName,
};
}
/// <summary>