test
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled

This commit is contained in:
2026-01-24 03:05:12 +11:00
parent f78f2388b3
commit 539852f81c
2584 changed files with 287471 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class AddConversationMessageTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void NoRole()
{
// Act, Assert
this.ExecuteTest(
nameof(AddConversationMessage),
"TestVariable",
conversation: StringExpression.Literal("#rev_9"),
content:
[
new AddConversationMessageContent.Builder()
{
Type = AgentMessageContentType.Text,
Value = TemplateLine.Parse("Hello! How can I help you today?"),
},
]);
}
[Fact]
public void WithRole()
{
// Act, Assert
this.ExecuteTest(
nameof(AddConversationMessage),
"TestVariable",
conversation: StringExpression.Variable(PropertyPath.Create("System.ConversationId")),
role: AgentMessageRoleWrapper.Get(AgentMessageRole.Agent),
content:
[
new AddConversationMessageContent.Builder()
{
Type = AgentMessageContentType.Text,
Value = TemplateLine.Parse("Hello! How can I help you today?"),
},
]);
}
[Fact]
public void WithMetadataLiteral()
{
// Act, Assert
this.ExecuteTest(
nameof(AddConversationMessage),
"TestVariable",
conversation: StringExpression.Variable(PropertyPath.Create("System.Conversation.Id")),
role: AgentMessageRoleWrapper.Get(AgentMessageRole.Agent),
metadata: ObjectExpression<RecordDataValue>.Literal(
new RecordDataValue(
new Dictionary<string, DataValue>
{
{ "key1", StringDataValue.Create("value1") },
{ "key2", NumberDataValue.Create(42) },
}.ToImmutableDictionary())),
content:
[
new AddConversationMessageContent.Builder()
{
Type = AgentMessageContentType.Text,
Value = TemplateLine.Parse("Hello! How can I help you today?"),
},
]);
}
[Fact]
public void WithMetadataVariable()
{
// Act, Assert
this.ExecuteTest(
nameof(AddConversationMessage),
"TestVariable",
conversation: StringExpression.Literal("#rev_9"),
role: AgentMessageRoleWrapper.Get(AgentMessageRole.Agent),
metadata: ObjectExpression<RecordDataValue>.Variable(PropertyPath.TopicVariable("MyMetadata")),
content:
[
new AddConversationMessageContent.Builder()
{
Type = AgentMessageContentType.Text,
Value = TemplateLine.Parse("Hello! How can I help you today?"),
},
]);
}
private void ExecuteTest(
string displayName,
string variableName,
StringExpression conversation,
IEnumerable<AddConversationMessageContent.Builder> content,
AgentMessageRoleWrapper? role = null,
ObjectExpression<RecordDataValue>.Builder? metadata = null)
{
// Arrange
AddConversationMessage model =
this.CreateModel(
displayName,
FormatVariablePath(variableName),
conversation,
content,
role,
metadata);
// Act
AddConversationMessageTemplate template = new(model);
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertGeneratedCode<ActionExecutor>(template.Id, workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
AssertGeneratedAssignment(model.Message?.Path, workflowCode);
}
private AddConversationMessage CreateModel(
string displayName,
string variablePath,
StringExpression conversation,
IEnumerable<AddConversationMessageContent.Builder> contents,
AgentMessageRoleWrapper? role,
ObjectExpression<RecordDataValue>.Builder? metadata)
{
AddConversationMessage.Builder actionBuilder =
new()
{
Id = this.CreateActionId("add_message"),
DisplayName = this.FormatDisplayName(displayName),
ConversationId = conversation,
Message = PropertyPath.Create(variablePath),
Role = role,
Metadata = metadata,
};
foreach (AddConversationMessageContent.Builder content in contents)
{
actionBuilder.Content.Add(content);
}
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class BreakLoopTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void BreakLoop()
{
// Act, Assert
this.ExecuteTest(nameof(BreakLoop));
}
private void ExecuteTest(string displayName)
{
// Arrange
BreakLoop model = this.CreateModel(displayName);
// Act
DefaultTemplate template = new(model, "workflow_id");
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertDelegate(template.Id, "workflow_id", workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
}
private BreakLoop CreateModel(string displayName)
{
BreakLoop.Builder actionBuilder =
new()
{
Id = this.CreateActionId("break_loop"),
DisplayName = this.FormatDisplayName(displayName),
};
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,76 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class ClearAllVariablesTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void LiteralEnum()
{
// Arrange
EnumExpression<VariablesToClearWrapper>.Builder expressionBuilder = new(EnumExpression<VariablesToClearWrapper>.Literal(VariablesToClear.AllGlobalVariables));
// Act, Assert
this.ExecuteTest(nameof(LiteralEnum), expressionBuilder);
}
[Fact]
public void VariableEnum()
{
// Arrange
EnumExpression<VariablesToClearWrapper>.Builder expressionBuilder = new(EnumExpression<VariablesToClearWrapper>.Variable(PropertyPath.TopicVariable("MyClearEnum")));
// Act, Assert
this.ExecuteTest(nameof(VariableEnum), expressionBuilder);
}
[Fact]
public void UnsupportedEnum()
{
// Arrange
EnumExpression<VariablesToClearWrapper>.Builder expressionBuilder = new(EnumExpression<VariablesToClearWrapper>.Literal(VariablesToClear.UserScopedVariables));
// Act, Assert
this.ExecuteTest(nameof(UnsupportedEnum), expressionBuilder);
}
private void ExecuteTest(
string displayName,
EnumExpression<VariablesToClearWrapper>.Builder variablesExpression)
{
// Arrange
ClearAllVariables model =
this.CreateModel(
displayName,
variablesExpression);
// Act
ClearAllVariablesTemplate template = new(model);
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertGeneratedCode<ActionExecutor>(template.Id, workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
}
private ClearAllVariables CreateModel(
string displayName,
EnumExpression<VariablesToClearWrapper>.Builder variablesExpression)
{
ClearAllVariables.Builder actionBuilder =
new()
{
Id = this.CreateActionId("set_variable"),
DisplayName = this.FormatDisplayName(displayName),
Variables = variablesExpression,
};
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,108 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class ConditionGroupTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void NoElse()
{
// Act, Assert
this.ExecuteTest(
nameof(WithElse),
hasElse: false);
}
[Fact]
public void WithElse()
{
// Act, Assert
this.ExecuteTest(
nameof(WithElse),
hasElse: true);
}
private void ExecuteTest(string displayName, bool hasElse = false)
{
// Arrange
ConditionGroup model = this.CreateModel(displayName, hasElse);
// Act
ConditionGroupTemplate template = new(model);
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertGeneratedCode<ActionExecutor>(template.Id, workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
foreach (ConditionItem condition in model.Conditions)
{
Assert.Contains(@$"""{condition.Id}""", workflowCode);
}
if (model.ElseActions?.Actions.Length > 0)
{
Assert.Contains(@$"""{model.ElseActions.Id}""", workflowCode);
}
}
private ConditionGroup CreateModel(string displayName, bool hasElse = false)
{
ConditionGroup.Builder actionBuilder =
new()
{
Id = this.CreateActionId("condition_group"),
DisplayName = this.FormatDisplayName(displayName),
};
actionBuilder.Conditions.Add(
new ConditionItem.Builder
{
Id = "condition_item_a",
Condition = BoolExpression.Expression("2 > 3"),
Actions = this.CreateActions("condition_a"),
});
actionBuilder.Conditions.Add(
new ConditionItem.Builder
{
Id = "condition_item_b",
Condition = BoolExpression.Expression("2 < 3"),
Actions = this.CreateActions("condition_b"),
});
if (hasElse)
{
actionBuilder.ElseActions = this.CreateActions("condition_else");
}
return actionBuilder.Build();
}
private ActionScope.Builder CreateActions(string prefix, int count = 2)
{
ActionScope.Builder actions =
new()
{
Id = this.CreateActionId("${prefix}_actions"),
};
for (int index = 1; index <= count; ++index)
{
actions.Actions.Add(
new SendActivity.Builder
{
Id = this.CreateActionId($"{prefix}_action_{index}"),
Activity = new MessageActivityTemplate
{
//Value = TemplateLine.Parse($"This is message #{index}"),
},
});
}
return actions;
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class ContinueLoopTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void ContinueLoop()
{
// Act, Assert
this.ExecuteTest(nameof(ContinueLoop));
}
private void ExecuteTest(string displayName)
{
// Arrange
ContinueLoop model = this.CreateModel(displayName);
// Act
DefaultTemplate template = new(model, "workflow_id");
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertDelegate(template.Id, "workflow_id", workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
}
private ContinueLoop CreateModel(string displayName)
{
ContinueLoop.Builder actionBuilder =
new()
{
Id = this.CreateActionId("continue_loop"),
DisplayName = this.FormatDisplayName(displayName),
};
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,72 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class CopyConversationMessagesTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void CopyConversationMessagesLiteral()
{
// Act, Assert
this.ExecuteTest(
nameof(CopyConversationMessagesLiteral),
StringExpression.Literal("#conv_dm99"),
ValueExpression.Variable(PropertyPath.TopicVariable("MyMessages")));
}
[Fact]
public void CopyConversationMessagesVariable()
{
// Act, Assert
this.ExecuteTest(
nameof(CopyConversationMessagesVariable),
StringExpression.Variable(PropertyPath.TopicVariable("TestConversation")),
ValueExpression.Variable(PropertyPath.TopicVariable("MyMessages")));
}
private void ExecuteTest(
string displayName,
StringExpression conversation,
ValueExpression messages,
ValueExpression? metadata = null)
{
// Arrange
CopyConversationMessages model =
this.CreateModel(
displayName,
conversation,
messages);
// Act
CopyConversationMessagesTemplate template = new(model);
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertGeneratedCode<ActionExecutor>(template.Id, workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
}
private CopyConversationMessages CreateModel(
string displayName,
StringExpression conversation,
ValueExpression messages,
ValueExpression? metadata = null)
{
CopyConversationMessages.Builder actionBuilder =
new()
{
Id = this.CreateActionId("copy_messages"),
DisplayName = this.FormatDisplayName(displayName),
ConversationId = conversation,
Messages = messages,
};
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,83 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class CreateConversationTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void Basic()
{
// Act, Assert
this.ExecuteTest(
nameof(Basic),
"TestVariable");
}
[Fact]
public void WithMetadata()
{
Dictionary<string, string> metadata =
new()
{
["key1"] = "value1",
["key2"] = "value2",
};
// Act, Assert
this.ExecuteTest(
nameof(WithMetadata),
"TestVariable",
ObjectExpression<RecordDataValue>.Literal(metadata.ToRecordValue()));
}
private void ExecuteTest(
string displayName,
string variableName,
ObjectExpression<RecordDataValue>? metadata = null)
{
// Arrange
CreateConversation model =
this.CreateModel(
displayName,
FormatVariablePath(variableName),
metadata);
// Act
CreateConversationTemplate template = new(model);
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertGeneratedCode<ActionExecutor>(template.Id, workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
AssertGeneratedAssignment(model.ConversationId?.Path, workflowCode);
}
private CreateConversation CreateModel(
string displayName,
string variablePath,
ObjectExpression<RecordDataValue>? metadata = null)
{
CreateConversation.Builder actionBuilder =
new()
{
Id = this.CreateActionId("create_conversation"),
DisplayName = this.FormatDisplayName(displayName),
ConversationId = PropertyPath.Create(variablePath),
};
if (metadata is not null)
{
actionBuilder.Metadata = metadata;
}
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.IO;
using System.Threading.Tasks;
using Shared.Code;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
/// <summary>
/// Tests execution of workflow created by <see cref="DeclarativeWorkflowBuilder"/>.
/// </summary>
public sealed class DeclarativeEjectionTest(ITestOutputHelper output) : WorkflowTest(output)
{
[Theory]
[InlineData("AddConversationMessage.yaml")]
[InlineData("CancelWorkflow.yaml")]
[InlineData("ClearAllVariables.yaml")]
[InlineData("CopyConversationMessages.yaml")]
[InlineData("Condition.yaml")]
[InlineData("ConditionElse.yaml")]
[InlineData("CreateConversation.yaml")]
[InlineData("EditTable.yaml")]
[InlineData("EditTableV2.yaml")]
[InlineData("EndConversation.yaml")]
[InlineData("EndWorkflow.yaml")]
[InlineData("Goto.yaml")]
[InlineData("InvokeAgent.yaml")]
[InlineData("LoopBreak.yaml")]
[InlineData("LoopContinue.yaml")]
[InlineData("LoopEach.yaml")]
[InlineData("ParseValue.yaml")]
[InlineData("ResetVariable.yaml")]
[InlineData("RetrieveConversationMessage.yaml")]
[InlineData("RetrieveConversationMessages.yaml")]
[InlineData("SendActivity.yaml")]
[InlineData("SetVariable.yaml")]
[InlineData("SetTextVariable.yaml")]
public Task ExecuteActionAsync(string workflowFile) =>
this.EjectWorkflowAsync(workflowFile);
private async Task EjectWorkflowAsync(string workflowFile)
{
using StreamReader yamlReader = File.OpenText(Path.Combine("Workflows", workflowFile));
string workflowCode = DeclarativeWorkflowBuilder.Eject(yamlReader, DeclarativeWorkflowLanguage.CSharp, "Test.WorkflowProviders");
string baselinePath = Path.Combine("Workflows", Path.ChangeExtension(workflowFile, ".cs"));
string generatedPath = Path.Combine("Workflows", Path.ChangeExtension(workflowFile, ".g.cs"));
this.Output.WriteLine($"WRITING BASELINE TO: {Path.GetFullPath(generatedPath)}\n");
try
{
File.WriteAllText(Path.GetFullPath(generatedPath), workflowCode);
Compiler.Build(workflowCode, Compiler.RepoDependencies(typeof(DeclarativeWorkflowBuilder))); // Throws if build fails
}
finally
{
Console.WriteLine(workflowCode);
}
string expectedCode = File.ReadAllText(baselinePath);
string[] expectedLines = expectedCode.Trim().Split('\n');
string[] workflowLines = workflowCode.Trim().Split('\n');
Assert.Equal(expectedLines.Length, workflowLines.Length);
for (int index = 0; index < workflowLines.Length; ++index)
{
this.Output.WriteLine($"Comparing line #{index + 1}/{workflowLines.Length}.");
Assert.Equal(expectedLines[index].Trim(), workflowLines[index].Trim());
}
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class EdgeTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void InitializeNext()
{
this.ExecuteTest("set_variable_1", "invoke_agent_2");
}
private void ExecuteTest(string sourceId, string targetId)
{
// Arrange
EdgeTemplate template = new(sourceId, targetId);
// Act
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
Assert.Equal("builder.AddEdge(setVariable1, invokeAgent2);", workflowCode.Trim());
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class EndConversationTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void EndConversation()
{
// Act, Assert
this.ExecuteTest(nameof(EndConversation));
}
private void ExecuteTest(string displayName)
{
// Arrange
EndConversation model = this.CreateModel(displayName);
// Act
DefaultTemplate template = new(model, "workflow_id");
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertDelegate(template.Id, "workflow_id", workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
}
private EndConversation CreateModel(string displayName)
{
EndConversation.Builder actionBuilder =
new()
{
Id = this.CreateActionId("end_conversation"),
DisplayName = this.FormatDisplayName(displayName),
};
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class EndDialogTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void EndDialog()
{
// Act, Assert
this.ExecuteTest(nameof(EndDialog));
}
private void ExecuteTest(string displayName)
{
// Arrange
EndDialog model = this.CreateModel(displayName);
// Act
DefaultTemplate template = new(model, "workflow_id");
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertDelegate(template.Id, "workflow_id", workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
}
private EndDialog CreateModel(string displayName)
{
EndDialog.Builder actionBuilder =
new()
{
Id = this.CreateActionId("end_Dialog"),
DisplayName = this.FormatDisplayName(displayName),
};
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,82 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class ForeachTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void LoopNoIndex()
{
// Act, Assert
this.ExecuteTest(
nameof(LoopNoIndex),
ValueExpression.Variable(PropertyPath.TopicVariable("MyItems")),
"LoopValue");
}
[Fact]
public void LoopWithIndex()
{
// Act, Assert
this.ExecuteTest(
nameof(LoopNoIndex),
ValueExpression.Variable(PropertyPath.TopicVariable("MyItems")),
"LoopValue",
"IndexValue");
}
private void ExecuteTest(
string displayName,
ValueExpression items,
string valueName,
string? indexName = null)
{
// Arrange
Foreach model =
this.CreateModel(
displayName,
items,
FormatVariablePath(valueName),
FormatOptionalPath(indexName));
// Act
ForeachTemplate template = new(model);
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertGeneratedCode<ActionExecutor>(template.Id, workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
AssertGeneratedMethod(nameof(ForeachExecutor.TakeNextAsync), workflowCode);
AssertGeneratedMethod(nameof(ForeachExecutor.ResetAsync), workflowCode);
}
private Foreach CreateModel(
string displayName,
ValueExpression items,
string valueName,
string? indexName = null)
{
Foreach.Builder actionBuilder =
new()
{
Id = this.CreateActionId("loop_action"),
DisplayName = this.FormatDisplayName(displayName),
Items = items,
Value = PropertyPath.Create(valueName),
};
if (indexName is not null)
{
actionBuilder.Index = PropertyPath.Create(indexName);
}
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class GotoTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void GotoAction()
{
// Act, Assert
this.ExecuteTest(nameof(GotoAction), "target_action_id");
}
private void ExecuteTest(string displayName, string targetId)
{
// Arrange
GotoAction model = this.CreateModel(displayName, targetId);
// Act
DefaultTemplate template = new(model, "workflow_id");
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertDelegate(template.Id, "workflow_id", workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
}
private GotoAction CreateModel(string displayName, string targetId)
{
GotoAction.Builder actionBuilder =
new()
{
Id = this.CreateActionId("goto_action"),
DisplayName = this.FormatDisplayName(displayName),
ActionId = new ActionId(targetId),
};
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,140 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class InvokeAzureAgentTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void LiteralConversation()
{
// Act, Assert
this.ExecuteTest(
nameof(LiteralConversation),
StringExpression.Literal("asst_123abc"),
StringExpression.Literal("conv_123abc"),
messagesVariable: null);
}
[Fact]
public void VariableConversation()
{
// Act, Assert
this.ExecuteTest(
nameof(VariableConversation),
StringExpression.Variable(PropertyPath.GlobalVariable("TestAgent")),
StringExpression.Variable(PropertyPath.TopicVariable("TestConversation")),
"MyMessages",
BoolExpression.Literal(true));
}
[Fact]
public void ExpressionAutosend()
{
// Act, Assert
this.ExecuteTest(
nameof(VariableConversation),
StringExpression.Literal("asst_123abc"),
StringExpression.Variable(PropertyPath.TopicVariable("TestConversation")),
"MyMessages",
BoolExpression.Expression("1 < 2"));
}
[Fact]
public void InputMessagesVariable()
{
// Act, Assert
this.ExecuteTest(
nameof(VariableConversation),
StringExpression.Literal("asst_123abc"),
StringExpression.Variable(PropertyPath.TopicVariable("TestConversation")),
"MyMessages",
messages: ValueExpression.Variable(PropertyPath.TopicVariable("TestConversation")));
}
[Fact]
public void InputMessagesExpression()
{
// Act, Assert
this.ExecuteTest(
nameof(VariableConversation),
StringExpression.Literal("asst_123abc"),
StringExpression.Literal("conv_123abc"),
"MyMessages",
messages: ValueExpression.Expression("[UserMessage(System.LastMessageText)]"));
}
private void ExecuteTest(
string displayName,
StringExpression.Builder agentName,
StringExpression.Builder conversation,
string? messagesVariable = null,
BoolExpression.Builder? autoSend = null,
ValueExpression.Builder? messages = null)
{
// Arrange
InvokeAzureAgent model =
this.CreateModel(
displayName,
agentName,
conversation,
messagesVariable,
autoSend,
messages);
// Act
InvokeAzureAgentTemplate template = new(model);
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertGeneratedCode<AgentExecutor>(template.Id, workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
AssertOptionalAssignment(model.Output?.Messages?.Path, workflowCode);
}
private InvokeAzureAgent CreateModel(
string displayName,
StringExpression.Builder agentName,
StringExpression.Builder conversation,
string? messagesVariable = null,
BoolExpression.Builder? autoSend = null,
ValueExpression.Builder? messages = null)
{
InitializablePropertyPath? outputMessages = null;
if (messagesVariable is not null)
{
outputMessages = PropertyPath.Create(FormatVariablePath(messagesVariable));
}
InvokeAzureAgent.Builder actionBuilder =
new()
{
Id = this.CreateActionId("invoke_agent"),
DisplayName = this.FormatDisplayName(displayName),
ConversationId = conversation,
Agent =
new AzureAgentUsage.Builder
{
Name = agentName,
},
Input =
new AzureAgentInput.Builder
{
Messages = messages,
},
Output =
new AzureAgentOutput.Builder
{
AutoSend = autoSend,
Messages = outputMessages,
},
};
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,114 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class ProviderTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public async Task WithNamespaceAsync()
{
await this.ExecuteTestAsync(
[
"""
internal sealed class TestExecutor1() : ActionExecutor(id: "test_1")
{
protected override ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
// Nothing to do
return default;
}
}
"""
],
[
"""
TestExecutor1 test1 = new();
"""
],
[
"""
builder.AddEdge(builder.Root, test1);
"""
],
"Test.Workflows.Generated");
}
[Fact]
public async Task WithoutNamespaceAsync()
{
await this.ExecuteTestAsync(
[
"""
internal sealed class TestExecutor1() : ActionExecutor(id: "test_1")
{
protected override ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
// Nothing to do
return default;
}
}
internal sealed class TestExecutor2() : ActionExecutor(id: "test_2")
{
protected override ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
// Nothing to do
return default;
}
}
"""
],
[
"""
TestExecutor1 test1 = new();
TestExecutor2 test2 = new();
"""
],
[
"""
builder.AddEdge(builder.Root, test1);
builder.AddEdge(test1, test2);
"""
]);
}
private async Task ExecuteTestAsync(
string[] executors,
string[] instances,
string[] edges,
string? workflowNamespace = null)
{
// Arrange
ProviderTemplate template = new("worflow-id", executors, instances, edges) { Namespace = workflowNamespace };
// Act
string workflowCode = template.TransformText();
// Assert
this.Output.WriteLine(workflowCode);
Assert.True(Contains(executors));
Assert.True(Contains(instances));
Assert.True(Contains(edges));
bool Contains(string[] code)
{
foreach (string block in code)
{
foreach (string line in block.Split('\n'))
{
if (!workflowCode.Contains(line.Trim()))
{
return false;
}
}
}
return true;
}
}
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class ResetVariableTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void ResetVariable()
{
// Act, Assert
this.ExecuteTest(nameof(ResetVariable), "TestVariable");
}
private void ExecuteTest(string displayName, string variableName)
{
// Arrange
ResetVariable model =
this.CreateModel(
displayName,
FormatVariablePath(variableName));
// Act
ResetVariableTemplate template = new(model);
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertGeneratedCode<ActionExecutor>(template.Id, workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
}
private ResetVariable CreateModel(string displayName, string variablePath)
{
ResetVariable.Builder actionBuilder =
new()
{
Id = this.CreateActionId("set_variable"),
DisplayName = this.FormatDisplayName(displayName),
Variable = PropertyPath.Create(variablePath)
};
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,76 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class RetrieveConversationMessageTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void RetrieveConversationVariable()
{
// Act, Assert
this.ExecuteTest(
nameof(RetrieveConversationVariable),
"TestVariable",
StringExpression.Variable(PropertyPath.TopicVariable("TestConversation")),
StringExpression.Literal("#mid_43"));
}
[Fact]
public void RetrieveMessageVariable()
{
// Act, Assert
this.ExecuteTest(
nameof(RetrieveMessageVariable),
"TestVariable",
StringExpression.Literal("#cid_3"),
StringExpression.Variable(PropertyPath.TopicVariable("TestMessage")));
}
private void ExecuteTest(
string displayName,
string variableName,
StringExpression conversationExpression,
StringExpression messageExpression)
{
// Arrange
RetrieveConversationMessage model =
this.CreateModel(
displayName,
FormatVariablePath(variableName),
conversationExpression,
messageExpression);
// Act
RetrieveConversationMessageTemplate template = new(model);
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertGeneratedCode<ActionExecutor>(template.Id, workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
AssertGeneratedAssignment(model.Message?.Path, workflowCode);
}
private RetrieveConversationMessage CreateModel(
string displayName,
string variableName,
StringExpression conversationExpression,
StringExpression messageExpression)
{
RetrieveConversationMessage.Builder actionBuilder =
new()
{
Id = this.CreateActionId("retrieve_message"),
DisplayName = this.FormatDisplayName(displayName),
Message = PropertyPath.Create(variableName),
ConversationId = conversationExpression,
MessageId = messageExpression,
};
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,137 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class RetrieveConversationMessagesTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void DefaultQuery()
{
// Act, Assert
this.ExecuteTest(
nameof(DefaultQuery),
"TestVariable",
StringExpression.Variable(PropertyPath.TopicVariable("TestConversation")));
}
[Fact]
public void LimitCountQuery()
{
// Act, Assert
this.ExecuteTest(
nameof(DefaultQuery),
"TestVariable",
StringExpression.Literal("#cid_3"),
limit: IntExpression.Literal(94));
}
[Fact]
public void AfterMessageQuery()
{
// Act, Assert
this.ExecuteTest(
nameof(DefaultQuery),
"TestVariable",
StringExpression.Literal("#cid_3"),
after: StringExpression.Literal("#mid_43"));
}
[Fact]
public void BeforeMessageQuery()
{
// Act, Assert
this.ExecuteTest(
nameof(DefaultQuery),
"TestVariable",
StringExpression.Literal("#cid_3"),
before: StringExpression.Literal("#mid_43"));
}
[Fact]
public void NewestFirstQuery()
{
// Act, Assert
this.ExecuteTest(
nameof(DefaultQuery),
"TestVariable",
StringExpression.Literal("#cid_3"),
sortOrder: EnumExpression<AgentMessageSortOrderWrapper>.Literal(AgentMessageSortOrderWrapper.Get(AgentMessageSortOrder.NewestFirst)));
}
private void ExecuteTest(
string displayName,
string variableName,
StringExpression conversation,
IntExpression? limit = null,
StringExpression? after = null,
StringExpression? before = null,
EnumExpression<AgentMessageSortOrderWrapper>? sortOrder = null)
{
// Arrange
RetrieveConversationMessages model =
this.CreateModel(
displayName,
FormatVariablePath(variableName),
conversation,
limit,
after,
before,
sortOrder);
// Act
RetrieveConversationMessagesTemplate template = new(model);
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertGeneratedCode<ActionExecutor>(template.Id, workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
AssertGeneratedAssignment(model.Messages?.Path, workflowCode);
}
private RetrieveConversationMessages CreateModel(
string displayName,
string variableName,
StringExpression conversationExpression,
IntExpression? limitExpression,
StringExpression? afterExpression,
StringExpression? beforeExpression,
EnumExpression<AgentMessageSortOrderWrapper>? sortExpression)
{
RetrieveConversationMessages.Builder actionBuilder =
new()
{
Id = this.CreateActionId("retrieve_messages"),
DisplayName = this.FormatDisplayName(displayName),
Messages = PropertyPath.Create(variableName),
ConversationId = conversationExpression,
};
if (limitExpression is not null)
{
actionBuilder.Limit = limitExpression;
}
if (afterExpression is not null)
{
actionBuilder.MessageAfter = afterExpression;
}
if (beforeExpression is not null)
{
actionBuilder.MessageBefore = beforeExpression;
}
if (sortExpression is not null)
{
actionBuilder.SortOrder = sortExpression;
}
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class SetMultipleVariablesTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void InitializeMultipleValues()
{
// Act, Assert
this.ExecuteTest(
nameof(InitializeMultipleValues),
new AssignmentCase("TestVariable1", new ValueExpression.Builder(ValueExpression.Literal(new NumberDataValue(420))), FormulaValue.New(420)),
new AssignmentCase("TestVariable2", new ValueExpression.Builder(ValueExpression.Variable(PropertyPath.TopicVariable("MyValue"))), FormulaValue.New(6)),
new AssignmentCase("TestVariable3", new ValueExpression.Builder(ValueExpression.Expression("9 - 3")), FormulaValue.New(6)));
}
private void ExecuteTest(string displayName, params AssignmentCase[] assignments)
{
// Arrange
SetMultipleVariables model =
this.CreateModel(
displayName,
assignments);
// Act
SetMultipleVariablesTemplate template = new(model);
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertGeneratedCode<ActionExecutor>(template.Id, workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
foreach (AssignmentCase assignment in assignments)
{
AssertGeneratedAssignment(PropertyPath.TopicVariable(assignment.Path), workflowCode);
}
}
private SetMultipleVariables CreateModel(string displayName, params AssignmentCase[] assignments)
{
SetMultipleVariables.Builder actionBuilder =
new()
{
Id = this.CreateActionId("set_multiple"),
DisplayName = this.FormatDisplayName(displayName),
};
foreach (AssignmentCase assignment in assignments)
{
actionBuilder.Assignments.Add(
new VariableAssignment.Builder()
{
Variable = PropertyPath.Create(FormatVariablePath(assignment.Path)),
Value = assignment.Expression,
});
}
return actionBuilder.Build();
}
private sealed record AssignmentCase(string Path, ValueExpression.Builder Expression, FormulaValue Expected);
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class SetTextVariableTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void InitializeTemplate()
{
// Act, Assert
this.ExecuteTest(nameof(InitializeTemplate), "TestVariable", "Value: {OtherVar}");
}
private void ExecuteTest(
string displayName,
string variableName,
string textValue)
{
// Arrange
SetTextVariable model =
this.CreateModel(
displayName,
FormatVariablePath(variableName),
textValue);
// Act
SetTextVariableTemplate template = new(model);
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertGeneratedCode<ActionExecutor>(template.Id, workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
AssertGeneratedAssignment(model.Variable?.Path, workflowCode);
Assert.Contains(textValue, workflowCode);
}
private SetTextVariable CreateModel(string displayName, string variablePath, string textValue)
{
SetTextVariable.Builder actionBuilder =
new()
{
Id = this.CreateActionId("set_variable"),
DisplayName = this.FormatDisplayName(displayName),
Variable = PropertyPath.Create(variablePath),
Value = TemplateLine.Parse(textValue),
};
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.CodeGen;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
public class SetVariableTemplateTest(ITestOutputHelper output) : WorkflowActionTemplateTest(output)
{
[Fact]
public void InitializeLiteralValue()
{
// Arrange
ValueExpression.Builder expressionBuilder = new(ValueExpression.Literal(new NumberDataValue(420)));
// Act, Assert
this.ExecuteTest(nameof(InitializeLiteralValue), "TestVariable", expressionBuilder, FormulaValue.New(420));
}
[Fact]
public void InitializeVariable()
{
// Arrange
ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("MyValue")));
// Act, Assert
this.ExecuteTest(nameof(InitializeVariable), "TestVariable", expressionBuilder, FormulaValue.New(6));
}
[Fact]
public void InitializeExpression()
{
ValueExpression.Builder expressionBuilder = new(ValueExpression.Expression("9 - 3"));
// Act, Assert
this.ExecuteTest(nameof(InitializeExpression), "TestVariable", expressionBuilder, FormulaValue.New(6));
}
private void ExecuteTest(
string displayName,
string variableName,
ValueExpression.Builder valueExpression,
FormulaValue expectedValue)
{
// Arrange
SetVariable model =
this.CreateModel(
displayName,
FormatVariablePath(variableName),
valueExpression);
// Act
SetVariableTemplate template = new(model);
string workflowCode = template.TransformText();
this.Output.WriteLine(workflowCode.Trim());
// Assert
AssertGeneratedCode<ActionExecutor>(template.Id, workflowCode);
AssertAgentProvider(template.UseAgentProvider, workflowCode);
AssertGeneratedAssignment(model.Variable?.Path, workflowCode);
}
private SetVariable CreateModel(string displayName, string variablePath, ValueExpression.Builder valueExpression)
{
SetVariable.Builder actionBuilder =
new()
{
Id = this.CreateActionId("set_variable"),
DisplayName = this.FormatDisplayName(displayName),
Variable = PropertyPath.Create(variablePath),
Value = valueExpression,
};
return actionBuilder.Build();
}
}

View File

@@ -0,0 +1,67 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.CodeGen;
/// <summary>
/// Base test class for text template.
/// </summary>
public abstract class WorkflowActionTemplateTest(ITestOutputHelper output) : WorkflowTest(output)
{
private int ActionIndex { get; set; } = 1;
#pragma warning disable CA1308 // Normalize strings to uppercase
protected ActionId CreateActionId(string seed) => new($"{seed.ToLowerInvariant()}_{this.ActionIndex++}");
#pragma warning restore CA1308 // Normalize strings to uppercase
protected string FormatDisplayName(string name) => $"{this.GetType().Name}_{name}";
protected static void AssertGeneratedCode<TBase>(string actionId, string workflowCode) where TBase : class
{
Assert.Contains($"internal sealed class {actionId.FormatType()}", workflowCode);
Assert.Contains($") : {typeof(TBase).Name}(", workflowCode);
Assert.Contains(@$"""{actionId}""", workflowCode);
}
protected static void AssertGeneratedMethod(string methodName, string workflowCode) =>
Assert.Contains($"ValueTask {methodName}(", workflowCode);
protected static void AssertAgentProvider(bool expected, string workflowCode)
{
if (expected)
{
Assert.Contains(", WorkflowAgentProvider agentProvider", workflowCode);
}
else
{
Assert.DoesNotContain(", WorkflowAgentProvider agentProvider", workflowCode);
}
}
protected static void AssertOptionalAssignment(PropertyPath? variablePath, string workflowCode)
{
if (variablePath is not null)
{
Assert.Contains(@$"key: ""{variablePath.VariableName}""", workflowCode);
Assert.Contains(@$"scopeName: ""{variablePath.NamespaceAlias}""", workflowCode);
}
}
protected static void AssertGeneratedAssignment(PropertyPath? variablePath, string workflowCode)
{
Assert.NotNull(variablePath);
Assert.Contains(@$"key: ""{variablePath.VariableName}""", workflowCode);
Assert.Contains(@$"scopeName: ""{variablePath.NamespaceAlias}""", workflowCode);
}
protected static void AssertDelegate(string actionId, string rootId, string workflowCode)
{
Assert.Contains($"{nameof(DelegateExecutor)} {actionId.FormatName()} = new(", workflowCode);
Assert.Contains(@$"""{actionId}""", workflowCode);
Assert.Contains($"{rootId.FormatName()}.Session", workflowCode);
}
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
public class DeclarativeWorkflowContextTests
{
[Fact]
public void InitializeDefaultValues()
{
// Act
Mock<WorkflowAgentProvider> mockProvider = new(MockBehavior.Strict);
DeclarativeWorkflowOptions context = new(mockProvider.Object);
// Assert
Assert.Equal(mockProvider.Object, context.AgentProvider);
Assert.Null(context.MaximumCallDepth);
Assert.Null(context.MaximumExpressionLength);
Assert.Same(NullLoggerFactory.Instance, context.LoggerFactory);
}
[Fact]
public void InitializeExplicitValues()
{
// Arrange
TokenCredential credentials = new DefaultAzureCredential();
const int MaxCallDepth = 10;
const int MaxExpressionLength = 100;
ILoggerFactory loggerFactory = LoggerFactory.Create(builder => { });
// Act
Mock<WorkflowAgentProvider> mockProvider = new(MockBehavior.Strict);
DeclarativeWorkflowOptions context = new(mockProvider.Object)
{
MaximumCallDepth = MaxCallDepth,
MaximumExpressionLength = MaxExpressionLength,
LoggerFactory = loggerFactory
};
// Assert
Assert.Equal(mockProvider.Object, context.AgentProvider);
Assert.Equal(MaxCallDepth, context.MaximumCallDepth);
Assert.Equal(MaxExpressionLength, context.MaximumExpressionLength);
Assert.Same(loggerFactory, context.LoggerFactory);
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
/// <summary>
/// Tests declarative workflow exceptions.
/// </summary>
public sealed class DeclarativeWorkflowExceptionTest(ITestOutputHelper output) : WorkflowTest(output)
{
[Fact]
public void WorkflowExecutionException()
{
AssertDefault<DeclarativeActionException>(() => throw new DeclarativeActionException());
AssertMessage<DeclarativeActionException>((message) => throw new DeclarativeActionException(message));
AssertInner<DeclarativeActionException>((message, inner) => throw new DeclarativeActionException(message, inner));
}
[Fact]
public void WorkflowModelException()
{
AssertDefault<DeclarativeModelException>(() => throw new DeclarativeModelException());
AssertMessage<DeclarativeModelException>((message) => throw new DeclarativeModelException(message));
AssertInner<DeclarativeModelException>((message, inner) => throw new DeclarativeModelException(message, inner));
}
private static void AssertDefault<TException>(Action throwAction) where TException : Exception
{
TException exception = Assert.Throws<TException>(throwAction.Invoke);
Assert.NotEmpty(exception.Message);
Assert.Null(exception.InnerException);
}
private static void AssertMessage<TException>(Action<string> throwAction) where TException : Exception
{
const string Message = "Test exception message";
TException exception = Assert.Throws<TException>(() => throwAction.Invoke(Message));
Assert.Equal(Message, exception.Message);
Assert.Null(exception.InnerException);
}
private static void AssertInner<TException>(Action<string, Exception> throwAction) where TException : Exception
{
const string Message = "Test exception message";
NotSupportedException innerException = new("Inner exception message");
TException exception = Assert.Throws<TException>(() => throwAction.Invoke(Message, innerException));
Assert.Equal(Message, exception.Message);
Assert.Equal(innerException, exception.InnerException);
}
}

View File

@@ -0,0 +1,386 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Moq;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
/// <summary>
/// Tests execution of workflow created by <see cref="DeclarativeWorkflowBuilder"/>.
/// </summary>
public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : WorkflowTest(output)
{
private List<WorkflowEvent> WorkflowEvents { get; } = [];
private Dictionary<Type, int> WorkflowEventCounts { get; set; } = [];
[Theory]
[InlineData("BadEmpty.yaml")]
[InlineData("BadId.yaml")]
[InlineData("BadKind.yaml")]
public async Task InvalidWorkflowAsync(string workflowFile)
{
await Assert.ThrowsAsync<DeclarativeModelException>(() => this.RunWorkflowAsync(workflowFile));
this.AssertNotExecuted("end_all");
}
[Fact]
public async Task LoopEachActionAsync()
{
await this.RunWorkflowAsync("LoopEach.yaml");
this.AssertExecutionCount(expectedCount: 34);
this.AssertExecuted("foreach_loop");
this.AssertExecuted("set_variable_inner");
this.AssertExecuted("send_activity_inner");
this.AssertExecuted("end_all");
}
[Fact]
public async Task LoopBreakActionAsync()
{
await this.RunWorkflowAsync("LoopBreak.yaml");
this.AssertExecutionCount(expectedCount: 6);
this.AssertExecuted("foreach_loop");
this.AssertExecuted("break_loop_now");
this.AssertExecuted("end_all");
this.AssertNotExecuted("set_variable_inner");
this.AssertNotExecuted("send_activity_inner");
}
[Fact]
public async Task LoopContinueActionAsync()
{
await this.RunWorkflowAsync("LoopContinue.yaml");
this.AssertExecutionCount(expectedCount: 22);
this.AssertExecuted("foreach_loop");
this.AssertExecuted("continue_loop_now");
this.AssertExecuted("end_all");
this.AssertNotExecuted("set_variable_inner");
this.AssertNotExecuted("send_activity_inner");
}
[Fact]
public async Task EndConversationActionAsync()
{
await this.RunWorkflowAsync("EndConversation.yaml");
this.AssertExecutionCount(expectedCount: 1);
this.AssertExecuted("end_all");
this.AssertNotExecuted("sendActivity_1");
}
[Fact]
public async Task GotoActionAsync()
{
await this.RunWorkflowAsync("Goto.yaml");
this.AssertExecutionCount(expectedCount: 2);
this.AssertExecuted("goto_end");
this.AssertExecuted("end_all");
this.AssertNotExecuted("sendActivity_1");
this.AssertNotExecuted("sendActivity_2");
this.AssertNotExecuted("sendActivity_3");
}
[Theory]
[InlineData(12)]
[InlineData(37)]
public async Task ConditionActionAsync(int input)
{
await this.RunWorkflowAsync("Condition.yaml", input);
this.AssertExecutionCount(expectedCount: 9);
this.AssertExecuted("setVariable_test");
this.AssertExecuted("conditionGroup_test");
if (input % 2 == 0)
{
this.AssertExecuted("conditionItem_even", isScope: true);
this.AssertExecuted("sendActivity_even");
this.AssertNotExecuted("conditionItem_odd");
this.AssertNotExecuted("sendActivity_odd");
this.AssertMessage("EVEN");
}
else
{
this.AssertExecuted("conditionItem_odd", isScope: true);
this.AssertExecuted("sendActivity_odd");
this.AssertNotExecuted("conditionItem_even");
this.AssertNotExecuted("sendActivity_even");
this.AssertMessage("ODD");
}
this.AssertExecuted("activity_final");
}
[Theory]
[InlineData(12, 7)]
[InlineData(37, 9)]
public async Task ConditionActionWithElseAsync(int input, int expectedActions)
{
await this.RunWorkflowAsync("ConditionElse.yaml", input);
this.AssertExecutionCount(expectedActions);
this.AssertExecuted("setVariable_test");
this.AssertExecuted("conditionGroup_test");
if (input % 2 == 0)
{
this.AssertExecuted("sendActivity_else", isScope: true);
this.AssertNotExecuted("conditionItem_odd");
this.AssertNotExecuted("sendActivity_odd");
}
else
{
this.AssertExecuted("conditionItem_odd", isScope: true);
this.AssertExecuted("sendActivity_odd");
this.AssertNotExecuted("sendActivity_else");
}
this.AssertExecuted("activity_final");
}
[Theory]
[InlineData(12, 4)]
[InlineData(37, 9)]
public async Task ConditionActionWithFallThroughAsync(int input, int expectedActions)
{
await this.RunWorkflowAsync("ConditionFallThrough.yaml", input);
this.AssertExecutionCount(expectedActions);
this.AssertExecuted("setVariable_test");
this.AssertExecuted("conditionGroup_test", isScope: true);
if (input % 2 == 0)
{
this.AssertNotExecuted("conditionItem_odd");
this.AssertNotExecuted("sendActivity_odd");
}
else
{
this.AssertExecuted("conditionItem_odd", isScope: true);
this.AssertExecuted("sendActivity_odd");
this.AssertMessage("ODD");
}
this.AssertExecuted("activity_final");
}
[Theory]
[InlineData("CancelWorkflow.yaml", 1, "end_all")]
[InlineData("EndConversation.yaml", 1, "end_all")]
[InlineData("EndWorkflow.yaml", 1, "end_all")]
[InlineData("EditTable.yaml", 2, "edit_var")]
[InlineData("EditTableV2.yaml", 2, "edit_var")]
[InlineData("ParseValue.yaml", 2, "parse_var")]
[InlineData("ParseValueList.yaml", 2, "parse_var")]
[InlineData("SendActivity.yaml", 2, "activity_input")]
[InlineData("SetVariable.yaml", 1, "set_var")]
[InlineData("SetTextVariable.yaml", 1, "set_text")]
[InlineData("ClearAllVariables.yaml", 1, "clear_all")]
[InlineData("ResetVariable.yaml", 2, "clear_var")]
[InlineData("MixedScopes.yaml", 2, "activity_input")]
[InlineData("CaseInsensitive.yaml", 6, "end_when_match")]
public async Task ExecuteActionAsync(string workflowFile, int expectedCount, string expectedId)
{
await this.RunWorkflowAsync(workflowFile);
this.AssertExecutionCount(expectedCount);
this.AssertExecuted(expectedId);
}
[Theory]
[InlineData(typeof(ActivateExternalTrigger.Builder))]
[InlineData(typeof(AdaptiveCardPrompt.Builder))]
[InlineData(typeof(BeginDialog.Builder))]
[InlineData(typeof(CSATQuestion.Builder))]
[InlineData(typeof(CreateSearchQuery.Builder))]
[InlineData(typeof(DeleteActivity.Builder))]
[InlineData(typeof(DisableTrigger.Builder))]
[InlineData(typeof(DisconnectedNodeContainer.Builder))]
[InlineData(typeof(EmitEvent.Builder))]
[InlineData(typeof(GetActivityMembers.Builder))]
[InlineData(typeof(GetConversationMembers.Builder))]
[InlineData(typeof(HttpRequestAction.Builder))]
[InlineData(typeof(InvokeAIBuilderModelAction.Builder))]
[InlineData(typeof(InvokeConnectorAction.Builder))]
[InlineData(typeof(InvokeCustomModelAction.Builder))]
[InlineData(typeof(InvokeFlowAction.Builder))]
[InlineData(typeof(InvokeSkillAction.Builder))]
[InlineData(typeof(LogCustomTelemetryEvent.Builder))]
[InlineData(typeof(OAuthInput.Builder))]
[InlineData(typeof(RecognizeIntent.Builder))]
[InlineData(typeof(RepeatDialog.Builder))]
[InlineData(typeof(ReplaceDialog.Builder))]
[InlineData(typeof(SearchAndSummarizeContent.Builder))]
[InlineData(typeof(SearchAndSummarizeWithCustomModel.Builder))]
[InlineData(typeof(SearchKnowledgeSources.Builder))]
[InlineData(typeof(SignOutUser.Builder))]
[InlineData(typeof(TransferConversation.Builder))]
[InlineData(typeof(TransferConversationV2.Builder))]
[InlineData(typeof(UnknownDialogAction.Builder))]
[InlineData(typeof(UpdateActivity.Builder))]
[InlineData(typeof(WaitForConnectorTrigger.Builder))]
public void UnsupportedAction(Type type)
{
DialogAction.Builder? unsupportedAction = (DialogAction.Builder?)Activator.CreateInstance(type);
Assert.NotNull(unsupportedAction);
unsupportedAction.Id = "action_bad";
AdaptiveDialog.Builder dialogBuilder =
new()
{
BeginDialog =
new OnActivity.Builder()
{
Id = "anything",
Actions = [unsupportedAction]
}
};
AdaptiveDialog dialog = dialogBuilder.Build();
WorkflowFormulaState state = new(RecalcEngineFactory.Create());
Mock<WorkflowAgentProvider> mockAgentProvider = CreateMockProvider("1");
DeclarativeWorkflowOptions options = new(mockAgentProvider.Object);
WorkflowActionVisitor visitor = new(new DeclarativeWorkflowExecutor<string>(WorkflowActionVisitor.Steps.Root("anything"), options, state, (message) => DeclarativeWorkflowBuilder.DefaultTransform(message)), state, options);
WorkflowElementWalker walker = new(visitor);
walker.Visit(dialog);
Assert.True(visitor.HasUnsupportedActions);
}
[Theory]
[InlineData("CaseInsensitive.yaml", "end_when_match")]
[InlineData("ClearAllVariables.yaml", "clear_all")]
[InlineData("Condition.yaml", "setVariable_test")]
[InlineData("ConditionElse.yaml", "setVariable_test")]
[InlineData("EndConversation.yaml", "end_all")]
[InlineData("EndWorkflow.yaml", "end_all")]
[InlineData("EditTable.yaml", "edit_var")]
[InlineData("EditTableV2.yaml", "edit_var")]
[InlineData("Goto.yaml", "goto_end")]
[InlineData("LoopBreak.yaml", "break_loop_now")]
[InlineData("LoopContinue.yaml", "foreach_loop")]
[InlineData("LoopEach.yaml", "foreach_loop")]
[InlineData("MixedScopes.yaml", "activity_input")]
[InlineData("ParseValue.yaml", "parse_var")]
[InlineData("ParseValueList.yaml", "parse_var")]
[InlineData("ResetVariable.yaml", "clear_var")]
[InlineData("SendActivity.yaml", "activity_input")]
[InlineData("SetVariable.yaml", "set_var")]
[InlineData("SetTextVariable.yaml", "set_text")]
public async Task CancelRunAsync(string workflowPath, string expectedExecutedId)
{
// Arrange
const string WorkflowInput = "Test input message";
Workflow workflow = this.CreateWorkflow(workflowPath, WorkflowInput);
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow: workflow, input: WorkflowInput);
// Act
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
{
this.WorkflowEvents.Add(workflowEvent);
if (workflowEvent is DeclarativeActionInvokedEvent actionInvokedEvent && actionInvokedEvent.ActionId == expectedExecutedId)
{
// Cancel run after the specified declarative action is invoked.
await run.CancelRunAsync();
}
}
RunStatus currentRunStatus = await run.GetStatusAsync();
this.WorkflowEventCounts = this.WorkflowEvents.GroupBy(e => e.GetType()).ToDictionary(e => e.Key, e => e.Count());
// Assert
Assert.Equal(expected: RunStatus.Ended, actual: currentRunStatus);
Assert.NotEmpty(this.WorkflowEventCounts);
Assert.Contains(this.WorkflowEvents.OfType<DeclarativeActionInvokedEvent>(), e => e.ActionId == expectedExecutedId);
Assert.DoesNotContain(this.WorkflowEvents.OfType<DeclarativeActionCompletedEvent>(), e => e.ActionId == expectedExecutedId);
}
private void AssertExecutionCount(int expectedCount)
{
Assert.Equal(expectedCount + 2, this.WorkflowEventCounts[typeof(ExecutorInvokedEvent)]);
Assert.Equal(expectedCount + 2, this.WorkflowEventCounts[typeof(ExecutorCompletedEvent)]);
}
private void AssertNotExecuted(string executorId)
{
Assert.DoesNotContain(this.WorkflowEvents.OfType<ExecutorInvokedEvent>(), e => e.ExecutorId == executorId);
Assert.DoesNotContain(this.WorkflowEvents.OfType<ExecutorCompletedEvent>(), e => e.ExecutorId == executorId);
}
private void AssertExecuted(string executorId, bool isScope = false)
{
Assert.Contains(this.WorkflowEvents.OfType<ExecutorInvokedEvent>(), e => e.ExecutorId == executorId);
Assert.Contains(this.WorkflowEvents.OfType<ExecutorCompletedEvent>(), e => e.ExecutorId == executorId);
if (!isScope)
{
Assert.Contains(this.WorkflowEvents.OfType<DeclarativeActionInvokedEvent>(), e => e.ActionId == executorId);
Assert.Contains(this.WorkflowEvents.OfType<DeclarativeActionCompletedEvent>(), e => e.ActionId == executorId);
}
}
private void AssertMessage(string message) =>
Assert.Contains(this.WorkflowEvents.OfType<MessageActivityEvent>(), e => string.Equals(e.Message.Trim(), message, StringComparison.Ordinal));
private Task RunWorkflowAsync(string workflowPath) =>
this.RunWorkflowAsync(workflowPath, "Test input message");
private async Task RunWorkflowAsync<TInput>(string workflowPath, TInput workflowInput) where TInput : notnull
{
Workflow workflow = this.CreateWorkflow(workflowPath, workflowInput);
await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, workflowInput);
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
{
this.WorkflowEvents.Add(workflowEvent);
switch (workflowEvent)
{
case ExecutorInvokedEvent invokeEvent:
ActionExecutorResult? message = invokeEvent.Data as ActionExecutorResult;
this.Output.WriteLine($"EXEC: {invokeEvent.ExecutorId} << {message?.ExecutorId ?? "?"} [{message?.Result ?? "-"}]");
break;
case DeclarativeActionInvokedEvent actionInvokeEvent:
this.Output.WriteLine($"ACTION ENTER: {actionInvokeEvent.ActionId}");
break;
case DeclarativeActionCompletedEvent actionCompleteEvent:
this.Output.WriteLine($"ACTION EXIT: {actionCompleteEvent.ActionId}");
break;
case MessageActivityEvent activityEvent:
this.Output.WriteLine($"ACTIVITY: {activityEvent.Message}");
break;
case AgentResponseEvent messageEvent:
this.Output.WriteLine($"MESSAGE: {messageEvent.Response.Messages[0].Text.Trim()}");
break;
case ExecutorFailedEvent failureEvent:
Console.WriteLine($"Executor failed [{failureEvent.ExecutorId}]: {failureEvent.Data?.Message ?? "Unknown"}");
break;
case WorkflowErrorEvent errorEvent:
throw errorEvent.Data as Exception ?? new XunitException("Unexpected failure...");
}
}
this.WorkflowEventCounts = this.WorkflowEvents.GroupBy(e => e.GetType()).ToDictionary(e => e.Key, e => e.Count());
}
private Workflow CreateWorkflow<TInput>(string workflowPath, TInput workflowInput) where TInput : notnull
{
using StreamReader yamlReader = File.OpenText(Path.Combine("Workflows", workflowPath));
Mock<WorkflowAgentProvider> mockAgentProvider = CreateMockProvider($"{workflowInput}");
DeclarativeWorkflowOptions workflowContext = new(mockAgentProvider.Object) { LoggerFactory = this.Output };
return DeclarativeWorkflowBuilder.Build<TInput>(yamlReader, workflowContext);
}
private static Mock<WorkflowAgentProvider> CreateMockProvider(string input)
{
Mock<WorkflowAgentProvider> mockAgentProvider = new(MockBehavior.Strict);
mockAgentProvider.Setup(provider => provider.CreateConversationAsync(It.IsAny<CancellationToken>())).Returns(() => Task.FromResult(Guid.NewGuid().ToString("N")));
mockAgentProvider.Setup(provider => provider.CreateMessageAsync(It.IsAny<string>(), It.IsAny<ChatMessage>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(new ChatMessage(ChatRole.Assistant, input)));
return mockAgentProvider;
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.Entities;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Entities;
/// <summary>
/// Tests for <see cref="EntityExtractionResult"/>.
/// </summary>
public sealed class EntityExtractionResultTest(ITestOutputHelper output) : WorkflowTest(output)
{
[Fact]
public void ConstructorWithErrorMessage()
{
// Arrange
const string ErrorMessage = "Test error message";
// Act
EntityExtractionResult result = new(ErrorMessage);
// Assert
Assert.Null(result.Value);
Assert.Equal(ErrorMessage, result.ErrorMessage);
Assert.False(result.IsValid);
}
[Fact]
public void ConstructorWithNullValue()
{
// Arrange
FormulaValue? value = null;
// Act
EntityExtractionResult result = new(value);
// Assert
Assert.Null(result.Value);
Assert.Null(result.ErrorMessage);
Assert.False(result.IsValid);
}
[Fact]
public void ConstructorWithNumberValue()
{
// Arrange
FormulaValue value = FormulaValue.New(double.MaxValue);
// Act
EntityExtractionResult result = new(value);
// Assert
NumberValue numberValue = Assert.IsType<NumberValue>(result.Value);
Assert.Equal(double.MaxValue, numberValue.Value);
}
[Fact]
public void ConstructorWithBlankValue_IsValid()
{
// Arrange
FormulaValue value = FormulaValue.NewBlank();
// Act
EntityExtractionResult result = new(value);
// Assert
Assert.Equal(value, result.Value);
Assert.True(result.IsValid);
}
}

View File

@@ -0,0 +1,759 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Agents.AI.Workflows.Declarative.Entities;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Entities;
/// <summary>
/// Tests for <see cref="EntityExtractor"/>.
/// </summary>
public sealed class EntityExtractorTest(ITestOutputHelper output) : WorkflowTest(output)
{
[Fact]
public void Parse_NullEntity_WithNonEmptyValue_ReturnsStringValue()
{
// Arrange
EntityReference? entity = null;
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, "test value");
// Assert
Assert.True(result.IsValid);
Assert.NotNull(result.Value);
StringValue stringValue = Assert.IsType<StringValue>(result.Value);
Assert.Equal("test value", stringValue.Value);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("\t")]
public void Parse_NullEntity_WithEmptyValue_ReturnsBlankValue(string value)
{
// Arrange
EntityReference? entity = null;
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.IsType<BlankValue>(result.Value);
}
[Theory]
[InlineData("true", true)]
[InlineData("false", false)]
[InlineData("True", true)]
[InlineData("False", false)]
[InlineData("TRUE", true)]
[InlineData("FALSE", false)]
public void Parse_BooleanEntity_ValidValue_ReturnsBoolean(string value, bool expected)
{
// Arrange
EntityReference entity = CreateBooleanEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(expected, (result.Value as BooleanValue)?.Value);
}
[Theory]
[InlineData("invalid")]
[InlineData("123")]
[InlineData("yes")]
public void Parse_BooleanEntity_InvalidValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreateBooleanEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Contains("Invalid boolean value", result.ErrorMessage);
}
[Theory]
[InlineData("2023-12-25")]
[InlineData("12/25/2023")]
[InlineData("2023-12-25 10:30:00")]
public void Parse_DateEntity_ValidValue_ReturnsDate(string value)
{
// Arrange
EntityReference entity = CreateDateEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.IsType<DateTimeValue>(result.Value);
}
[Theory]
[InlineData("invalid date")]
[InlineData("not-a-date")]
public void Parse_DateEntity_InvalidValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreateDateEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Contains("Invalid date value", result.ErrorMessage);
}
[Theory]
[InlineData("2023-12-25 10:30:00")]
[InlineData("12/25/2023 10:30:00 AM")]
public void Parse_DateTimeEntity_ValidValue_ReturnsDateTime(string value)
{
// Arrange
EntityReference entity = CreateDateTimeEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.IsType<DateTimeValue>(result.Value);
}
[Theory]
[InlineData("invalid datetime")]
public void Parse_DateTimeEntity_InvalidValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreateDateTimeEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Contains("Invalid date-time value", result.ErrorMessage);
}
[Theory]
[InlineData("2023-12-25 10:30:00")]
[InlineData("12/25/2023 10:30:00")]
public void Parse_DateTimeNoTimeZoneEntity_ValidValue_ReturnsDateTime(string value)
{
// Arrange
EntityReference entity = CreateDateTimeNoTimeZoneEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
DateTimeValue dateTimeValue = Assert.IsType<DateTimeValue>(result.Value);
DateTime dateTime = dateTimeValue.GetConvertedValue(null);
Assert.Equal(DateTime.Parse(value), dateTime);
}
[Theory]
[InlineData("01:30:00")]
[InlineData("1:30:00")]
[InlineData("10.12:30:45")]
public void Parse_DurationEntity_ValidValue_ReturnsDuration(string value)
{
// Arrange
EntityReference entity = CreateDurationEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.IsType<TimeValue>(result.Value);
}
[Theory]
[InlineData("invalid duration")]
[InlineData("not a timespan")]
public void Parse_DurationEntity_InvalidValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreateDurationEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Contains("Invalid duration value", result.ErrorMessage);
}
[Theory]
[InlineData("test@example.com")]
[InlineData("user.name@domain.co.uk")]
public void Parse_EmailEntity_ValidValue_ReturnsEmail(string value)
{
// Arrange
EntityReference entity = CreateEmailEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(value, (result.Value as StringValue)?.Value);
}
[Theory]
[InlineData("invalid email")]
[InlineData("@example.com")]
[InlineData("test@")]
public void Parse_EmailEntity_InvalidValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreateEmailEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Contains("Invalid email value", result.ErrorMessage);
}
[Theory]
[InlineData("123")]
[InlineData("456.78")]
[InlineData("-123.45")]
[InlineData("1,234.56")]
public void Parse_NumberEntity_ValidValue_ReturnsNumber(string value)
{
// Arrange
EntityReference entity = CreateNumberEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.IsType<NumberValue>(result.Value);
}
[Theory]
[InlineData("not a number")]
[InlineData("abc")]
public void Parse_NumberEntity_InvalidValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreateNumberEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Contains("Invalid double value", result.ErrorMessage);
}
[Theory]
[InlineData("25 years")]
[InlineData("30 years old")]
[InlineData("45")]
public void Parse_AgeEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateAgeEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.IsType<StringValue>(result.Value);
}
[Theory]
[InlineData("not an age")]
public void Parse_AgeEntity_InvalidValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreateAgeEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Contains("Invalid age value", result.ErrorMessage);
}
[Theory]
[InlineData("$100")]
[InlineData("100 dollars")]
[InlineData("123.45")]
public void Parse_MoneyEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateMoneyEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.IsType<StringValue>(result.Value);
}
[Theory]
[InlineData("not money")]
public void Parse_MoneyEntity_InvalidValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreateMoneyEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Contains("Invalid money value", result.ErrorMessage);
}
[Theory]
[InlineData("50%")]
[InlineData("75 percent")]
[InlineData("99.5")]
public void Parse_PercentageEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreatePercentageEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.IsType<StringValue>(result.Value);
}
[Theory]
[InlineData("not a percentage")]
public void Parse_PercentageEntity_InvalidValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreatePercentageEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Contains("Invalid percentage value", result.ErrorMessage);
}
[Theory]
[InlineData("60 mph")]
[InlineData("100 km/h")]
[InlineData("25.5")]
public void Parse_SpeedEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateSpeedEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.IsType<StringValue>(result.Value);
}
[Theory]
[InlineData("not a speed")]
public void Parse_SpeedEntity_InvalidValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreateSpeedEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Contains("Invalid speed value", result.ErrorMessage);
}
[Theory]
[InlineData("72°F")]
[InlineData("20°C")]
[InlineData("98.6")]
public void Parse_TemperatureEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateTemperatureEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.IsType<StringValue>(result.Value);
}
[Theory]
[InlineData("not a temperature")]
public void Parse_TemperatureEntity_InvalidValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreateTemperatureEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Contains("Invalid temperature value", result.ErrorMessage);
}
[Theory]
[InlineData("150 lbs")]
[InlineData("70 kg")]
[InlineData("180.5")]
public void Parse_WeightEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateWeightEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.IsType<StringValue>(result.Value);
}
[Theory]
[InlineData("not a weight")]
public void Parse_WeightEntity_InvalidValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreateWeightEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Contains("Invalid weight value", result.ErrorMessage);
}
[Theory]
[InlineData("https://www.example.com", "https://www.example.com/")]
[InlineData("http://test.com/path", "http://test.com/path")]
[InlineData("ftp://files.example.com", "ftp://files.example.com/")]
public void Parse_URLEntity_ValidValue_ReturnsURL(string value, string expected)
{
// Arrange
EntityReference entity = CreateURLEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(expected, (result.Value as StringValue)?.Value);
}
[Theory]
[InlineData("not a url")]
[InlineData("invalid url")]
public void Parse_URLEntity_InvalidValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreateURLEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Contains("Invalid double value", result.ErrorMessage);
}
[Theory]
[InlineData("Seattle")]
[InlineData("New York")]
public void Parse_CityEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateCityEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(value, (result.Value as StringValue)?.Value);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Parse_CityEntity_EmptyValue_ReturnsError(string value)
{
// Arrange
EntityReference entity = CreateCityEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.False(result.IsValid);
Assert.Equal("Empty value", result.ErrorMessage);
}
[Theory]
[InlineData("Washington")]
[InlineData("California")]
public void Parse_StateEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateStateEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(value, (result.Value as StringValue)?.Value);
}
[Theory]
[InlineData("USA")]
[InlineData("United Kingdom")]
public void Parse_CountryOrRegionEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateCountryOrRegionEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(value, (result.Value as StringValue)?.Value);
}
[Theory]
[InlineData("Europe")]
[InlineData("Asia")]
public void Parse_ContinentEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateContinentEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(value, (result.Value as StringValue)?.Value);
}
[Theory]
[InlineData("123 Main Street")]
[InlineData("456 Oak Avenue")]
public void Parse_StreetAddressEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateStreetAddressEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(value, (result.Value as StringValue)?.Value);
}
[Theory]
[InlineData("+1-555-1234")]
[InlineData("(555) 123-4567")]
public void Parse_PhoneNumberEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreatePhoneNumberEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(value, (result.Value as StringValue)?.Value);
}
[Theory]
[InlineData("red")]
[InlineData("blue")]
public void Parse_ColorEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateColorEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(value, (result.Value as StringValue)?.Value);
}
[Theory]
[InlineData("English")]
[InlineData("Spanish")]
public void Parse_LanguageEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateLanguageEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(value, (result.Value as StringValue)?.Value);
}
[Theory]
[InlineData("Conference")]
[InlineData("Meeting")]
public void Parse_EventEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateEventEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(value, (result.Value as StringValue)?.Value);
}
[Theory]
[InlineData("Starbucks")]
[InlineData("Museum")]
public void Parse_PointOfInterestEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreatePointOfInterestEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(value, (result.Value as StringValue)?.Value);
}
[Theory]
[InlineData("test string")]
[InlineData("any text")]
public void Parse_StringEntity_ValidValue_ReturnsString(string value)
{
// Arrange
EntityReference entity = CreateStringEntity();
// Act
EntityExtractionResult result = EntityExtractor.Parse(entity, value);
// Assert
Assert.True(result.IsValid);
Assert.Equal(value, (result.Value as StringValue)?.Value);
}
private static BooleanPrebuiltEntity CreateBooleanEntity() =>
new BooleanPrebuiltEntity.Builder().Build();
private static DatePrebuiltEntity CreateDateEntity() =>
new DatePrebuiltEntity.Builder().Build();
private static DateTimePrebuiltEntity CreateDateTimeEntity() =>
new DateTimePrebuiltEntity.Builder().Build();
private static DateTimeNoTimeZonePrebuiltEntity CreateDateTimeNoTimeZoneEntity() =>
new DateTimeNoTimeZonePrebuiltEntity.Builder().Build();
private static DurationPrebuiltEntity CreateDurationEntity() =>
new DurationPrebuiltEntity.Builder().Build();
private static EmailPrebuiltEntity CreateEmailEntity() =>
new EmailPrebuiltEntity.Builder().Build();
private static NumberPrebuiltEntity CreateNumberEntity() =>
new NumberPrebuiltEntity.Builder().Build();
private static AgePrebuiltEntity CreateAgeEntity() =>
new AgePrebuiltEntity.Builder().Build();
private static MoneyPrebuiltEntity CreateMoneyEntity() =>
new MoneyPrebuiltEntity.Builder().Build();
private static PercentagePrebuiltEntity CreatePercentageEntity() =>
new PercentagePrebuiltEntity.Builder().Build();
private static SpeedPrebuiltEntity CreateSpeedEntity() =>
new SpeedPrebuiltEntity.Builder().Build();
private static TemperaturePrebuiltEntity CreateTemperatureEntity() =>
new TemperaturePrebuiltEntity.Builder().Build();
private static WeightPrebuiltEntity CreateWeightEntity() =>
new WeightPrebuiltEntity.Builder().Build();
private static URLPrebuiltEntity CreateURLEntity() =>
new URLPrebuiltEntity.Builder().Build();
private static CityPrebuiltEntity CreateCityEntity() =>
new CityPrebuiltEntity.Builder().Build();
private static StatePrebuiltEntity CreateStateEntity() =>
new StatePrebuiltEntity.Builder().Build();
private static CountryOrRegionPrebuiltEntity CreateCountryOrRegionEntity() =>
new CountryOrRegionPrebuiltEntity.Builder().Build();
private static ContinentPrebuiltEntity CreateContinentEntity() =>
new ContinentPrebuiltEntity.Builder().Build();
private static StreetAddressPrebuiltEntity CreateStreetAddressEntity() =>
new StreetAddressPrebuiltEntity.Builder().Build();
private static PhoneNumberPrebuiltEntity CreatePhoneNumberEntity() =>
new PhoneNumberPrebuiltEntity.Builder().Build();
private static ColorPrebuiltEntity CreateColorEntity() =>
new ColorPrebuiltEntity.Builder().Build();
private static LanguagePrebuiltEntity CreateLanguageEntity() =>
new LanguagePrebuiltEntity.Builder().Build();
private static EventPrebuiltEntity CreateEventEntity() =>
new EventPrebuiltEntity.Builder().Build();
private static PointOfInterestPrebuiltEntity CreatePointOfInterestEntity() =>
new PointOfInterestPrebuiltEntity.Builder().Build();
private static StringPrebuiltEntity CreateStringEntity() =>
new StringPrebuiltEntity.Builder().Build();
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Text.Json;
using Microsoft.Extensions.AI;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
/// <summary>
/// Base class for event tests.
/// </summary>
public abstract class EventTest(ITestOutputHelper output) : WorkflowTest(output)
{
protected static TEvent VerifyEventSerialization<TEvent>(TEvent source)
{
string? text = JsonSerializer.Serialize(source, AIJsonUtilities.DefaultOptions);
Assert.NotNull(text);
TEvent? copy = JsonSerializer.Deserialize<TEvent>(text, AIJsonUtilities.DefaultOptions);
Assert.NotNull(copy);
return copy;
}
protected static void AssertMessage(ChatMessage source, ChatMessage copy)
{
Assert.Equal(source.Role, copy.Role);
Assert.Equal(source.Text, copy.Text);
Assert.Equal(source.Contents.Count, copy.Contents.Count);
}
protected static TContent AssertContent<TContent>(ChatMessage message) where TContent : AIContent
{
TContent[] contents = message.Contents.OfType<TContent>().ToArray();
return Assert.Single(contents);
}
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.Events;
using Microsoft.Extensions.AI;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events;
/// <summary>
/// Verify <see cref="ExternalInputRequest"/> class
/// </summary>
public sealed class ExternalInputRequestTest(ITestOutputHelper output) : EventTest(output)
{
[Fact]
public void VerifySerializationWithText()
{
// Arrange
ExternalInputRequest source = new(new AgentResponse(new ChatMessage(ChatRole.User, "Wassup?")));
// Act
ExternalInputRequest copy = VerifyEventSerialization(source);
// Assert
ChatMessage messageCopy = Assert.Single(source.AgentResponse.Messages);
AssertMessage(messageCopy, copy.AgentResponse.Messages[0]);
}
[Fact]
public void VerifySerializationWithRequests()
{
// Arrange
ExternalInputRequest source =
new(new AgentResponse(
new ChatMessage(
ChatRole.Assistant,
[
new McpServerToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")),
new FunctionApprovalRequestContent("call2", new FunctionCallContent("call2", "result1")),
new FunctionCallContent("call3", "myfunc"),
new TextContent("Heya"),
])));
// Act
ExternalInputRequest copy = VerifyEventSerialization(source);
// Assert
ChatMessage messageCopy = Assert.Single(source.AgentResponse.Messages);
Assert.Equal(messageCopy.Contents.Count, copy.AgentResponse.Messages[0].Contents.Count);
McpServerToolApprovalRequestContent mcpRequest = AssertContent<McpServerToolApprovalRequestContent>(messageCopy);
Assert.Equal("call1", mcpRequest.Id);
FunctionApprovalRequestContent functionRequest = AssertContent<FunctionApprovalRequestContent>(messageCopy);
Assert.Equal("call2", functionRequest.Id);
FunctionCallContent functionCall = AssertContent<FunctionCallContent>(messageCopy);
Assert.Equal("call3", functionCall.CallId);
TextContent textContent = AssertContent<TextContent>(messageCopy);
Assert.Equal("Heya", textContent.Text);
}
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.Events;
using Microsoft.Extensions.AI;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events;
/// <summary>
/// Verify <see cref="ExternalInputResponse"/> class
/// </summary>
public sealed class ExternalInputResponseTest(ITestOutputHelper output) : EventTest(output)
{
[Fact]
public void VerifySerializationEmpty()
{
// Arrange
ExternalInputResponse source = new(new ChatMessage(ChatRole.User, "Wassup?"));
// Act
ExternalInputResponse copy = VerifyEventSerialization(source);
// Assert
ChatMessage messageCopy = Assert.Single(source.Messages);
AssertMessage(messageCopy, copy.Messages[0]);
}
[Fact]
public void VerifySerializationWithResponses()
{
// Arrange
ExternalInputResponse source =
new(new ChatMessage(
ChatRole.Assistant,
[
new McpServerToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")).CreateResponse(approved: true),
new FunctionApprovalRequestContent("call2", new FunctionCallContent("call2", "result1")).CreateResponse(approved: true),
new FunctionResultContent("call3", 33),
new TextContent("Heya"),
]));
// Act
ExternalInputResponse copy = VerifyEventSerialization(source);
// Assert
ChatMessage responseMessage = Assert.Single(source.Messages);
Assert.Equal(responseMessage.Contents.Count, copy.Messages[0].Contents.Count);
McpServerToolApprovalResponseContent mcpApproval = AssertContent<McpServerToolApprovalResponseContent>(responseMessage);
Assert.Equal("call1", mcpApproval.Id);
FunctionApprovalResponseContent functionApproval = AssertContent<FunctionApprovalResponseContent>(responseMessage);
Assert.Equal("call2", functionApproval.Id);
FunctionResultContent functionResult = AssertContent<FunctionResultContent>(responseMessage);
Assert.Equal("call3", functionResult.CallId);
TextContent textContent = AssertContent<TextContent>(responseMessage);
Assert.Equal("Heya", textContent.Text);
}
}

View File

@@ -0,0 +1,669 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class ChatMessageExtensionsTests
{
[Fact]
public void ToRecordWithSimpleTextMessage()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Hello World");
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result);
Assert.Contains(result.Fields, f => f.Name == TypeSchema.Message.Fields.Role);
Assert.Contains(result.Fields, f => f.Name == TypeSchema.Message.Fields.Text);
FormulaValue roleField = result.GetField(TypeSchema.Message.Fields.Role);
StringValue roleValue = Assert.IsType<StringValue>(roleField);
Assert.Equal(ChatRole.User.Value, roleValue.Value);
}
[Fact]
public void ToRecordWithAssistantMessage()
{
// Arrange
ChatMessage message = new(ChatRole.Assistant, "I can help you");
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result);
Assert.Contains(result.Fields, f => f.Name == TypeSchema.Message.Fields.Role);
FormulaValue roleField = result.GetField(TypeSchema.Message.Fields.Role);
StringValue roleValue = Assert.IsType<StringValue>(roleField);
Assert.Equal(ChatRole.Assistant.Value, roleValue.Value);
}
[Fact]
public void ToRecordIncludesAllStandardFields()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Test")
{
MessageId = "msg-123"
};
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result.GetField(TypeSchema.Discriminator));
Assert.NotNull(result.GetField(TypeSchema.Message.Fields.Id));
Assert.NotNull(result.GetField(TypeSchema.Message.Fields.Role));
Assert.NotNull(result.GetField(TypeSchema.Message.Fields.Content));
Assert.NotNull(result.GetField(TypeSchema.Message.Fields.Text));
Assert.NotNull(result.GetField(TypeSchema.Message.Fields.Metadata));
}
[Fact]
public void ToTableWithMultipleMessages()
{
// Arrange
IEnumerable<ChatMessage> messages =
[
new(ChatRole.User, "First message"),
new(ChatRole.Assistant, "Second message"),
new(ChatRole.User, "Third message")
];
// Act
TableValue result = messages.ToTable();
// Assert
Assert.NotNull(result);
Assert.Equal(3, result.Rows.Count());
}
[Fact]
public void ToTableWithEmptyMessages()
{
// Arrange
IEnumerable<ChatMessage> messages = [];
// Act
TableValue result = messages.ToTable();
// Assert
Assert.NotNull(result);
Assert.Empty(result.Rows);
}
[Fact]
public void ToChatMessagesWithNull()
{
// Arrange
DataValue? value = null;
// Act
IEnumerable<ChatMessage>? result = value.ToChatMessages();
// Assert
Assert.Null(result);
}
[Fact]
public void ToChatMessagesWithBlankDataValue()
{
// Arrange
DataValue value = DataValue.Blank();
// Act
IEnumerable<ChatMessage>? result = value.ToChatMessages();
// Assert
Assert.Null(result);
}
[Fact]
public void ToChatMessagesWithStringDataValue()
{
// Arrange
DataValue value = StringDataValue.Create("Hello");
// Act
IEnumerable<ChatMessage>? result = value.ToChatMessages();
// Assert
Assert.NotNull(result);
ChatMessage message = Assert.Single(result);
Assert.Equal(ChatRole.User, message.Role);
Assert.Equal("Hello", message.Text);
}
[Fact]
public void ToChatMessagesWithRecordDataValue()
{
// Arrange
ChatMessage source = new(ChatRole.User, "Test");
DataValue record = source.ToRecord().ToDataValue();
// Act
IEnumerable<ChatMessage>? result = record.ToChatMessages();
// Assert
Assert.NotNull(result);
ChatMessage message = Assert.Single(result);
Assert.Equal(source.Role, message.Role);
Assert.Equal(source.Text, message.Text);
}
[Fact]
public void ToChatMessagesWithTableDataValue()
{
// Arrange
ChatMessage[] source = [new(ChatRole.User, "Test")];
DataValue table = source.ToTable().ToDataValue();
// Act
IEnumerable<ChatMessage>? result = table.ToChatMessages();
// Assert
Assert.NotNull(result);
ChatMessage message = Assert.Single(result);
Assert.Equal(source[0].Role, message.Role);
Assert.Equal(source[0].Text, message.Text);
}
[Fact]
public void ToChatMessagesWithTableOfDataValue()
{
// Arrange
TableDataValue table = DataValue.TableFromValues([new StringDataValue("test")]);
// Act
IEnumerable<ChatMessage>? result = table.ToChatMessages();
// Assert
Assert.NotNull(result);
ChatMessage message = Assert.Single(result);
Assert.Equal(ChatRole.User, message.Role);
Assert.Equal("test", message.Text);
}
[Fact]
public void ToChatMessagesWithUnsupportedValue()
{
// Arrange
BooleanDataValue booleanValue = new(true);
// Act
IEnumerable<ChatMessage>? messages = booleanValue.ToChatMessages();
// Assert
Assert.Null(messages);
}
[Fact]
public void ToChatMessageFromStringDataValue()
{
// Arrange
StringDataValue value = StringDataValue.Create("Test message");
// Act
ChatMessage result = value.ToChatMessage();
// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.User, result.Role);
Assert.Equal("Test message", result.Text);
}
[Fact]
public void ToChatMessageFromDataValueRecord()
{
// Arrange
ChatMessage source = new(ChatRole.User, "Test");
DataValue record = source.ToRecord().ToDataValue();
// Act
ChatMessage? result = record.ToChatMessage();
// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.User, result.Role);
Assert.Equal("Test", result.Text);
}
[Fact]
public void ToChatMessageFromDataValueString()
{
// Arrange
DataValue value = StringDataValue.Create("Test message");
// Act
ChatMessage? result = value.ToChatMessage();
// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.User, result.Role);
Assert.Equal("Test message", result.Text);
}
[Fact]
public void ToChatMessageFromBlankDataValue()
{
// Arrange
DataValue value = DataValue.Blank();
// Act
ChatMessage? result = value.ToChatMessage();
// Assert
Assert.Null(result);
}
[Fact]
public void ToChatMessageFromUnsupportedValue()
{
// Arrange
DataValue value = BooleanDataValue.Create(true);
// Act & Assert
Assert.Throws<DeclarativeActionException>(() => value.ToChatMessage());
}
[Fact]
public void ToChatMessageFromRecordDataValue()
{
// Arrange
// Note: Use "Agent" not "Assistant" - AgentMessageRole.Agent maps to ChatRole.Assistant
RecordDataValue record = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Role, StringDataValue.Create("Agent")),
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Content, DataValue.EmptyTable));
// Act
ChatMessage result = record.ToChatMessage();
// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.Assistant, result.Role);
}
[Fact]
public void ToChatMessageWithImpliedRole()
{
// Arrange
RecordValue source =
FormulaValue.NewRecordFromFields(
new NamedValue(TypeSchema.Message.Fields.Role, FormulaValue.New(string.Empty)),
new NamedValue(
TypeSchema.Message.Fields.Content,
FormulaValue.NewTable(
TypeSchema.Message.ContentRecordType,
FormulaValue.NewRecordFromFields(
new NamedValue(TypeSchema.Message.Fields.ContentType, TypeSchema.Message.ContentTypes.Text.ToFormula()),
new NamedValue(TypeSchema.Message.Fields.ContentValue, FormulaValue.New("Test"))))));
RecordDataValue record = source.ToRecord();
// Act
ChatMessage? result = record.ToChatMessage();
// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.User, result.Role);
Assert.Equal("Test", result.Text);
}
[Fact]
public void ToChatMessageWithImageUrlContentType()
{
// Arrange
ChatMessage source = new(ChatRole.User, [AgentMessageContentType.ImageUrl.ToContent("https://example.com/image.jpg")!]);
DataValue record = source.ToRecord().ToDataValue();
// Act
ChatMessage? result = record.ToChatMessage();
// Assert
Assert.NotNull(result);
AIContent content = Assert.Single(result.Contents);
Assert.IsType<UriContent>(content);
}
[Fact]
public void ToChatMessageWithWithImageDataContentType()
{
// Arrange
ChatMessage source = new(ChatRole.User, [AgentMessageContentType.ImageUrl.ToContent("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA")!]);
DataValue record = source.ToRecord().ToDataValue();
// Act
ChatMessage? result = record.ToChatMessage();
// Assert
Assert.NotNull(result);
AIContent content = Assert.Single(result.Contents);
Assert.IsType<DataContent>(content);
}
[Fact]
public void ToChatMessageWithWithImageFileContentType()
{
// Arrange
ChatMessage source = new(ChatRole.User, [AgentMessageContentType.ImageFile.ToContent("file-id-123")!]);
DataValue record = source.ToRecord().ToDataValue();
// Act
ChatMessage? result = record.ToChatMessage();
// Assert
Assert.NotNull(result);
AIContent content = Assert.Single(result.Contents);
Assert.IsType<HostedFileContent>(content);
}
[Fact]
public void ToChatMessageWithUnsupportedContent()
{
// Arrange
ChatMessage source = new(ChatRole.User, "Test");
RecordDataValue record = source.ToRecord().ToRecord();
DataValue contentValue = record.Properties[TypeSchema.Message.Fields.Content];
TableDataValue contentValues = Assert.IsType<TableDataValue>(contentValue, exactMatch: false);
RecordDataValue badContent = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.ContentType, StringDataValue.Create(TypeSchema.Message.ContentTypes.Text)),
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.ContentValue, BooleanDataValue.Create(true)));
contentValues.Values.Add(badContent);
// Act
ChatMessage message = record.ToChatMessage();
// Assert
Assert.Single(message.Contents);
Assert.Equal("Test", message.Text);
}
[Fact]
public void ToChatMessageWithEmptyContent()
{
// Arrange
ChatMessage source = new(ChatRole.User, "Test");
source.Contents.Add(new TextContent(string.Empty));
RecordDataValue record = source.ToRecord().ToRecord();
// Act
ChatMessage message = record.ToChatMessage();
// Assert
Assert.Single(message.Contents);
Assert.Equal("Test", message.Text);
}
[Fact]
public void ToMetadataWithNull()
{
// Arrange
RecordDataValue? metadata = null;
// Act
AdditionalPropertiesDictionary? result = metadata.ToMetadata();
// Assert
Assert.Null(result);
}
[Fact]
public void ToMetadataWithProperties()
{
// Arrange
RecordDataValue metadata = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>("key1", StringDataValue.Create("value1")),
new KeyValuePair<string, DataValue>("key2", NumberDataValue.Create(42)));
// Act
AdditionalPropertiesDictionary? result = metadata.ToMetadata();
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Count);
Assert.Equal("value1", result["key1"]);
Assert.Equal(42m, result["key2"]);
}
[Fact]
public void ToChatRoleFromAgentMessageRole()
{
// Act & Assert
Assert.Equal(ChatRole.Assistant, AgentMessageRole.Agent.ToChatRole());
Assert.Equal(ChatRole.User, AgentMessageRole.User.ToChatRole());
Assert.Equal(ChatRole.User, ((AgentMessageRole)99).ToChatRole());
Assert.Equal(ChatRole.User, ((AgentMessageRole?)null).ToChatRole());
}
[Fact]
public void AgentMessageContentTypeToContentMissing()
{
// Act & Assert
Assert.Null(AgentMessageContentType.Text.ToContent(string.Empty));
Assert.Null(AgentMessageContentType.Text.ToContent(null));
}
[Fact]
public void AgentMessageContentTypeToContentText()
{
// Arrange & Act
AIContent? result = AgentMessageContentType.Text.ToContent("Sample text");
// Assert
Assert.NotNull(result);
TextContent textContent = Assert.IsType<TextContent>(result);
Assert.Equal("Sample text", textContent.Text);
}
[Fact]
public void ToContentWithImageUrlContentType()
{
// Arrange & Act
AIContent? result = AgentMessageContentType.ImageUrl.ToContent("https://example.com/image.jpg");
// Assert
Assert.NotNull(result);
UriContent uriContent = Assert.IsType<UriContent>(result);
Assert.Equal("https://example.com/image.jpg", uriContent.Uri.ToString());
}
[Fact]
public void ToContentWithImageUrlContentTypeDataUri()
{
// Arrange & Act
AIContent? result = AgentMessageContentType.ImageUrl.ToContent("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA");
// Assert
Assert.NotNull(result);
DataContent dataContent = Assert.IsType<DataContent>(result);
Assert.False(dataContent.Data.IsEmpty);
}
[Fact]
public void ToContentWithImageFileContentType()
{
// Arrange & Act
AIContent? result = AgentMessageContentType.ImageFile.ToContent("file-id-123");
// Assert
Assert.NotNull(result);
HostedFileContent fileContent = Assert.IsType<HostedFileContent>(result);
Assert.Equal("file-id-123", fileContent.FileId);
}
[Fact]
public void ToChatMessageFromFunctionResultContents()
{
// Arrange
IEnumerable<FunctionResultContent> functionResults =
[
new(callId: "call1", result: "Result 1"),
new(callId: "call2", result: "Result 2")
];
// Act
ChatMessage result = functionResults.ToChatMessage();
// Assert
Assert.NotNull(result);
Assert.Equal(ChatRole.Tool, result.Role);
Assert.Equal(2, result.Contents.Count);
}
[Fact]
public void ToChatMessagesFromTableDataValueWithStrings()
{
// Arrange
TableDataValue table =
DataValue.TableFromValues(
[
StringDataValue.Create("Message 1"),
StringDataValue.Create("Message 2")
]);
// Act
IEnumerable<ChatMessage> result = table.ToChatMessages();
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Count());
Assert.All(result, msg => Assert.Equal(ChatRole.User, msg.Role));
}
[Fact]
public void ToChatMessagesFromTableDataValueWithRecords()
{
// Arrange
RecordDataValue record1 = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Role, StringDataValue.Create("User")),
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Content, DataValue.EmptyTable));
RecordDataValue record2 = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Role, StringDataValue.Create("Assistant")),
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Content, DataValue.EmptyTable));
TableDataValue table = DataValue.TableFromRecords(record1, record2);
// Act
IEnumerable<ChatMessage> result = table.ToChatMessages();
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Count());
}
[Fact]
public void ToChatMessagesFromTableDataValueWithSingleColumnRecords()
{
// Arrange
RecordDataValue innerRecord = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Role, StringDataValue.Create("User")),
new KeyValuePair<string, DataValue>(TypeSchema.Message.Fields.Content, DataValue.EmptyTable));
RecordDataValue wrappedRecord = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>("Value", innerRecord));
TableDataValue table = DataValue.TableFromRecords(wrappedRecord);
// Act
IEnumerable<ChatMessage> result = table.ToChatMessages();
// Assert
Assert.NotNull(result);
ChatMessage message = Assert.Single(result);
Assert.Equal(ChatRole.User, message.Role);
}
[Fact]
public void ToRecordWithMessageContainingMultipleContentItems()
{
// Arrange
ChatMessage message =
new(ChatRole.User,
[
new TextContent("First part"),
new TextContent("Second part")
]);
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result);
FormulaValue contentField = result.GetField(TypeSchema.Message.Fields.Content);
TableValue contentTable = Assert.IsType<TableValue>(contentField, exactMatch: false);
Assert.Equal(2, contentTable.Rows.Count());
}
[Fact]
public void ToRecordWithMessageContainingUriContent()
{
// Arrange
ChatMessage message =
new(ChatRole.User,
[
new UriContent("https://example.com/image.jpg", "image/*")
]);
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result);
FormulaValue contentField = result.GetField(TypeSchema.Message.Fields.Content);
TableValue contentTable = Assert.IsType<TableValue>(contentField, exactMatch: false);
Assert.Single(contentTable.Rows);
}
[Fact]
public void ToRecordWithMessageContainingHostedFileContent()
{
// Arrange
ChatMessage message =
new(ChatRole.User,
[
new HostedFileContent("file-123")
]);
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result);
FormulaValue contentField = result.GetField(TypeSchema.Message.Fields.Content);
TableValue contentTable = Assert.IsType<TableValue>(contentField, exactMatch: false);
Assert.Single(contentTable.Rows);
}
[Fact]
public void ToRecordWithMessageContainingMetadata()
{
// Arrange
ChatMessage message = new(ChatRole.User, "Test message")
{
AdditionalProperties = new AdditionalPropertiesDictionary
{
["custom_key"] = "custom_value",
["count"] = 5
}
};
// Act
RecordValue result = message.ToRecord();
// Assert
Assert.NotNull(result);
FormulaValue metadataField = result.GetField(TypeSchema.Message.Fields.Metadata);
RecordValue metadataRecord = Assert.IsType<RecordValue>(metadataField, exactMatch: false);
Assert.Equal(2, metadataRecord.Fields.Count());
}
}

View File

@@ -0,0 +1,845 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class DataValueExtensionsTests
{
[Fact]
public void ToDataValueWithNull()
{
// Arrange
object? value = null;
// Act
DataValue result = value.ToDataValue();
// Assert
Assert.IsType<BlankDataValue>(result);
}
[Fact]
public void ToDataValueWithUnassignedValue()
{
// Arrange
object value = UnassignedValue.Instance;
// Act
DataValue result = value.ToDataValue();
// Assert
Assert.IsType<BlankDataValue>(result);
}
[Fact]
public void ToDataValueWithBooleanTrue()
{
// Arrange
const bool Value = true;
// Act
DataValue result = Value.ToDataValue();
// Assert
BooleanDataValue boolValue = Assert.IsType<BooleanDataValue>(result);
Assert.True(boolValue.Value);
}
[Fact]
public void ToDataValueWithBooleanFalse()
{
// Arrange
const bool Value = false;
// Act
DataValue result = Value.ToDataValue();
// Assert
BooleanDataValue boolValue = Assert.IsType<BooleanDataValue>(result);
Assert.False(boolValue.Value);
}
[Fact]
public void ToDataValueWithInt()
{
// Arrange
const int Value = 42;
// Act
DataValue result = Value.ToDataValue();
// Assert
NumberDataValue numberValue = Assert.IsType<NumberDataValue>(result);
Assert.Equal(42, numberValue.Value);
}
[Fact]
public void ToDataValueWithLong()
{
// Arrange
const long Value = 9876543210L;
// Act
DataValue result = Value.ToDataValue();
// Assert
NumberDataValue numberValue = Assert.IsType<NumberDataValue>(result);
Assert.Equal(9876543210L, numberValue.Value);
}
[Fact]
public void ToDataValueWithFloat()
{
// Arrange
const float Value = 3.14f;
// Act
DataValue result = Value.ToDataValue();
// Assert
FloatDataValue floatValue = Assert.IsType<FloatDataValue>(result);
Assert.Equal(3.14f, floatValue.Value, precision: 2);
}
[Fact]
public void ToDataValueWithDecimal()
{
// Arrange
const decimal Value = 123.456m;
// Act
DataValue result = Value.ToDataValue();
// Assert
NumberDataValue numberValue = Assert.IsType<NumberDataValue>(result);
Assert.Equal(123.456m, numberValue.Value);
}
[Fact]
public void ToDataValueWithDouble()
{
// Arrange
const double Value = 2.71828;
// Act
DataValue result = Value.ToDataValue();
// Assert
FloatDataValue floatValue = Assert.IsType<FloatDataValue>(result);
Assert.Equal(2.71828, floatValue.Value, precision: 5);
}
[Fact]
public void ToDataValueWithString()
{
// Arrange
const string Value = "Test String";
// Act
DataValue result = Value.ToDataValue();
// Assert
StringDataValue stringValue = Assert.IsType<StringDataValue>(result);
Assert.Equal("Test String", stringValue.Value);
}
[Fact]
public void ToDataValueWithDateTimeZeroTime()
{
// Arrange
DateTime value = new(2025, 10, 17, 0, 0, 0);
// Act
DataValue result = value.ToDataValue();
// Assert
DateDataValue dateValue = Assert.IsType<DateDataValue>(result);
Assert.Equal(new DateTime(2025, 10, 17), dateValue.Value);
}
[Fact]
public void ToDataValueWithDateTimeNonZeroTime()
{
// Arrange
DateTime value = new(2025, 10, 17, 14, 30, 45);
// Act
DataValue result = value.ToDataValue();
// Assert
DateTimeDataValue dateTimeValue = Assert.IsType<DateTimeDataValue>(result);
Assert.Equal(new DateTime(2025, 10, 17, 14, 30, 45), dateTimeValue.Value.DateTime);
}
[Fact]
public void ToDataValueWithTimeSpan()
{
// Arrange
TimeSpan value = TimeSpan.FromHours(2.5);
// Act
DataValue result = value.ToDataValue();
// Assert
TimeDataValue timeValue = Assert.IsType<TimeDataValue>(result);
Assert.Equal(TimeSpan.FromHours(2.5), timeValue.Value);
}
[Fact]
public void ToDataValueWithDataValue()
{
// Arrange
DataValue value = StringDataValue.Create("Already a DataValue");
// Act
DataValue result = value.ToDataValue();
// Assert
Assert.Same(value, result);
}
[Fact]
public void ToDataValueWithFormulaValue()
{
// Arrange
FormulaValue value = FormulaValue.New(123);
// Act
DataValue result = value.ToDataValue();
// Assert
NumberDataValue numberValue = Assert.IsType<NumberDataValue>(result);
Assert.Equal(123, numberValue.Value);
}
[Fact]
public void ToFormulaWithNull()
{
// Arrange
DataValue? value = null;
// Act
FormulaValue result = value.ToFormula();
// Assert
Assert.IsType<BlankValue>(result);
}
[Fact]
public void ToFormulaWithBlankDataValue()
{
// Arrange
DataValue value = DataValue.Blank();
// Act
FormulaValue result = value.ToFormula();
// Assert
Assert.IsType<BlankValue>(result);
}
[Fact]
public void ToFormulaWithBooleanDataValue()
{
// Arrange
DataValue value = BooleanDataValue.Create(true);
// Act
FormulaValue result = value.ToFormula();
// Assert
BooleanValue boolValue = Assert.IsType<BooleanValue>(result);
Assert.True(boolValue.Value);
}
[Fact]
public void ToFormulaWithNumberDataValue()
{
// Arrange
DataValue value = NumberDataValue.Create(99.5m);
// Act
FormulaValue result = value.ToFormula();
// Assert
DecimalValue decimalValue = Assert.IsType<DecimalValue>(result);
Assert.Equal(99.5m, decimalValue.Value);
}
[Fact]
public void ToFormulaWithFloatDataValue()
{
// Arrange
DataValue value = FloatDataValue.Create(1.23);
// Act
FormulaValue result = value.ToFormula();
// Assert
NumberValue numberValue = Assert.IsType<NumberValue>(result);
Assert.Equal(1.23, numberValue.Value, precision: 2);
}
[Fact]
public void ToFormulaWithStringDataValue()
{
// Arrange
DataValue value = StringDataValue.Create("Test");
// Act
FormulaValue result = value.ToFormula();
// Assert
StringValue stringValue = Assert.IsType<StringValue>(result);
Assert.Equal("Test", stringValue.Value);
}
[Fact]
public void ToFormulaWithDateTimeDataValue()
{
// Arrange
DateTime dateTime = new(2025, 10, 17, 12, 0, 0);
DataValue value = DateTimeDataValue.Create(dateTime);
// Act
FormulaValue result = value.ToFormula();
// Assert
DateTimeValue dateTimeValue = Assert.IsType<DateTimeValue>(result);
Assert.Equal(dateTime, dateTimeValue.GetConvertedValue(TimeZoneInfo.Utc));
}
[Fact]
public void ToFormulaWithDateDataValue()
{
// Arrange
DateTime date = new(2025, 10, 17);
DataValue value = DateDataValue.Create(date);
// Act
FormulaValue result = value.ToFormula();
// Assert
DateValue dateValue = Assert.IsType<DateValue>(result);
Assert.Equal(date, dateValue.GetConvertedValue(TimeZoneInfo.Utc));
}
[Fact]
public void ToFormulaWithTimeDataValue()
{
// Arrange
TimeSpan time = TimeSpan.FromHours(3);
DataValue value = TimeDataValue.Create(time);
// Act
FormulaValue result = value.ToFormula();
// Assert
TimeValue timeValue = Assert.IsType<TimeValue>(result);
Assert.Equal(time, timeValue.Value);
}
[Fact]
public void ToFormulaWithRecordDataValue()
{
// Arrange
DataValue value = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>("Name", StringDataValue.Create("John")),
new KeyValuePair<string, DataValue>("Age", NumberDataValue.Create(30)));
// Act
FormulaValue result = value.ToFormula();
// Assert
RecordValue recordValue = Assert.IsType<RecordValue>(result, exactMatch: false);
Assert.Equal(2, recordValue.Fields.Count());
}
[Fact]
public void ToFormulaWithTableDataValue()
{
// Arrange
RecordDataValue record = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>("Field", StringDataValue.Create("Value")));
DataValue value = DataValue.TableFromRecords(ImmutableArray.Create(record));
// Act
FormulaValue result = value.ToFormula();
// Assert
TableValue tableValue = Assert.IsType<TableValue>(result, exactMatch: false);
Assert.Single(tableValue.Rows);
}
[Fact]
public void ToFormulaTypeWithNull()
{
// Arrange
DataValue? value = null;
// Act
FormulaType result = value.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Blank, result);
}
[Fact]
public void ToFormulaTypeWithBooleanDataValue()
{
// Arrange
DataValue value = BooleanDataValue.Create(true);
// Act
FormulaType result = value.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Boolean, result);
}
[Fact]
public void ToFormulaTypeWithStringDataValue()
{
// Arrange
DataValue value = StringDataValue.Create("Test");
// Act
FormulaType result = value.ToFormulaType();
// Assert
Assert.Equal(FormulaType.String, result);
}
[Fact]
public void DataTypeToFormulaTypeWithNull()
{
// Arrange
DataType? type = null;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Blank, result);
}
[Fact]
public void DataTypeToFormulaTypeWithBooleanDataType()
{
// Arrange
DataType type = BooleanDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Boolean, result);
}
[Fact]
public void DataTypeToFormulaTypeWithNumberDataType()
{
// Arrange
DataType type = NumberDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Decimal, result);
}
[Fact]
public void DataTypeToFormulaTypeWithFloatDataType()
{
// Arrange
DataType type = FloatDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Number, result);
}
[Fact]
public void DataTypeToFormulaTypeWithStringDataType()
{
// Arrange
DataType type = StringDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.String, result);
}
[Fact]
public void DataTypeToFormulaTypeWithDateTimeDataType()
{
// Arrange
DataType type = DateTimeDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.DateTime, result);
}
[Fact]
public void DataTypeToFormulaTypeWithDateDataType()
{
// Arrange
DataType type = DateDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Date, result);
}
[Fact]
public void DataTypeToFormulaTypeWithTimeDataType()
{
// Arrange
DataType type = TimeDataType.Instance;
// Act
FormulaType result = type.ToFormulaType();
// Assert
Assert.Equal(FormulaType.Time, result);
}
[Fact]
public void ToObjectWithNull()
{
// Arrange
DataValue? value = null;
// Act
object? result = value.ToObject();
// Assert
Assert.Null(result);
}
[Fact]
public void ToObjectWithBlankDataValue()
{
// Arrange
DataValue value = DataValue.Blank();
// Act
object? result = value.ToObject();
// Assert
Assert.Null(result);
}
[Fact]
public void ToObjectWithBooleanDataValue()
{
// Arrange
DataValue value = BooleanDataValue.Create(true);
// Act
object? result = value.ToObject();
// Assert
Assert.IsType<bool>(result);
Assert.True((bool)result);
}
[Fact]
public void ToObjectWithNumberDataValue()
{
// Arrange
DataValue value = NumberDataValue.Create(42.5m);
// Act
object? result = value.ToObject();
// Assert
Assert.IsType<decimal>(result);
Assert.Equal(42.5m, (decimal)result);
}
[Fact]
public void ToObjectWithStringDataValue()
{
// Arrange
DataValue value = StringDataValue.Create("Hello");
// Act
object? result = value.ToObject();
// Assert
Assert.IsType<string>(result);
Assert.Equal("Hello", (string)result);
}
[Fact]
public void ToClrTypeWithBooleanDataType()
{
// Arrange
DataType type = BooleanDataType.Instance;
// Act
Type result = type.ToClrType();
// Assert
Assert.Equal(typeof(bool), result);
}
[Fact]
public void ToClrTypeWithNumberDataType()
{
// Arrange
DataType type = NumberDataType.Instance;
// Act
Type result = type.ToClrType();
// Assert
Assert.Equal(typeof(decimal), result);
}
[Fact]
public void ToClrTypeWithFloatDataType()
{
// Arrange
DataType type = FloatDataType.Instance;
// Act
Type result = type.ToClrType();
// Assert
Assert.Equal(typeof(double), result);
}
[Fact]
public void ToClrTypeWithStringDataType()
{
// Arrange
DataType type = StringDataType.Instance;
// Act
Type result = type.ToClrType();
// Assert
Assert.Equal(typeof(string), result);
}
[Fact]
public void ToClrTypeWithDateTimeDataType()
{
// Arrange
DataType type = DateTimeDataType.Instance;
// Act
Type result = type.ToClrType();
// Assert
Assert.Equal(typeof(DateTime), result);
}
[Fact]
public void ToClrTypeWithTimeDataType()
{
// Arrange
DataType type = TimeDataType.Instance;
// Act
Type result = type.ToClrType();
// Assert
Assert.Equal(typeof(TimeSpan), result);
}
[Fact]
public void AsListWithNull()
{
// Arrange
DataValue? value = null;
// Act
IList<string>? result = value.AsList<string>();
// Assert
Assert.Null(result);
}
[Fact]
public void AsListWithBlankDataValue()
{
// Arrange
DataValue value = DataValue.Blank();
// Act
IList<string>? result = value.AsList<string>();
// Assert
Assert.Null(result);
}
[Fact]
public void NewBlankWithNullDataType()
{
// Arrange
DataType? type = null;
// Act
FormulaValue result = type.NewBlank();
// Assert
Assert.IsType<BlankValue>(result);
}
[Fact]
public void NewBlankWithBooleanDataType()
{
// Arrange
DataType type = BooleanDataType.Instance;
// Act
FormulaValue result = type.NewBlank();
// Assert
Assert.IsType<BlankValue>(result);
}
[Fact]
public void ToRecordValueWithRecordDataValue()
{
// Arrange
RecordDataValue recordDataValue = DataValue.RecordFromFields(
new KeyValuePair<string, DataValue>("Field1", StringDataValue.Create("Value1")),
new KeyValuePair<string, DataValue>("Field2", NumberDataValue.Create(123)));
// Act
RecordValue result = recordDataValue.ToRecordValue();
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Fields.Count());
Assert.NotNull(result.GetField("Field1"));
Assert.NotNull(result.GetField("Field2"));
}
[Fact]
public void ToRecordTypeWithRecordDataType()
{
// Arrange
RecordDataType recordDataType = new RecordDataType.Builder
{
Properties =
{
["Name"] = new PropertyInfo.Builder
{
Type = StringDataType.Instance
}.Build(),
["Count"] = new PropertyInfo.Builder
{
Type = NumberDataType.Instance
}.Build()
}
}.Build();
// Act
RecordType result = recordDataType.ToRecordType();
// Assert
Assert.NotNull(result);
IEnumerable<NamedFormulaType> fieldTypes = result.GetFieldTypes();
List<NamedFormulaType> fieldTypesList = fieldTypes.ToList();
Assert.Equal(2, fieldTypesList.Count);
IEnumerable<string> fieldNames = fieldTypesList.Select(f => f.Name.Value);
Assert.Contains("Name", fieldNames);
Assert.Contains("Count", fieldNames);
NamedFormulaType nameField = fieldTypesList.First(f => f.Name.Value == "Name");
NamedFormulaType countField = fieldTypesList.First(f => f.Name.Value == "Count");
Assert.Equal(FormulaType.String, nameField.Type);
Assert.Equal(FormulaType.Decimal, countField.Type);
}
[Fact]
public void ToRecordValueWithDictionary()
{
// Arrange
IDictionary dictionary = new Dictionary<string, object>
{
["Key1"] = "Value1",
["Key2"] = 42
};
// Act
RecordDataValue result = dictionary.ToRecordValue();
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Properties.Count);
Assert.True(result.Properties.ContainsKey("Key1"));
Assert.True(result.Properties.ContainsKey("Key2"));
}
[Fact]
public void ToTableValueWithEmptyEnumerable()
{
// Arrange
IEnumerable enumerable = Array.Empty<object>();
// Act
TableDataValue result = enumerable.ToTableValue();
// Assert
Assert.NotNull(result);
Assert.Empty(result.Values);
}
[Fact]
public void ToTableValueWithDictionaryEnumerable()
{
// Arrange
IEnumerable enumerable = new List<IDictionary>
{
new Dictionary<string, object> { ["Name"] = "Alice", ["Age"] = 30 },
new Dictionary<string, object> { ["Name"] = "Bob", ["Age"] = 25 }
};
// Act
TableDataValue result = enumerable.ToTableValue();
// Assert
Assert.NotNull(result);
}
[Fact]
public void ToTableValueWithPrimitiveEnumerable()
{
// Arrange
IEnumerable enumerable = new List<int> { 1, 2, 3 };
// Act
TableDataValue result = enumerable.ToTableValue();
// Assert
Assert.NotNull(result);
Assert.Equal(3, result.Values.Length);
}
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.PowerFx;
using Moq;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class DeclarativeWorkflowOptionsExtensionsTests
{
[Fact]
public void NullContext_UsesDefaultMaximumExpressionLength()
{
// Arrange
DeclarativeWorkflowOptions? options = null;
// Act
RecalcEngine engine = options.CreateRecalcEngine();
// Assert
Assert.NotNull(engine);
Assert.Equal(10000, engine.Config.MaximumExpressionLength);
}
[Fact]
public void OptionsWithoutLimits_UsesDefaults()
{
// Arrange
DeclarativeWorkflowOptions options = CreateOptions();
// Act
RecalcEngine engine = options.CreateRecalcEngine();
// Assert
Assert.NotNull(engine);
Assert.Equal(10000, engine.Config.MaximumExpressionLength);
Assert.True(engine.Config.MaxCallDepth >= 0);
}
[Fact]
public void OptionsWithBothLimits()
{
// Arrange
const int ExpectedLength = 5000;
const int ExpectedDepth = 12;
DeclarativeWorkflowOptions context = CreateOptions(ExpectedLength, ExpectedDepth);
// Act
RecalcEngine engine = context.CreateRecalcEngine();
// Assert
Assert.Equal(ExpectedLength, engine.Config.MaximumExpressionLength);
Assert.Equal(ExpectedDepth, engine.Config.MaxCallDepth);
}
// Factory for creating options and mock provider
private static DeclarativeWorkflowOptions CreateOptions(
int? maximumExpressionLength = null,
int? maximumCallDepth = null)
{
Mock<WorkflowAgentProvider> providerMock = new(MockBehavior.Strict);
return
new(providerMock.Object)
{
MaximumExpressionLength = maximumExpressionLength,
MaximumCallDepth = maximumCallDepth
};
}
}

View File

@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Bot.ObjectModel;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
/// <summary>
/// Tests for <see cref="DialogBaseExtensions"/>.
/// </summary>
public sealed class DialogBaseExtensionsTests
{
[Fact]
public void WrapWithBotCreatesValidBotDefinition()
{
// Arrange
AdaptiveDialog dialog = new AdaptiveDialog.Builder()
{
BeginDialog = new OnActivity.Builder()
{
Id = "test_dialog",
},
}.Build();
// Assert
Assert.False(dialog.HasSchemaName);
// Act
AdaptiveDialog wrappedDialog = dialog.WrapWithBot();
// Assert
VerifyWrappedDialog(wrappedDialog);
// Act & Assert
VerifyWrappedDialog(wrappedDialog.WrapWithBot());
}
private static void VerifyWrappedDialog(AdaptiveDialog wrappedDialog)
{
Assert.NotNull(wrappedDialog);
Assert.NotNull(wrappedDialog.BeginDialog);
Assert.Equal("test_dialog", wrappedDialog.BeginDialog.Id);
Assert.True(wrappedDialog.HasSchemaName);
}
}

View File

@@ -0,0 +1,217 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class ExpandoObjectExtensionsTests
{
[Fact]
public void ToRecordTypeWithEmptyExpandoObject()
{
// Arrange
ExpandoObject expando = new();
// Act
RecordType recordType = expando.ToRecordType();
// Assert
Assert.NotNull(recordType);
Assert.Empty(recordType.GetFieldTypes());
}
[Fact]
public void ToRecordTypeWithStringProperty()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.Name = "John Doe";
// Act
RecordType recordType = ((ExpandoObject)expando).ToRecordType();
// Assert
Assert.NotNull(recordType);
IEnumerable<NamedFormulaType> fieldTypes = recordType.GetFieldTypes();
Assert.Single(fieldTypes);
NamedFormulaType field = fieldTypes.First();
Assert.Equal("Name", field.Name.Value);
Assert.Equal(FormulaType.String, field.Type);
}
[Fact]
public void ToRecordTypeWithMultipleProperties()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.Name = "Alice";
expando.Age = 30;
expando.IsActive = true;
// Act
RecordType recordType = ((ExpandoObject)expando).ToRecordType();
// Assert
Assert.NotNull(recordType);
IEnumerable<NamedFormulaType> fieldTypes = recordType.GetFieldTypes();
Assert.Equal(3, fieldTypes.Count());
IEnumerable<string> fieldNames = fieldTypes.Select(f => f.Name.Value);
Assert.Contains("Name", fieldNames);
Assert.Contains("Age", fieldNames);
Assert.Contains("IsActive", fieldNames);
}
[Fact]
public void ToRecordTypeWithNullProperty()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.Name = "Test";
expando.NullValue = null;
// Act
RecordType recordType = ((ExpandoObject)expando).ToRecordType();
// Assert
Assert.NotNull(recordType);
IEnumerable<NamedFormulaType> fieldTypes = recordType.GetFieldTypes();
Assert.Equal(2, fieldTypes.Count());
IEnumerable<string> fieldNames = fieldTypes.Select(f => f.Name.Value);
Assert.Contains("Name", fieldNames);
Assert.Contains("NullValue", fieldNames);
}
[Fact]
public void ToRecordWithEmptyExpandoObject()
{
// Arrange
ExpandoObject expando = new();
// Act
RecordValue recordValue = expando.ToRecord();
// Assert
Assert.NotNull(recordValue);
Assert.Empty(recordValue.Fields);
}
[Fact]
public void ToRecordWithStringProperty()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.Message = "Hello World";
// Act
RecordValue recordValue = ((ExpandoObject)expando).ToRecord();
// Assert
Assert.NotNull(recordValue);
Assert.Single(recordValue.Fields);
NamedValue field = recordValue.Fields.First();
Assert.Equal("Message", field.Name);
StringValue stringValue = Assert.IsType<StringValue>(field.Value);
Assert.Equal("Hello World", stringValue.Value);
}
[Fact]
public void ToRecordWithMultiplePropertiesOfDifferentTypes()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.Name = "Bob";
expando.Count = 42;
expando.Active = true;
// Act
RecordValue recordValue = ((ExpandoObject)expando).ToRecord();
// Assert
Assert.NotNull(recordValue);
Assert.Equal(3, recordValue.Fields.Count());
FormulaValue nameField = recordValue.GetField("Name");
StringValue nameValue = Assert.IsType<StringValue>(nameField);
Assert.Equal("Bob", nameValue.Value);
FormulaValue countField = recordValue.GetField("Count");
DecimalValue countValue = Assert.IsType<DecimalValue>(countField);
Assert.Equal(42, countValue.Value);
FormulaValue activeField = recordValue.GetField("Active");
BooleanValue activeValue = Assert.IsType<BooleanValue>(activeField);
Assert.True(activeValue.Value);
}
[Fact]
public void ToRecordWithNestedExpandoObject()
{
// Arrange
dynamic nested = new ExpandoObject();
nested.InnerValue = "Inner";
dynamic expando = new ExpandoObject();
expando.Outer = "Outer";
expando.Nested = nested;
// Act
RecordValue recordValue = ((ExpandoObject)expando).ToRecord();
// Assert
Assert.NotNull(recordValue);
Assert.Equal(2, recordValue.Fields.Count());
Assert.NotNull(recordValue.GetField("Outer"));
FormulaValue nestedField = recordValue.GetField("Nested");
Assert.NotNull(nestedField);
RecordValue nestedRecord = Assert.IsType<RecordValue>(nestedField, exactMatch: false);
Assert.Single(nestedRecord.Fields);
}
[Fact]
public void ToRecordWithNullProperty()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.Name = "Test";
expando.NullValue = null;
// Act
RecordValue recordValue = ((ExpandoObject)expando).ToRecord();
// Assert
Assert.NotNull(recordValue);
Assert.Equal(2, recordValue.Fields.Count());
FormulaValue nullField = recordValue.GetField("NullValue");
Assert.IsType<BlankValue>(nullField);
}
[Fact]
public void ToRecordTypeAndToRecordAreConsistent()
{
// Arrange
dynamic expando = new ExpandoObject();
expando.StringField = "Value";
expando.IntField = 123;
expando.BoolField = false;
// Act
RecordType recordType = ((ExpandoObject)expando).ToRecordType();
RecordValue recordValue = ((ExpandoObject)expando).ToRecord();
// Assert
List<NamedFormulaType> fieldTypesList = recordType.GetFieldTypes().ToList();
Assert.Equal(fieldTypesList.Count, recordValue.Fields.Count());
foreach (NamedFormulaType fieldType in fieldTypesList)
{
Assert.Contains(recordValue.Fields, f => f.Name == fieldType.Name.Value);
}
}
}

View File

@@ -0,0 +1,215 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public class FormulaValueExtensionsTests
{
[Fact]
public void BooleanValue()
{
BooleanValue formulaValue = FormulaValue.New(true);
DataValue dataValue = formulaValue.ToDataValue();
BooleanDataValue typedValue = Assert.IsType<BooleanDataValue>(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
BooleanValue formulaCopy = Assert.IsType<BooleanValue>(dataValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal(bool.TrueString, formulaValue.Format());
}
[Fact]
public void StringValues()
{
StringValue formulaValue = FormulaValue.New("test value");
Assert.Equal(StringDataType.Instance, formulaValue.GetDataType());
DataValue dataValue = formulaValue.ToDataValue();
StringDataValue typedValue = Assert.IsType<StringDataValue>(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
StringValue formulaCopy = Assert.IsType<StringValue>(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal(formulaValue.Value, formulaValue.Format());
}
[Fact]
public void DecimalValues()
{
DecimalValue formulaValue = FormulaValue.New(45.3m);
Assert.Equal(NumberDataType.Instance, formulaValue.GetDataType());
DataValue dataValue = formulaValue.ToDataValue();
NumberDataValue typedValue = Assert.IsType<NumberDataValue>(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
DecimalValue formulaCopy = Assert.IsType<DecimalValue>(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal("45.3", formulaValue.Format());
}
[Fact]
public void NumberValues()
{
NumberValue formulaValue = FormulaValue.New(3.1415926535897);
Assert.Equal(FloatDataType.Instance, formulaValue.GetDataType());
DataValue dataValue = formulaValue.ToDataValue();
FloatDataValue typedValue = Assert.IsType<FloatDataValue>(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
NumberValue formulaCopy = Assert.IsType<NumberValue>(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal("3.1415926535897", formulaValue.Format());
}
[Fact]
public void BlankValues()
{
BlankValue formulaValue = FormulaValue.NewBlank();
Assert.Equal(DataType.Blank, formulaValue.GetDataType());
Assert.IsType<BlankDataValue>(formulaValue.ToDataValue());
Assert.Equal(string.Empty, formulaValue.Format());
}
[Fact]
public void VoidValues()
{
VoidValue formulaValue = FormulaValue.NewVoid();
Assert.Equal(DataType.Unspecified, formulaValue.GetDataType());
Assert.IsType<BlankDataValue>(formulaValue.ToDataValue());
}
[Fact]
public void DateValues()
{
DateTime timestamp = DateTime.UtcNow.Date;
DateValue formulaValue = FormulaValue.NewDateOnly(timestamp);
Assert.Equal(DataType.Date, formulaValue.GetDataType());
DataValue dataValue = formulaValue.ToDataValue();
DateDataValue typedValue = Assert.IsType<DateDataValue>(dataValue);
Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), typedValue.Value);
DateValue formulaCopy = Assert.IsType<DateValue>(dataValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc));
Assert.Equal($"{timestamp}", formulaValue.Format());
}
[Fact]
public void DateTimeValues()
{
DateTime timestamp = DateTime.UtcNow;
DateTimeValue formulaValue = FormulaValue.New(timestamp);
Assert.Equal(DataType.DateTime, formulaValue.GetDataType());
DataValue dataValue = formulaValue.ToDataValue();
DateTimeDataValue typedValue = Assert.IsType<DateTimeDataValue>(dataValue);
Assert.Equal(formulaValue.GetConvertedValue(TimeZoneInfo.Utc), typedValue.Value);
DateTimeValue formulaCopy = Assert.IsType<DateTimeValue>(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.GetConvertedValue(TimeZoneInfo.Utc));
Assert.Equal($"{timestamp}", formulaValue.Format());
}
[Fact]
public void TimeValues()
{
TimeValue formulaValue = FormulaValue.New(TimeSpan.Parse("10:35"));
Assert.Equal(DataType.Time, formulaValue.GetDataType());
DataValue dataValue = formulaValue.ToDataValue();
TimeDataValue typedValue = Assert.IsType<TimeDataValue>(dataValue);
Assert.Equal(formulaValue.Value, typedValue.Value);
TimeValue formulaCopy = Assert.IsType<TimeValue>(typedValue.ToFormula());
Assert.Equal(typedValue.Value, formulaCopy.Value);
Assert.Equal("10:35:00", formulaValue.Format());
}
[Fact]
public void RecordValues()
{
RecordValue formulaValue = FormulaValue.NewRecordFromFields(
new NamedValue("FieldA", FormulaValue.New("Value1")),
new NamedValue("FieldB", FormulaValue.New("Value2")),
new NamedValue("FieldC", FormulaValue.New("Value3")));
Assert.Equal(DataType.EmptyRecord, formulaValue.GetDataType());
RecordDataValue dataValue = formulaValue.ToRecord();
Assert.Equal(formulaValue.Fields.Count(), dataValue.Properties.Count);
foreach (KeyValuePair<string, DataValue> property in dataValue.Properties)
{
Assert.Contains(property.Key, formulaValue.Fields.Select(field => field.Name));
}
RecordValue formulaCopy = Assert.IsType<RecordValue>(dataValue.ToFormula(), exactMatch: false);
Assert.Equal(formulaCopy.Fields.Count(), dataValue.Properties.Count);
foreach (NamedValue field in formulaCopy.Fields)
{
Assert.Contains(field.Name, dataValue.Properties.Keys);
}
Assert.Equal(
"""
{
"FieldA": "Value1",
"FieldB": "Value2",
"FieldC": "Value3"
}
""",
formulaValue.Format().Replace(Environment.NewLine, "\n"));
Dictionary<string, int> source =
new()
{
["FieldA"] = 1,
["FieldB"] = 2,
["FieldC"] = 3
};
FormulaValue formula = source.ToFormula();
Assert.IsType<RecordValue>(formula, exactMatch: false);
}
[Fact]
public void TableValues()
{
RecordValue recordValue = FormulaValue.NewRecordFromFields(
new NamedValue("FieldA", FormulaValue.New("Value1")),
new NamedValue("FieldB", FormulaValue.New("Value2")),
new NamedValue("FieldC", FormulaValue.New("Value3")));
TableValue formulaValue = FormulaValue.NewTable(recordValue.Type, [recordValue]);
TableDataValue dataValue = formulaValue.ToTable();
Assert.Equal(formulaValue.Rows.Count(), dataValue.Values.Length);
TableValue formulaCopy = Assert.IsType<TableValue>(dataValue.ToFormula(), exactMatch: false);
Assert.Equal(formulaCopy.Rows.Count(), dataValue.Values.Length);
Assert.Equal(
"""
[
{
"FieldA": "Value1",
"FieldB": "Value2",
"FieldC": "Value3"
}
]
""",
formulaValue.Format().Replace(Environment.NewLine, "\n"));
}
}

View File

@@ -0,0 +1,387 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class JsonDocumentExtensionsTests
{
[Fact]
public void ParseRecord_Object_PrimitiveFields_Succeeds()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("text", typeof(string)),
("numberInt", typeof(int)),
("numberLong", typeof(long)),
("numberDecimal", typeof(decimal)),
("numberDouble", typeof(double)),
("flag", typeof(bool)),
("date", typeof(DateTime)),
("time", typeof(TimeSpan))
]);
DateTime expectedDateTime = new(2024, 10, 01, 12, 34, 56, DateTimeKind.Utc);
TimeSpan expectedTimeSpan = new(12, 34, 56);
JsonDocument document = JsonDocument.Parse(
"""
{
"text": "hello",
"numberInt": 7,
"numberLong": 9223372036854775807,
"numberDecimal": 12.5,
"numberDouble": 3.99E99,
"flag": true,
"date": "2024-10-01T12:34:56Z",
"time": "12:34:56"
}
""");
// Act
Dictionary<string, object?> result = document.ParseRecord(recordType);
// Assert
Assert.Equal("hello", result["text"]);
Assert.Equal(7, result["numberInt"]);
Assert.Equal(9223372036854775807L, result["numberLong"]);
Assert.Equal(12.5m, result["numberDecimal"]);
Assert.Equal(3.99E99, result["numberDouble"]);
Assert.Equal(true, result["flag"]);
Assert.Equal(expectedDateTime, result["date"]);
Assert.Equal(expectedTimeSpan, result["time"]);
}
[Fact]
public void ParseRecord_Object_NoSchema_Succeeds()
{
// Arrange
JsonDocument document = JsonDocument.Parse(
"""
{
"text": "hello",
"numberInt": 7,
"numberLong": 9223372036854775807,
"numberDecimal": 12.5,
"numberDouble": 3.99E99,
"flag": true,
"date": "2024-10-01T12:34:56Z",
"time": "12:34:56"
}
""");
// Act
Dictionary<string, object?> result = document.ParseRecord(VariableType.RecordType);
// Assert
Assert.Equal("hello", result["text"]);
Assert.Equal(7, result["numberInt"]);
Assert.Equal(9223372036854775807L, result["numberLong"]);
Assert.Equal(12.5m, result["numberDecimal"]);
Assert.Equal(3.99E99, result["numberDouble"]);
Assert.Equal(true, result["flag"]);
Assert.Equal("2024-10-01T12:34:56Z", result["date"]);
Assert.Equal("12:34:56", result["time"]);
}
[Fact]
public void ParseRecord_Object_NestedRecord_Succeeds()
{
// Arrange
VariableType innerRecord =
VariableType.Record(
[
("innerText", typeof(string)),
("innerNumber", typeof(int))
]);
VariableType outerRecord =
VariableType.Record(
[
("outerText", typeof(string)),
("nested", innerRecord)
]);
JsonDocument document = JsonDocument.Parse(
"""
{
"outerText": "outer",
"nested": {
"innerText": "inner",
"innerNumber": 42
}
}
""");
// Act
Dictionary<string, object?> result = document.ParseRecord(outerRecord);
// Assert
Assert.Equal("outer", result["outerText"]);
Dictionary<string, object?> nested = (Dictionary<string, object?>)result["nested"]!;
Assert.NotNull(nested);
Assert.True(nested.ContainsKey("innerText"));
Assert.Equal("inner", nested["innerText"]);
Assert.Equal(42, nested["innerNumber"]);
}
[Fact]
public void ParseRecord_NullRoot_ReturnsEmpty()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("text", typeof(string))
]);
JsonDocument document = JsonDocument.Parse("null");
// Act
Dictionary<string, object?> result = document.ParseRecord(recordType);
// Assert
Assert.Empty(result);
}
[Fact]
public void ParseRecord_ArrayWithSingleRecord_Succeeds()
{
// Arrange
VariableType listType =
VariableType.List(
[
("name", typeof(string)),
("value", typeof(int))
]);
JsonDocument document = JsonDocument.Parse(
"""
[
{
"name": "item",
"value": 5
}
]
""");
// Act
List<object?> result = document.ParseList(listType);
// Assert
Assert.Single(result);
Dictionary<string, object?> element = Assert.IsType<Dictionary<string, object?>>(result[0]);
Assert.Equal("item", element["name"]);
Assert.Equal(5, element["value"]);
}
[Fact]
public void ParseRecord_ArrayWithMultipleRecords_Throws()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("id", typeof(int))
]);
JsonDocument document = JsonDocument.Parse(
"""
[
{ "id": 1 },
{ "id": 2 }
]
""");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(recordType));
}
[Fact]
public void ParseRecord_InvalidTargetType_Throws()
{
// Arrange
VariableType notARecord = typeof(string);
JsonDocument document = JsonDocument.Parse(
"""
{ "x": 1 }
""");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(notARecord));
}
[Fact]
public void ParseRecord_InvalidRootKind_Throws()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("text", typeof(string))
]);
JsonDocument document = JsonDocument.Parse(@"""not-an-object""");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(recordType));
}
[Fact]
public void ParseRecord_UnsupportedPropertyType_Throws()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("unsupported", typeof(Guid))
]);
JsonDocument document = JsonDocument.Parse(
"""
{ "unsupported": "C2556C11-210E-4BB6-BF18-4A8968CB45A8" }
""");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(recordType));
}
[Fact]
public void ParseRecord_MissingRequiredProperty_Throws()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("required", typeof(bool))
]);
JsonDocument document = JsonDocument.Parse("{}");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseRecord(recordType));
}
[Fact]
public void ParseRecord_MissingNullableProperty_Succeeds()
{
// Arrange
VariableType recordType =
VariableType.Record(
[
("required", typeof(string))
]);
JsonDocument document = JsonDocument.Parse("{}");
// Act
Dictionary<string, object?> result = document.ParseRecord(recordType);
// Assert
Assert.Single(result);
Dictionary<string, object?> element = Assert.IsType<Dictionary<string, object?>>(result);
Assert.Null(element["required"]);
}
[Fact]
public void ParseList_NullRoot_ReturnsEmpty()
{
// Arrange
JsonDocument document = JsonDocument.Parse("null");
// Act
List<object?> result = document.ParseList(typeof(int[]));
// Assert
Assert.Empty(result);
}
[Fact]
public void ParseList_Array_Primitives_Succeeds()
{
// Arrange
JsonDocument document = JsonDocument.Parse("[1,2,3]");
// Act
List<object?> result = document.ParseList(typeof(int[]));
// Assert
Assert.Equal(3, result.Count);
Assert.Equal(1, result[0]);
Assert.Equal(2, result[1]);
Assert.Equal(3, result[2]);
}
[Fact]
public void ParseList_PrimitiveRoot_WrappedAsSingleElement_Succeeds()
{
// Arrange
JsonDocument document = JsonDocument.Parse("7");
// Act
List<object?> result = document.ParseList(typeof(int));
// Assert
Assert.Single(result);
Assert.Equal(7, result[0]);
}
[Fact]
public void ParseList_Array_Records_Succeeds()
{
// Arrange
VariableType listType =
VariableType.List(
[
("id", typeof(int)),
("name", typeof(string))
]);
JsonDocument document = JsonDocument.Parse(
"""
[
{ "id": 1, "name": "a" },
{ "id": 2, "name": "b" }
]
""");
// Act
List<object?> result = document.ParseList(listType);
// Assert
Assert.Equal(2, result.Count);
Dictionary<string, object?> first = (Dictionary<string, object?>)result[0]!;
Dictionary<string, object?> second = (Dictionary<string, object?>)result[1]!;
Assert.NotNull(first);
Assert.Equal(1, first["id"]);
Assert.Equal("a", first["name"]);
Assert.NotNull(second);
Assert.Equal(2, second["id"]);
Assert.Equal("b", second["name"]);
}
[Fact]
public void ParseList_InvalidTargetType_Throws()
{
// Arrange
JsonDocument document = JsonDocument.Parse("[1,2]");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseList(typeof(int)));
}
[Fact]
public void ParseList_Array_MixedTypes_Throws()
{
// Arrange
JsonDocument document = JsonDocument.Parse("[1,\"two\",3]");
// Act / Assert
Assert.Throws<DeclarativeActionException>(() => document.ParseList(typeof(int[])));
}
}

View File

@@ -0,0 +1,124 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class ObjectExtensionsTests
{
[Fact]
public void AsListWithNullInput()
{
object[]? nullList = null;
IList<string>? result = nullList.AsList<string>();
Assert.Null(result);
}
[Fact]
public void AsListWithEmptyInput()
{
IList<string>? result = Array.Empty<int>().AsList<string>();
Assert.NotNull(result);
Assert.Empty(result);
}
[Fact]
public void AsListWithSingleElement()
{
const string Value = "Test";
IList<string>? result = Value.AsList<string>();
Assert.NotNull(result);
Assert.Single(result);
Assert.Equal(Value, result[0]);
}
[Fact]
public void AsListWithMultipleInput()
{
object[] inputs = ["33.3", "test"];
IList<string>? result = inputs.AsList<string>();
Assert.NotNull(result);
Assert.Equal(2, result.Count);
}
[Fact]
public void ConvertSame()
{
VerifyConversion(true, typeof(bool), true);
VerifyConversion(32, typeof(int), 32);
VerifyConversion("Test", typeof(string), "Test");
DateTime now = DateTime.Now;
VerifyConversion(now, typeof(DateTime), now);
VerifyConversion(now.TimeOfDay, typeof(TimeSpan), now.TimeOfDay);
}
[Fact]
public void ConvertFailure()
{
VerifyInvalid(32, VariableType.RecordType);
VerifyInvalid(true, VariableType.RecordType);
VerifyInvalid(Guid.NewGuid(), typeof(Guid));
}
[Fact]
public void ConvertToString()
{
VerifyConversion(true, typeof(string), bool.TrueString);
VerifyConversion(32, typeof(string), "32");
VerifyConversion(3.14d, typeof(string), "3.14");
DateTime now = DateTime.Now;
VerifyConversion(now, typeof(string), $"{now:o}");
VerifyConversion(now.TimeOfDay, typeof(string), $"{now.TimeOfDay:c}");
}
[Fact]
public void ConvertFromString()
{
VerifyConversion("true", typeof(bool), true);
VerifyConversion("32", typeof(int), 32);
VerifyConversion("3.14", typeof(double), 3.14D);
DateTime now = DateTime.Now;
VerifyConversion($"{now:o}", typeof(DateTime), now);
VerifyConversion($"{now.TimeOfDay:c}", typeof(TimeSpan), now.TimeOfDay);
}
[Fact]
public void ConvertJson()
{
const string Json =
"""
{
"id": "item1",
"count": 5
}
""";
Dictionary<string, object?> expected =
new()
{
{ "id", "item1"},
{ "count", 5},
};
VerifyConversion(Json, VariableType.Record(("id", typeof(string)), ("count", typeof(int))), expected);
}
private static void VerifyConversion(object? sourceValue, VariableType targetType, object? expectedValue)
{
object? actualValue = sourceValue.ConvertType(targetType);
if (expectedValue is IDictionary<string, object?> or DateTime)
{
Assert.Equivalent(expectedValue, actualValue);
}
else
{
Assert.Equal(expectedValue, actualValue);
}
}
private static void VerifyInvalid(object? sourceValue, VariableType targetType)
{
Assert.Throws<DeclarativeActionException>(() => sourceValue.ConvertType(targetType));
}
}

View File

@@ -0,0 +1,127 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class PortableValueExtensionsTests
{
[Fact]
public void InvalidType() => TestInvalidType(IPAddress.Loopback);
[Fact]
public void NullType() => TestValidType<object>(null, FormulaType.Blank);
[Fact]
public void BooleanType() => TestValidType(true, FormulaType.Boolean);
[Fact]
public void StringType() => TestValidType("Hello, World!", FormulaType.String);
[Fact]
public void IntType() => TestValidType(int.MinValue, FormulaType.Decimal);
[Fact]
public void LongType() => TestValidType(long.MaxValue, FormulaType.Decimal);
[Fact]
public void DecimalType() => TestValidType(decimal.MaxValue, FormulaType.Decimal);
[Fact]
public void FloatType() => TestValidType(float.MaxValue, FormulaType.Number);
[Fact]
public void DoubleType() => TestValidType(double.MinValue, FormulaType.Number);
[Fact]
public void DateType() => TestValidType(DateTime.UtcNow.Date, FormulaType.Date);
[Fact]
public void DateTimeType() => TestValidType(DateTime.UtcNow, FormulaType.DateTime);
[Fact]
public void TimeSpanType() => TestValidType(DateTime.UtcNow.TimeOfDay, FormulaType.Time);
[Fact]
public void ChatMessageType() => TestValidType(new ChatMessage(ChatRole.User, "input"), RecordType.Empty());
[Fact]
public void ListEmptyType()
{
TableValue convertedValue = (TableValue)TestValidType(Array.Empty<int>(), TableType.Empty());
Assert.Equal(0, convertedValue.Count());
}
[Fact]
public void ListSimpleType()
{
TableValue convertedValue = (TableValue)TestValidType(new List<int> { 1, 2, 3 }, TableType.Empty());
Assert.Equal(3, convertedValue.Count());
RecordValue firstElement = convertedValue.Rows.First().Value;
NamedValue recordElement = Assert.Single(firstElement.Fields);
Assert.Equal("Value", recordElement.Name);
DecimalValue recordValue = Assert.IsType<DecimalValue>(recordElement.Value);
Assert.Equal(1, recordValue.Value);
}
[Fact]
public void ListComplexType()
{
TableValue convertedValue = (TableValue)TestValidType(new List<ChatMessage> { new(ChatRole.User, "input"), new(ChatRole.Assistant, "output") }, TableType.Empty());
Assert.Equal(2, convertedValue.Count());
RecordValue firstElement = convertedValue.Rows.First().Value;
StringValue typeValue = Assert.IsType<StringValue>(firstElement.GetField(TypeSchema.Discriminator));
Assert.Equal(nameof(ChatMessage), typeValue.Value);
StringValue textValue = Assert.IsType<StringValue>(firstElement.GetField(TypeSchema.Message.Fields.Text));
Assert.Equal("input", textValue.Value);
}
[Fact]
public void DictionaryType()
{
RecordValue convertedValue = (RecordValue)TestValidType(new Dictionary<string, int> { { "A", 1 }, { "B", 2 } }, RecordType.Empty());
Assert.Equal(2, convertedValue.Fields.Count());
NamedValue firstElement = convertedValue.Fields.First();
Assert.Equal("A", firstElement.Name);
DecimalValue firstElementValue = Assert.IsType<DecimalValue>(firstElement.Value);
Assert.Equal(1, firstElementValue.Value);
}
[Fact]
public void ObjectType()
{
RecordValue convertedValue = (RecordValue)TestValidType(FormulaValue.NewRecordFromFields(new NamedValue("key", FormulaValue.New(3))).ToDataValue().ToObject(), RecordType.Empty());
Assert.Single(convertedValue.Fields);
NamedValue firstElement = convertedValue.Fields.First();
Assert.Equal("key", firstElement.Name);
DecimalValue firstElementValue = Assert.IsType<DecimalValue>(firstElement.Value);
Assert.Equal(3, firstElementValue.Value);
}
private static void TestInvalidType(object? sourceValue)
{
Assert.Throws<DeclarativeModelException>(() => sourceValue.AsPortable());
PortableValue portableValue = new(sourceValue ?? UnassignedValue.Instance);
Assert.Throws<DeclarativeModelException>(() => portableValue.ToFormula());
}
private static FormulaValue TestValidType<TValue>(TValue? sourceValue, FormulaType expectedType) where TValue : notnull
{
object portableObject = sourceValue.AsPortable();
Assert.IsNotType<PortableValue>(portableObject);
PortableValue portableValue = new(portableObject);
FormulaValue formulaValue = portableValue.ToFormula();
Assert.NotNull(formulaValue);
Assert.Equal(expectedType.GetType(), formulaValue.Type.GetType());
return formulaValue;
}
}

View File

@@ -0,0 +1,162 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class StringExtensionsTests
{
[Fact]
public void TrimJsonWithDelimiter()
{
// Arrange
const string Input =
"""
```json
{
"key": "value"
}
```
""";
// Act
string result = Input.TrimJsonDelimiter();
// Assert
Assert.Equal(
"""
{
"key": "value"
}
""",
result);
}
[Fact]
public void TrimJsonWithPadding()
{
// Arrange
const string Input =
"""
```json
{
"key": "value"
}
```
""";
// Act
string result = Input.TrimJsonDelimiter();
// Assert
Assert.Equal(
"""
{
"key": "value"
}
""",
result);
}
[Fact]
public void TrimJsonWithUnqualifiedDelimiter()
{
// Arrange
const string Input =
"""
```
{
"key": "value"
}
```
""";
// Act
string result = Input.TrimJsonDelimiter();
// Assert
Assert.Equal(
"""
{
"key": "value"
}
""",
result);
}
[Fact]
public void TrimJsonWithoutDelimiter()
{
// Arrange
const string Input =
"""
{
"key": "value"
}
""";
// Act
string result = Input.TrimJsonDelimiter();
// Assert
Assert.Equal(
"""
{
"key": "value"
}
""",
result);
}
[Fact]
public void TrimJsonWithoutDelimiterWithPadding()
{
// Arrange
const string Input =
"""
{
"key": "value"
}
""";
// Act
string result = Input.TrimJsonDelimiter();
// Assert
Assert.Equal(
"""
{
"key": "value"
}
""",
result);
}
[Fact]
public void TrimMissingWithDelimiter()
{
// Arrange
const string Input =
"""
```json
```
""";
// Act
string result = Input.TrimJsonDelimiter();
// Assert
Assert.Equal(string.Empty, result);
}
[Fact]
public void TrimEmptyString()
{
// Act
string result = string.Empty.TrimJsonDelimiter();
// Assert
Assert.Equal(string.Empty, result);
}
}

View File

@@ -0,0 +1,141 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class TemplateExtensionsTests
{
[Fact]
public void FormatTemplateWithTextSegments()
{
// Arrange
RecalcEngine engine = new();
IEnumerable<TemplateLine> template =
[
new TemplateLine.Builder
{
Segments =
{
new TextSegment.Builder { Value = "Hello " },
new TextSegment.Builder { Value = "World" }
}
}.Build()
];
// Act
string result = engine.Format(template);
// Assert
Assert.Equal("Hello World", result);
}
[Fact]
public void FormatTemplateWithMultipleLines()
{
// Arrange
RecalcEngine engine = new();
IEnumerable<TemplateLine> template =
[
new TemplateLine.Builder
{
Segments =
{
new TextSegment.Builder { Value = "Line 1" }
}
}.Build(),
new TemplateLine.Builder
{
Segments =
{
new TextSegment.Builder { Value = "Line 2" }
}
}.Build()
];
// Act
string result = engine.Format(template);
// Assert
Assert.Equal("Line 1Line 2", result);
}
[Fact]
public void FormatSingleTemplateLineWithNullValue()
{
// Arrange
RecalcEngine engine = new();
TemplateLine? line = null;
// Act
string result = engine.Format(line);
// Assert
Assert.Equal(string.Empty, result);
}
[Fact]
public void FormatSingleTemplateLineWithTextSegment()
{
// Arrange
RecalcEngine engine = new();
TemplateLine line = new TemplateLine.Builder
{
Segments =
{
new TextSegment.Builder { Value = "Test" }
}
}.Build();
// Act
string result = engine.Format(line);
// Assert
Assert.Equal("Test", result);
}
[Fact]
public void FormatTextSegmentWithNullValue()
{
// Arrange
RecalcEngine engine = new();
TextSegment segment = new TextSegment.Builder { Value = null }.Build();
// Act
string result = engine.Format(segment);
// Assert
Assert.Equal(string.Empty, result);
}
[Fact]
public void FormatTextSegmentWithEmptyValue()
{
// Arrange
RecalcEngine engine = new();
TextSegment segment = new TextSegment.Builder { Value = "" }.Build();
// Act
string result = engine.Format(segment);
// Assert
Assert.Equal(string.Empty, result);
}
[Fact]
public void FormatTextSegmentWithValue()
{
// Arrange
RecalcEngine engine = new();
TextSegment segment = new TextSegment.Builder { Value = "Hello World" }.Build();
// Act
string result = engine.Format(segment);
// Assert
Assert.Equal("Hello World", result);
}
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
public sealed class TypeExtensionsTests
{
[Fact]
public void ReferenceType() => VerifyIsNullable(typeof(string));
[Fact]
public void ClassType() => VerifyIsNullable(typeof(object));
[Fact]
public void InterfaceType() => VerifyIsNullable(typeof(IDisposable));
[Fact]
public void ArrayType() => VerifyIsNullable(typeof(int[]));
[Fact]
public void NonNullableValueType() => VerifyNotNullable(typeof(int));
[Fact]
public void NonNullableStructType() => VerifyNotNullable(typeof(DateTime));
[Fact]
public void NonNullableEnumType() => VerifyNotNullable(typeof(DayOfWeek));
[Fact]
public void NullableInt() => VerifyIsNullable(typeof(int?));
[Fact]
public void NullableDateTime() => VerifyIsNullable(typeof(DateTime?));
[Fact]
public void NullableEnum() => VerifyIsNullable(typeof(DayOfWeek?));
[Fact]
public void NullableCustomStruct() => VerifyIsNullable(typeof(TestStruct?));
private static void VerifyNotNullable(Type targetType)
{
// Act
bool result = targetType.IsNullable();
// Assert
Assert.False(result);
}
private static void VerifyIsNullable(Type targetType)
{
// Act
bool result = targetType.IsNullable();
// Assert
Assert.True(result);
}
private struct TestStruct
{
public int Value { get; set; }
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Interpreter;
/// <summary>
/// Tests execution of workflow created by <see cref="WorkflowModel{TCondition}"/>.
/// </summary>
public sealed class DeclarativeWorkflowModelTest(ITestOutputHelper output) : WorkflowTest(output)
{
[Fact]
public void GetDepthForDefault()
{
WorkflowModel<string> model = new(new TestExecutor("root"));
Assert.Equal(0, model.GetDepth(null));
}
[Fact]
public void GetDepthForMissingNode()
{
WorkflowModel<string> model = new(new TestExecutor("root"));
Assert.Throws<DeclarativeModelException>(() => model.GetDepth("missing"));
}
[Fact]
public void ConnectMissingNode()
{
TestExecutor rootExecutor = new("root");
WorkflowModel<string> model = new(rootExecutor);
model.AddLink("root", "missing");
TestWorkflowBuilder modelBuilder = new();
Assert.Throws<DeclarativeModelException>(() => model.Build(modelBuilder));
}
[Fact]
public void AddToMissingParent()
{
WorkflowModel<string> model = new(new TestExecutor("root"));
Assert.Throws<DeclarativeModelException>(() => model.AddNode(new TestExecutor("next"), "missing"));
}
[Fact]
public void LinkFromMissingSource()
{
WorkflowModel<string> model = new(new TestExecutor("root"));
Assert.Throws<DeclarativeModelException>(() => model.AddLink("missing", "anything"));
}
[Fact]
public void LocateMissingParent()
{
WorkflowModel<string> model = new(new TestExecutor("root"));
Assert.Null(model.LocateParent<TestExecutor>(null));
Assert.Throws<DeclarativeModelException>(() => model.LocateParent<TestExecutor>("missing"));
}
internal sealed class TestExecutor(string actionId) : IModeledAction
{
public string Id { get; } = actionId;
}
internal sealed class TestWorkflowBuilder : IModelBuilder<string>
{
public void Connect(IModeledAction source, IModeledAction target, string? condition = null)
{
Assert.Fail(); // Not expected to be called in this test.
}
}
}

View File

@@ -0,0 +1,166 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Kit;
public sealed class VariableTypeTests
{
[Fact]
public void IsValidPrimitivesReturnTrue()
{
Assert.True(VariableType.IsValid<bool>());
Assert.True(VariableType.IsValid<int>());
Assert.True(VariableType.IsValid<long>());
Assert.True(VariableType.IsValid<float>());
Assert.True(VariableType.IsValid<decimal>());
Assert.True(VariableType.IsValid<double>());
Assert.True(VariableType.IsValid<string>());
Assert.True(VariableType.IsValid<DateTime>());
Assert.True(VariableType.IsValid<TimeSpan>());
}
[Fact]
public void IsValidUnsupportedTypeReturnFalse()
{
Assert.False(VariableType.IsValid<Guid>());
Assert.False(VariableType.IsValid<Uri>());
}
[Fact]
public void IsListForListTypeReturnTrue()
{
VariableType listType = new(typeof(List<int>));
Assert.True(listType.IsList);
Assert.False(listType.IsRecord);
Assert.True(listType.IsValid());
}
[Fact]
public void IsRecordForDictionaryInterfaceReturnTrue()
{
VariableType recordType = new(typeof(IDictionary<string, object?>));
Assert.True(recordType.IsRecord);
Assert.False(recordType.IsList);
Assert.True(recordType.IsValid());
}
[Fact]
public void RecordFactoryCreatesSchema()
{
// Assuming the intended signature supports tuple params; adjust if needed.
VariableType nameType = new(typeof(string));
VariableType ageType = new(typeof(int));
// If the actual signature differs (params IEnumerable<...>), adapt test accordingly.
VariableType recordType = VariableType.Record(
[("name", nameType), ("age", ageType)]
);
Assert.True(recordType.IsRecord);
Assert.True(recordType.HasSchema);
Assert.NotNull(recordType.Schema);
Assert.Equal(2, recordType.Schema.Count);
Assert.True(recordType.Schema.ContainsKey("name"));
Assert.True(recordType.Schema.ContainsKey("age"));
Assert.Equal(typeof(string), recordType.Schema["name"].Type);
Assert.Equal(typeof(int), recordType.Schema["age"].Type);
}
[Fact]
public void EqualsPrimitiveTypeEquality()
{
VariableType t1 = new(typeof(int));
VariableType t2 = new(typeof(int));
VariableType t3 = new(typeof(string));
Assert.True(t1.Equals(t2));
Assert.True(t1.Equals(typeof(int)));
Assert.False(t1.Equals(t3));
Assert.False(t1.Equals(typeof(string)));
}
[Fact]
public void EqualsRecordEqualityIgnoresOrder()
{
VariableType strType = new(typeof(string));
VariableType intType = new(typeof(int));
VariableType recordA = VariableType.Record(
[("first", strType), ("second", intType)]
);
VariableType recordB = VariableType.Record(
[("second", intType), ("first", strType)]
);
Assert.True(recordA.Equals(recordB));
Assert.True(recordB.Equals(recordA));
}
[Fact]
public void EqualsRecordInequalityDifferentSchema()
{
VariableType strType = new(typeof(string));
VariableType intType = new(typeof(int));
VariableType recordA = VariableType.Record(
[("first", strType), ("second", intType)]
);
VariableType recordB = VariableType.Record(
[("first", strType)]
);
Assert.False(recordA.Equals(recordB));
Assert.False(recordB.Equals(recordA));
}
[Fact]
public void GetHashCodePrimitiveConsistency()
{
VariableType a = new(typeof(double));
VariableType b = new(typeof(double));
Assert.Equal(a, b);
Assert.Equal(a, typeof(double));
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void GetHashCodeRecordConsistency()
{
VariableType a = VariableType.Record(("a", typeof(string)), ("b", typeof(int)));
VariableType b = VariableType.Record(("a", typeof(string)), ("b", typeof(int)));
Assert.Equal(a, b);
Assert.NotEqual(a.GetHashCode(), b.GetHashCode());
}
[Fact]
public void HasSchemaFalseForNonRecord()
{
VariableType primitive = new(typeof(int));
Assert.False(primitive.HasSchema);
}
[Fact]
public void ImplicitOperatorFromTypeWrapsCorrectly()
{
VariableType vt = typeof(string);
Assert.Equal(typeof(string), vt.Type);
Assert.True(vt.IsValid());
}
[Fact]
public void EqualsNullAndDifferentTypes()
{
VariableType vt = new(typeof(int));
VariableType? nullType = null;
object? nullObj = null;
object different = "test";
Assert.False(vt.Equals(nullObj));
Assert.False(vt.Equals(nullType));
Assert.False(vt.Equals(different));
Assert.True(vt.Equals((object)typeof(int)));
}
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<InjectSharedIntegrationTestCode>true</InjectSharedIntegrationTestCode>
<InjectSharedBuildTestCode>true</InjectSharedBuildTestCode>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Workflows\*.cs" />
<None Include="Workflows\*.cs">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Workflows\*.yaml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Workflows\*.csproj">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,82 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Moq;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
/// <summary>
/// Mock implementation of <see cref="WorkflowAgentProvider"/> for unit testing purposes.
/// </summary>
internal sealed class MockAgentProvider : Mock<WorkflowAgentProvider>
{
public IList<string> ExistingConversationIds { get; } = [];
public List<ChatMessage>? TestMessages { get; set; }
public MockAgentProvider()
{
this.Setup(provider => provider.CreateConversationAsync(It.IsAny<CancellationToken>()))
.Returns(() => Task.FromResult(this.CreateConversationId()));
List<ChatMessage> testMessages = this.CreateMessages();
this.Setup(provider => provider.GetMessageAsync(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(testMessages.First()));
// Setup GetMessagesAsync to return test messages
this.Setup(provider => provider.GetMessagesAsync(
It.IsAny<string>(),
It.IsAny<int?>(),
It.IsAny<string?>(),
It.IsAny<string?>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()))
.Returns(ToAsyncEnumerableAsync(testMessages));
this.Setup(provider => provider.CreateMessageAsync(
It.IsAny<string>(),
It.IsAny<ChatMessage>(),
It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(testMessages.First()));
}
private string CreateConversationId()
{
string newConversationId = Guid.NewGuid().ToString("N");
this.ExistingConversationIds.Add(newConversationId);
return newConversationId;
}
private List<ChatMessage> CreateMessages()
{
// Create test messages
List<ChatMessage> messages = [];
const int MessageCount = 5;
for (int i = 0; i < MessageCount; i++)
{
messages.Add(new ChatMessage(ChatRole.User, $"Test message {i + 1}") { MessageId = Guid.NewGuid().ToString("N") });
}
this.TestMessages = messages;
return this.TestMessages;
}
private static async IAsyncEnumerable<ChatMessage> ToAsyncEnumerableAsync(IEnumerable<ChatMessage> messages)
{
foreach (ChatMessage message in messages)
{
yield return message;
}
await Task.CompletedTask;
}
}

View File

@@ -0,0 +1,83 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="AddConversationMessageExecutor"/>.
/// </summary>
public sealed class AddConversationMessageExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Theory]
[InlineData(AgentMessageRole.User)]
[InlineData(AgentMessageRole.Agent)]
public async Task AddMessageSuccessfullyAsync(AgentMessageRole role)
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(AddMessageSuccessfullyAsync),
variableName: "TestMessage",
role: AgentMessageRoleWrapper.Get(role),
messageText: $"Hello from {role}");
}
private async Task ExecuteTestAsync(
string displayName,
string variableName,
AgentMessageRoleWrapper role,
string messageText)
{
// Arrange
MockAgentProvider mockAgentProvider = new();
AddConversationMessage model = this.CreateModel(
this.FormatDisplayName(displayName),
FormatVariablePath(variableName),
"TestConversationId",
role,
messageText);
AddConversationMessageExecutor action = new(model, mockAgentProvider.Object, this.State);
// Act
await this.ExecuteAsync(action);
// Assert
ChatMessage? testMessage = mockAgentProvider.TestMessages?.FirstOrDefault();
Assert.NotNull(testMessage);
VerifyModel(model, action);
this.VerifyState(variableName, testMessage.ToRecord());
}
private AddConversationMessage CreateModel(
string displayName,
string messageVariable,
string conversationId,
AgentMessageRoleWrapper role,
string messageText)
{
AddConversationMessage.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Message = PropertyPath.Create(messageVariable),
ConversationId = StringExpression.Literal(conversationId),
Role = role,
};
actionBuilder.Content.Add(new AddConversationMessageContent.Builder
{
Type = AgentMessageContentType.Text,
Value = TemplateLine.Parse(messageText)
});
return AssignParent<AddConversationMessage>(actionBuilder);
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="ClearAllVariablesExecutor"/>.
/// </summary>
public sealed class ClearAllVariablesExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task ClearWorkflowScopeAsync()
{
// Arrange
this.State.Set("NoVar", FormulaValue.New("Old value"));
this.State.Bind();
ClearAllVariables model =
this.CreateModel(
this.FormatDisplayName(nameof(ClearWorkflowScopeAsync)),
VariablesToClear.ConversationScopedVariables);
// Act
ClearAllVariablesExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyUndefined("NoVar");
}
[Fact]
public async Task ClearUndefinedScopeAsync()
{
// Arrange
this.State.Set("NoVar", FormulaValue.New("Old value"));
this.State.Bind();
// Arrange
ClearAllVariables model =
this.CreateModel(
this.FormatDisplayName(nameof(ClearUndefinedScopeAsync)),
VariablesToClear.UserScopedVariables);
// Act
ClearAllVariablesExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("NoVar", FormulaValue.New("Old value"));
}
private ClearAllVariables CreateModel(string displayName, VariablesToClear variableTarget)
{
ClearAllVariables.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Variables = EnumExpression<VariablesToClearWrapper>.Literal(VariablesToClearWrapper.Get(variableTarget)),
};
return AssignParent<ClearAllVariables>(actionBuilder);
}
}

View File

@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="CreateConversationExecutor "/>.
/// </summary>
public sealed class CreateConversationExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task CreateNewConversationAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(nameof(CreateNewConversationAsync),
"TestConversationId",
executionIteration: 1);
}
[Fact]
public async Task CreateMultipleConversationsAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(nameof(CreateMultipleConversationsAsync),
"TestConversationId",
executionIteration: 4);
}
private async Task ExecuteTestAsync(
string displayName,
string variableName,
int executionIteration)
{
// Arrange
// Initialize state to simulate workflow environment.
this.State.InitializeSystem();
CreateConversation model = this.CreateModel(
this.FormatDisplayName(displayName),
FormatVariablePath(variableName));
MockAgentProvider mockAgentProvider = new();
CreateConversationExecutor action = new(model, mockAgentProvider.Object, this.State);
// Act
int expectedIterationCount = executionIteration;
while (executionIteration-- > 0)
{
await this.ExecuteAsync(action);
}
// Assert
VerifyModel(model, action);
Assert.Equal(expected: expectedIterationCount, actual: mockAgentProvider.ExistingConversationIds.Count);
this.VerifyState("TestConversationId", FormulaValue.New(mockAgentProvider.ExistingConversationIds.Last()));
}
private CreateConversation CreateModel(string displayName, string conversationIdVariable)
{
CreateConversation.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
ConversationId = PropertyPath.Create(conversationIdVariable)
};
return AssignParent<CreateConversation>(actionBuilder);
}
}

View File

@@ -0,0 +1,141 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="ParseValueExecutor"/>.
/// </summary>
public sealed class ParseValueExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task ParseRecordAsync()
{
// Arrange
RecordDataType.Builder recordBuilder =
new()
{
Properties =
{
{"key1", new PropertyInfo.Builder() { Type = DataType.String } },
}
};
ParseValue model =
this.CreateModel(
this.FormatDisplayName(nameof(ParseRecordAsync)),
recordBuilder,
@"{ ""key1"": ""val1"" }");
// Act
ParseValueExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("Target", FormulaValue.NewRecordFromFields(new NamedValue("key1", FormulaValue.New("val1"))));
}
[Fact]
public async Task ParseTableAsync()
{
// Arrange
RecordDataType.Builder recordBuilder =
new()
{
Properties =
{
{"key1", new PropertyInfo.Builder() { Type = DataType.String } },
}
};
ParseValue model =
this.CreateModel(
this.FormatDisplayName(nameof(ParseTableAsync)),
DataType.EmptyTable,
@"[""apple"",""banana"",""cat""]");
// Act
ParseValueExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("Target", FormulaValue.NewSingleColumnTable(FormulaValue.New("apple"), FormulaValue.New("banana"), FormulaValue.New("cat")));
}
[Fact]
public async Task ParseBooleanAsync()
{
// Arrange
ParseValue model =
this.CreateModel(
this.FormatDisplayName(nameof(ParseTableAsync)),
new BooleanDataType.Builder(),
"True");
// Act
ParseValueExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("Target", FormulaValue.New(true));
}
[Fact]
public async Task ParseNumberAsync()
{
// Arrange
ParseValue model =
this.CreateModel(
this.FormatDisplayName(nameof(ParseNumberAsync)),
new NumberDataType.Builder(),
"42");
// Act
ParseValueExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("Target", FormulaValue.New(42));
}
[Fact]
public async Task ParseStringAsync()
{
// Arrange
ParseValue model =
this.CreateModel(
this.FormatDisplayName(nameof(ParseStringAsync)),
new StringDataType.Builder(),
"Hello, World!");
// Act
ParseValueExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("Target", FormulaValue.New("Hello, World!"));
}
private ParseValue CreateModel(string displayName, DataType.Builder typeBuilder, string sourceText)
{
ParseValue.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
ValueType = typeBuilder,
Variable = PropertyPath.TopicVariable("Target"),
Value = new ValueExpression.Builder(ValueExpression.Literal(StringDataValue.Create(sourceText))),
};
return AssignParent<ParseValue>(actionBuilder);
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="ResetVariableExecutor"/>.
/// </summary>
public sealed class ResetVariableExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task ResetDefinedValueAsync()
{
// Arrange
this.State.Set("MyVar1", FormulaValue.New("Value #1"));
this.State.Set("MyVar2", FormulaValue.New("Value #2"));
ResetVariable model =
this.CreateModel(
this.FormatDisplayName(nameof(ResetDefinedValueAsync)),
FormatVariablePath("MyVar1"));
// Act
ResetVariableExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyUndefined("MyVar1");
this.VerifyState("MyVar2", FormulaValue.New("Value #2"));
}
[Fact]
public async Task ResetUndefinedValueAsync()
{
// Arrange
this.State.Set("MyVar1", FormulaValue.New("Value #1"));
ResetVariable model =
this.CreateModel(
this.FormatDisplayName(nameof(ResetUndefinedValueAsync)),
FormatVariablePath("NoVar"));
// Act
ResetVariableExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyUndefined("NoVar");
this.VerifyState("MyVar1", FormulaValue.New("Value #1"));
}
private ResetVariable CreateModel(string displayName, string variablePath)
{
ResetVariable.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Variable = InitializablePropertyPath.Create(variablePath),
};
return AssignParent<ResetVariable>(actionBuilder);
}
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="RetrieveConversationMessageExecutor"/>.
/// </summary>
public sealed class RetrieveConversationMessageExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task RetrieveMessageSuccessfullyAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(nameof(RetrieveMessageSuccessfullyAsync),
"TestMessage");
}
private async Task ExecuteTestAsync(
string displayName,
string variableName)
{
// Arrange
MockAgentProvider mockAgentProvider = new();
RetrieveConversationMessage model = this.CreateModel(
this.FormatDisplayName(displayName),
FormatVariablePath(variableName),
"TestConversationId",
"DefaultMessageId");
RetrieveConversationMessageExecutor action = new(model, mockAgentProvider.Object, this.State);
// Act
await this.ExecuteAsync(action);
// Assert
ChatMessage? testMessage = mockAgentProvider.TestMessages?.FirstOrDefault();
Assert.NotNull(testMessage);
VerifyModel(model, action);
this.VerifyState(variableName, testMessage.ToRecord());
}
private RetrieveConversationMessage CreateModel(
string displayName,
string messageVariable,
string conversationId,
string messageId)
{
RetrieveConversationMessage.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Message = PropertyPath.Create(messageVariable),
ConversationId = StringExpression.Literal(conversationId),
MessageId = StringExpression.Literal(messageId)
};
return AssignParent<RetrieveConversationMessage>(actionBuilder);
}
}

View File

@@ -0,0 +1,113 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="RetrieveConversationMessagesExecutor"/>.
/// </summary>
public sealed class RetrieveConversationMessagesExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task RetrieveAllMessagesSuccessfullyAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(
nameof(RetrieveAllMessagesSuccessfullyAsync),
"TestMessages",
"TestConversationId");
}
[Fact]
public async Task RetrieveMessagesWithOptionalValuesAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(
nameof(RetrieveMessagesWithOptionalValuesAsync),
"TestMessages",
"TestConversationId",
limit: IntExpression.Literal(2),
after: StringExpression.Literal("11/01/2025"),
before: StringExpression.Literal("12/01/2025"),
sortOrder: EnumExpression<AgentMessageSortOrderWrapper>.Literal(AgentMessageSortOrderWrapper.Get(AgentMessageSortOrder.NewestFirst)));
}
private async Task ExecuteTestAsync(
string displayName,
string variableName,
string conversationId,
IntExpression? limit = null,
StringExpression? after = null,
StringExpression? before = null,
EnumExpression<AgentMessageSortOrderWrapper>? sortOrder = null)
{
// Arrange
MockAgentProvider mockAgentProvider = new();
RetrieveConversationMessages model = this.CreateModel(
this.FormatDisplayName(displayName),
FormatVariablePath(variableName),
conversationId,
limit,
after,
before,
sortOrder);
RetrieveConversationMessagesExecutor action = new(model, mockAgentProvider.Object, this.State);
// Act
await this.ExecuteAsync(action);
// Assert
var testMessages = mockAgentProvider.TestMessages;
Assert.NotNull(testMessages);
VerifyModel(model, action);
this.VerifyState(variableName, testMessages.ToTable());
}
private RetrieveConversationMessages CreateModel(
string displayName,
string variableName,
string conversationId,
IntExpression? limit,
StringExpression? after,
StringExpression? before,
EnumExpression<AgentMessageSortOrderWrapper>? sortOrder)
{
RetrieveConversationMessages.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Messages = PropertyPath.Create(variableName),
ConversationId = StringExpression.Literal(conversationId)
};
if (limit is not null)
{
actionBuilder.Limit = limit;
}
if (after is not null)
{
actionBuilder.MessageAfter = after;
}
if (before is not null)
{
actionBuilder.MessageBefore = before;
}
if (sortOrder is not null)
{
actionBuilder.SortOrder = sortOrder;
}
return AssignParent<RetrieveConversationMessages>(actionBuilder);
}
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="SendActivityExecutor"/>.
/// </summary>
public sealed class SendActivityExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task CaptureActivityAsync()
{
// Arrange
SendActivity model =
this.CreateModel(
this.FormatDisplayName(nameof(CaptureActivityAsync)),
"Test activity message");
// Act
SendActivityExecutor action = new(model, this.State);
WorkflowEvent[] events = await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
Assert.Contains(events, e => e is MessageActivityEvent);
}
private SendActivity CreateModel(string displayName, string activityMessage, string? summary = null)
{
MessageActivityTemplate.Builder activityBuilder =
new()
{
Summary = summary,
Text = { TemplateLine.Parse(activityMessage) },
};
SendActivity.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Activity = activityBuilder.Build(),
};
return AssignParent<SendActivity>(actionBuilder);
}
}

View File

@@ -0,0 +1,154 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="SetMultipleVariablesExecutor"/>.
/// </summary>
public sealed class SetMultipleVariablesExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task SetMultipleVariablesAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetMultipleVariablesAsync),
assignments: [
new AssignmentCase("Variable1", new NumberDataValue(42), FormulaValue.New(42)),
new AssignmentCase("Variable2", new StringDataValue("Test"), FormulaValue.New("Test")),
new AssignmentCase("Variable3", new BooleanDataValue(true), FormulaValue.New(true))
]);
}
[Fact]
public async Task SetMultipleVariablesWithExpressionsAsync()
{
// Arrange
this.State.Set("SourceNumber", FormulaValue.New(10));
this.State.Set("SourceText", FormulaValue.New("Hello"));
this.State.Bind();
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetMultipleVariablesWithExpressionsAsync),
assignments: [
new AssignmentCase("CalcVariable", ValueExpression.Expression("Local.SourceNumber * 2"), FormulaValue.New(20)),
new AssignmentCase("ConcatVariable", ValueExpression.Expression(@"Concatenate(Local.SourceText, "" World"")"), FormulaValue.New("Hello World")),
new AssignmentCase("BoolVariable", ValueExpression.Expression("Local.SourceNumber > 5"), FormulaValue.New(true))
]);
}
[Fact]
public async Task SetMultipleVariablesWithVariableReferencesAsync()
{
// Arrange
this.State.Set("Source1", FormulaValue.New(123));
this.State.Set("Source2", FormulaValue.New("Reference"));
this.State.Bind();
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetMultipleVariablesWithVariableReferencesAsync),
assignments: [
new AssignmentCase("Target1", ValueExpression.Variable(PropertyPath.TopicVariable("Source1")), FormulaValue.New(123)),
new AssignmentCase("Target2", ValueExpression.Variable(PropertyPath.TopicVariable("Source2")), FormulaValue.New("Reference"))
]);
}
[Fact]
public async Task SetMultipleVariablesWithNullValuesAsync()
{
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetMultipleVariablesWithNullValuesAsync),
assignments: [
new AssignmentCase("NullVar1", null, FormulaValue.NewBlank()),
new AssignmentCase("NormalVar", new StringDataValue("NotNull"), FormulaValue.New("NotNull")),
new AssignmentCase("NullVar2", null, FormulaValue.NewBlank())
]);
}
[Fact]
public async Task SetMultipleVariablesUpdateExistingAsync()
{
// Arrange
this.State.Set("ExistingVar1", FormulaValue.New(999));
this.State.Set("ExistingVar2", FormulaValue.New("OldValue"));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetMultipleVariablesUpdateExistingAsync),
assignments: [
new AssignmentCase("ExistingVar1", new NumberDataValue(111), FormulaValue.New(111)),
new AssignmentCase("ExistingVar2", new StringDataValue("NewValue"), FormulaValue.New("NewValue")),
new AssignmentCase("NewVar", new BooleanDataValue(false), FormulaValue.New(false))
]);
}
[Fact]
public async Task SetMultipleVariablesEmptyAssignmentsAsync()
{
// Arrange
SetMultipleVariables model = this.CreateModel(nameof(SetMultipleVariablesEmptyAssignmentsAsync), []);
// Arrange, Act, Assert
Assert.Throws<DeclarativeModelException>(() =>
{
// Empty variables assignment should fail RequiredProperties validation.
_ = new SetMultipleVariablesExecutor(model, this.State);
});
}
private async Task ExecuteTestAsync(string displayName, AssignmentCase[] assignments)
{
// Arrange
SetMultipleVariables model = this.CreateModel(displayName, assignments);
// Act
SetMultipleVariablesExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
foreach (AssignmentCase assignment in assignments)
{
this.VerifyState(assignment.VariableName, assignment.ExpectedValue);
}
}
private SetMultipleVariables CreateModel(string displayName, AssignmentCase[] assignments)
{
SetMultipleVariables.Builder actionBuilder = new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
};
foreach (AssignmentCase assignment in assignments)
{
ValueExpression.Builder? valueExpressionBuilder = assignment.ValueExpression switch
{
null => null,
DataValue dataValue => new ValueExpression.Builder(ValueExpression.Literal(dataValue)),
ValueExpression valueExpression => new ValueExpression.Builder(valueExpression),
_ => throw new System.ArgumentException($"Unsupported value type: {assignment.ValueExpression?.GetType().Name}")
};
actionBuilder.Assignments.Add(new VariableAssignment.Builder()
{
Variable = PropertyPath.Create(FormatVariablePath(assignment.VariableName)),
Value = valueExpressionBuilder,
});
}
return AssignParent<SetMultipleVariables>(actionBuilder);
}
private sealed record AssignmentCase(string VariableName, object? ValueExpression, FormulaValue ExpectedValue);
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="SetTextVariableExecutor"/>.
/// </summary>
public sealed class SetTextVariableExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public async Task SetLiteralValueAsync()
{
// Arrange
SetTextVariable model =
this.CreateModel(
this.FormatDisplayName(nameof(SetLiteralValueAsync)),
FormatVariablePath("TextVar"),
"Text variable value");
// Act
SetTextVariableExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("TextVar", FormulaValue.New("Text variable value"));
}
[Fact]
public async Task UpdateExistingValueAsync()
{
// Arrange
this.State.Set("TextVar", FormulaValue.New("Old value"));
SetTextVariable model =
this.CreateModel(
this.FormatDisplayName(nameof(UpdateExistingValueAsync)),
FormatVariablePath("TextVar"),
"New value");
// Act
SetTextVariableExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState("TextVar", FormulaValue.New("New value"));
}
private SetTextVariable CreateModel(string displayName, string variablePath, string textValue)
{
SetTextVariable.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Variable = InitializablePropertyPath.Create(variablePath),
Value = TemplateLine.Parse(textValue),
};
return AssignParent<SetTextVariable>(actionBuilder);
}
}

View File

@@ -0,0 +1,205 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Tests for <see cref="SetVariableExecutor"/>.
/// </summary>
public sealed class SetVariableExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
{
[Fact]
public void InvalidModel() =>
// Arrange, Act, Assert
Assert.Throws<DeclarativeModelException>(() => new SetVariableExecutor(new SetVariable(), this.State));
[Fact]
public async Task SetNumericValueAsync() =>
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetNumericValueAsync),
variableName: "TestVariable",
variableValue: new NumberDataValue(42),
expectedValue: FormulaValue.New(42));
[Fact]
public async Task SetStringValueAsync() =>
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetStringValueAsync),
variableName: "TestVariable",
variableValue: new StringDataValue("Text"),
expectedValue: FormulaValue.New("Text"));
[Fact]
public async Task SetBooleanValueAsync() =>
// Arrange, Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanValueAsync),
variableName: "TestVariable",
variableValue: new BooleanDataValue(true),
expectedValue: FormulaValue.New(true));
[Fact]
public async Task SetBooleanExpressionAsync()
{
// Arrange
ValueExpression.Builder expressionBuilder = new(ValueExpression.Expression("true || false"));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanExpressionAsync),
variableName: "TestVariable",
valueExpression: expressionBuilder,
expectedValue: FormulaValue.New(true));
}
[Fact]
public async Task SetNumberExpressionAsync()
{
// Arrange
ValueExpression.Builder expressionBuilder = new(ValueExpression.Expression("9 - 3"));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanExpressionAsync),
variableName: "TestVariable",
valueExpression: expressionBuilder,
expectedValue: FormulaValue.New(6));
}
[Fact]
public async Task SetStringExpressionAsync()
{
// Arrange
ValueExpression.Builder expressionBuilder = new(ValueExpression.Expression(@"Concatenate(""A"", ""B"", ""C"")"));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanExpressionAsync),
variableName: "TestVariable",
valueExpression: expressionBuilder,
expectedValue: FormulaValue.New("ABC"));
}
[Fact]
public async Task SetBooleanVariableAsync()
{
// Arrange
this.State.Set("Source", FormulaValue.New(true));
this.State.Bind();
ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source")));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanExpressionAsync),
variableName: "TestVariable",
valueExpression: expressionBuilder,
expectedValue: FormulaValue.New(true));
}
[Fact]
public async Task SetNumberVariableAsync()
{
// Arrange
this.State.Set("Source", FormulaValue.New(321));
this.State.Bind();
ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source")));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanExpressionAsync),
variableName: "TestVariable",
valueExpression: expressionBuilder,
expectedValue: FormulaValue.New(321));
}
[Fact]
public async Task SetStringVariableAsync()
{
// Arrange
this.State.Set("Source", FormulaValue.New("Test"));
this.State.Bind();
ValueExpression.Builder expressionBuilder = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source")));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(SetBooleanExpressionAsync),
variableName: "TestVariable",
valueExpression: expressionBuilder,
expectedValue: FormulaValue.New("Test"));
}
[Fact]
public async Task UpdateExistingValueAsync()
{
// Arrange
this.State.Set("VarA", FormulaValue.New(33));
// Act, Assert
await this.ExecuteTestAsync(
displayName: nameof(UpdateExistingValueAsync),
variableName: "VarA",
variableValue: new NumberDataValue(42),
expectedValue: FormulaValue.New(42));
}
private Task ExecuteTestAsync(
string displayName,
string variableName,
DataValue variableValue,
FormulaValue expectedValue)
{
// Arrange
ValueExpression.Builder expressionBuilder = new(ValueExpression.Literal(variableValue));
// Act & Assert
return this.ExecuteTestAsync(displayName, variableName, expressionBuilder, expectedValue);
}
private async Task ExecuteTestAsync(
string displayName,
string variableName,
ValueExpression.Builder valueExpression,
FormulaValue expectedValue)
{
// Arrange
SetVariable model =
this.CreateModel(
displayName,
FormatVariablePath(variableName),
valueExpression);
this.State.Set(variableName, FormulaValue.New(33));
// Act
SetVariableExecutor action = new(model, this.State);
await this.ExecuteAsync(action);
// Assert
VerifyModel(model, action);
this.VerifyState(variableName, expectedValue);
}
private SetVariable CreateModel(string displayName, string variablePath, ValueExpression.Builder valueExpression)
{
SetVariable.Builder actionBuilder =
new()
{
Id = this.CreateActionId(),
DisplayName = this.FormatDisplayName(displayName),
Variable = InitializablePropertyPath.Create(variablePath),
Value = valueExpression,
};
return AssignParent<SetVariable>(actionBuilder);
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
/// <summary>
/// Base test class for <see cref="DeclarativeActionExecutor"/> implementations.
/// </summary>
public abstract class WorkflowActionExecutorTest(ITestOutputHelper output) : WorkflowTest(output)
{
internal WorkflowFormulaState State { get; } = new(RecalcEngineFactory.Create());
protected ActionId CreateActionId() => new($"{this.GetType().Name}_{Guid.NewGuid():N}");
protected string FormatDisplayName(string name) => $"{this.GetType().Name}_{name}";
internal async Task<WorkflowEvent[]> ExecuteAsync(DeclarativeActionExecutor executor)
{
TestWorkflowExecutor workflowExecutor = new();
WorkflowBuilder workflowBuilder = new(workflowExecutor);
workflowBuilder.AddEdge(workflowExecutor, executor);
await using StreamingRun run = await InProcessExecution.StreamAsync(workflowBuilder.Build(), this.State);
WorkflowEvent[] events = await run.WatchStreamAsync().ToArrayAsync();
Assert.Contains(events, e => e is DeclarativeActionInvokedEvent);
Assert.Contains(events, e => e is DeclarativeActionCompletedEvent);
ExecutorFailedEvent[] failureEvents = events.OfType<ExecutorFailedEvent>().ToArray();
switch (failureEvents.Length)
{
case 0:
break;
case 1:
throw failureEvents[0].Data ?? new XunitException("Executor failed without exception data.");
default:
AggregateException aggregateException = new("One or more executor failures occurred.", failureEvents.Select(e => e.Data).Where(e => e is not null).Cast<Exception>());
throw aggregateException;
}
return events;
}
internal static void VerifyModel(DialogAction model, DeclarativeActionExecutor action)
{
Assert.Equal(model.Id, action.Id);
Assert.Equal(model, action.Model);
}
protected void VerifyState(string variableName, FormulaValue expectedValue) => this.VerifyState(variableName, WorkflowFormulaState.DefaultScopeName, expectedValue);
internal void VerifyState(string variableName, string scopeName, FormulaValue expectedValue)
{
FormulaValue actualValue = this.State.Get(variableName, scopeName);
Assert.Equal(expectedValue.Format(), actualValue.Format());
}
internal void VerifyUndefined(string variableName, string? scopeName = null) =>
Assert.IsType<BlankValue>(this.State.Get(variableName, scopeName));
protected static TAction AssignParent<TAction>(DialogAction.Builder actionBuilder) where TAction : DialogAction
{
OnActivity.Builder activityBuilder =
new()
{
Id = new("root"),
};
activityBuilder.Actions.Add(actionBuilder);
OnActivity model = activityBuilder.Build();
return (TAction)model.Actions[0];
}
internal sealed class TestWorkflowExecutor() : Executor<WorkflowFormulaState>("test_workflow")
{
public override async ValueTask HandleAsync(WorkflowFormulaState message, IWorkflowContext context, CancellationToken cancellationToken) =>
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx.Functions;
public sealed class AgentMessageTests
{
[Fact]
public void Construct_Function()
{
AgentMessage function = new();
Assert.NotNull(function);
}
[Fact]
public void Execute_ReturnsBlank_ForEmptyInput()
{
// Arrange
StringValue sourceValue = FormulaValue.New(string.Empty);
// Act
FormulaValue result = AgentMessage.Execute(sourceValue);
// Assert
Assert.IsType<BlankValue>(result);
}
[Fact]
public void Execute_ReturnsExpectedRecord_ForNonEmptyInput()
{
const string Text = "Hello";
FormulaValue sourceValue = FormulaValue.New(Text);
StringValue stringValue = Assert.IsType<StringValue>(sourceValue);
FormulaValue result = AgentMessage.Execute(stringValue);
RecordValue recordResult = Assert.IsType<RecordValue>(result, exactMatch: false);
// Discriminator
FormulaValue discriminator = recordResult.GetField(TypeSchema.Discriminator);
StringValue discriminatorValue = Assert.IsType<StringValue>(discriminator);
Assert.Equal(nameof(ChatMessage), discriminatorValue.Value);
// Role
FormulaValue role = recordResult.GetField(TypeSchema.Message.Fields.Role);
StringValue roleValue = Assert.IsType<StringValue>(role);
Assert.Equal(ChatRole.Assistant.Value, roleValue.Value);
// Content table
FormulaValue content = recordResult.GetField(TypeSchema.Message.Fields.Content);
TableValue table = Assert.IsType<TableValue>(content, exactMatch: false);
List<RecordValue> rows = table.Rows.Select(value => value.Value).ToList();
Assert.Single(rows);
StringValue contentType = Assert.IsType<StringValue>(rows[0].GetField(TypeSchema.Message.Fields.ContentType));
Assert.Equal(TypeSchema.Message.ContentTypes.Text, contentType.Value);
StringValue contentValue = Assert.IsType<StringValue>(rows[0].GetField(TypeSchema.Message.Fields.ContentValue));
Assert.Equal(Text, contentValue.Value);
}
}

View File

@@ -0,0 +1,113 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx.Functions;
public sealed class MessageTextTests
{
[Fact]
public void Construct_Function()
{
MessageText.StringInput function1 = new();
Assert.NotNull(function1);
MessageText.RecordInput function2 = new();
Assert.NotNull(function2);
MessageText.TableInput function3 = new();
Assert.NotNull(function3);
}
[Fact]
public void Execute_ReturnsEmpty_ForEmptyInput()
{
// Arrange
StringValue sourceValue = FormulaValue.New(string.Empty);
// Act
FormulaValue result = MessageText.StringInput.Execute(sourceValue);
// Assert
StringValue stringResult = Assert.IsType<StringValue>(result);
Assert.Empty(stringResult.Value);
}
[Fact]
public void Execute_ReturnsText_ForStringInput()
{
// Arrange
StringValue sourceValue = FormulaValue.New("wowsie");
// Act
FormulaValue result = MessageText.StringInput.Execute(sourceValue);
// Assert
StringValue stringResult = Assert.IsType<StringValue>(result);
Assert.Equal(sourceValue.Value, stringResult.Value);
}
[Fact]
public void Execute_ReturnsText_ForMessageInput()
{
// Arrange
RecordValue sourceValue = new ChatMessage(ChatRole.User, "test message").ToRecord();
// Act
FormulaValue result = MessageText.RecordInput.Execute(sourceValue);
// Assert
StringValue stringResult = Assert.IsType<StringValue>(result);
Assert.Equal("test message", stringResult.Value);
}
[Fact]
public void Execute_ReturnsEmpty_ForUnknownInput()
{
// Arrange
RecordValue sourceValue = FormulaValue.NewRecordFromFields(new NamedValue("Anything", FormulaValue.New(333)));
// Act
FormulaValue result = MessageText.RecordInput.Execute(sourceValue);
// Assert
StringValue stringResult = Assert.IsType<StringValue>(result);
Assert.Empty(stringResult.Value);
}
[Fact]
public void Execute_ReturnsText_ForMessagesInput()
{
// Arrange
TableValue sourceValue = new ChatMessage[]
{
new(ChatRole.User, "test message 1"),
new(ChatRole.User, "test message 2"),
}.ToTable();
// Act
FormulaValue result = MessageText.TableInput.Execute(sourceValue);
// Assert
StringValue stringResult = Assert.IsType<StringValue>(result);
Assert.Equal("test message 1\ntest message 2", stringResult.Value);
}
[Fact]
public void Execute_ReturnsEmpty_ForEmptyList()
{
// Arrange
TableValue sourceValue = Array.Empty<ChatMessage>().ToTable();
// Act
FormulaValue result = MessageText.TableInput.Execute(sourceValue);
// Assert
StringValue stringResult = Assert.IsType<StringValue>(result);
Assert.Empty(stringResult.Value);
}
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions;
using Microsoft.Extensions.AI;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx.Functions;
public class UserMessageTests
{
[Fact]
public void Construct_Function()
{
UserMessage function = new();
Assert.NotNull(function);
}
[Fact]
public void Execute_ReturnsBlank_ForEmptyInput()
{
// Arrange
StringValue sourceValue = FormulaValue.New(string.Empty);
// Act
FormulaValue result = UserMessage.Execute(sourceValue);
// Assert
Assert.IsType<BlankValue>(result);
}
[Fact]
public void Execute_ReturnsExpectedRecord_ForNonEmptyInput()
{
const string Text = "Hello";
FormulaValue sourceValue = FormulaValue.New(Text);
StringValue stringValue = Assert.IsType<StringValue>(sourceValue);
FormulaValue result = UserMessage.Execute(stringValue);
RecordValue recordResult = Assert.IsType<RecordValue>(result, exactMatch: false);
// Discriminator
FormulaValue discriminator = recordResult.GetField(TypeSchema.Discriminator);
StringValue discriminatorValue = Assert.IsType<StringValue>(discriminator);
Assert.Equal(nameof(ChatMessage), discriminatorValue.Value);
// Role
FormulaValue role = recordResult.GetField(TypeSchema.Message.Fields.Role);
StringValue roleValue = Assert.IsType<StringValue>(role);
Assert.Equal(ChatRole.User.Value, roleValue.Value);
// Content table
FormulaValue content = recordResult.GetField(TypeSchema.Message.Fields.Content);
TableValue table = Assert.IsType<TableValue>(content, exactMatch: false);
List<RecordValue> rows = table.Rows.Select(value => value.Value).ToList();
Assert.Single(rows);
StringValue contentType = Assert.IsType<StringValue>(rows[0].GetField(TypeSchema.Message.Fields.ContentType));
Assert.Equal(TypeSchema.Message.ContentTypes.Text, contentType.Value);
StringValue contentValue = Assert.IsType<StringValue>(rows[0].GetField(TypeSchema.Message.Fields.ContentValue));
Assert.Equal(Text, contentValue.Value);
}
}

View File

@@ -0,0 +1,81 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.PowerFx;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx;
public class RecalcEngineFactoryTests(ITestOutputHelper output) : WorkflowTest(output)
{
[Fact]
public void DefaultNotNull()
{
// Act
RecalcEngine engine = RecalcEngineFactory.Create();
// Assert
Assert.NotNull(engine);
}
[Fact]
public void NewInstanceEachTime()
{
// Act
RecalcEngine engine1 = RecalcEngineFactory.Create();
RecalcEngine engine2 = RecalcEngineFactory.Create();
// Assert
Assert.NotNull(engine1);
Assert.NotNull(engine2);
Assert.NotSame(engine1, engine2);
}
[Fact]
public void HasSetFunctionEnabled()
{
// Arrange
RecalcEngine engine = RecalcEngineFactory.Create();
// Act
CheckResult result = engine.Check("1+1");
// Assert
Assert.True(result.IsSuccess);
}
[Fact]
public void HasCorrectMaximumExpressionLength()
{
// Arrange
RecalcEngine engine = RecalcEngineFactory.Create(2000, 3);
// Assert
Assert.Equal(2000, engine.Config.MaximumExpressionLength);
Assert.Equal(3, engine.Config.MaxCallDepth);
// Act: Create a long expression that is within the limit
string goodExpression = string.Concat(GenerateExpression(999));
CheckResult goodResult = engine.Check(goodExpression);
// Assert
Assert.True(goodResult.IsSuccess);
// Act: Create a long expression that exceeds the limit
string longExpression = string.Concat(GenerateExpression(1001));
CheckResult longResult = engine.Check(longExpression);
// Assert
Assert.False(longResult.IsSuccess);
static IEnumerable<string> GenerateExpression(int elements)
{
yield return "1";
for (int i = 0; i < elements - 1; i++)
{
yield return "+1";
}
}
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.PowerFx;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx;
/// <summary>
/// Base test class for PowerFx engine tests.
/// </summary>
public abstract class RecalcEngineTest(ITestOutputHelper output) : WorkflowTest(output)
{
internal WorkflowFormulaState State { get; } = new(RecalcEngineFactory.Create());
protected RecalcEngine Engine => this.State.Engine;
}

View File

@@ -0,0 +1,140 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx;
public class TemplateExtensionsTests(ITestOutputHelper output) : RecalcEngineTest(output)
{
[Fact]
public void FormatTemplateLines()
{
// Arrange
List<TemplateLine> template =
[
TemplateLine.Parse("Hello"),
TemplateLine.Parse(" "),
TemplateLine.Parse("World"),
];
// Act
string? result = this.Engine.Format(template);
// Assert
Assert.Equal("Hello World", result);
}
[Fact]
public void FormatTemplateLinesEmpty()
{
// Arrange
List<TemplateLine> template = [];
// Act
string? result = this.Engine.Format(template);
// Assert
Assert.Equal(string.Empty, result);
}
[Fact]
public void FormatTemplateLine()
{
// Arrange
TemplateLine line = TemplateLine.Parse("Test");
// Act
string? result = this.Engine.Format(line);
// Assert
Assert.Equal("Test", result);
}
[Fact]
public void FormatTemplateLineNull()
{
// Arrange
TemplateLine? line = null;
// Act
string? result = this.Engine.Format(line);
// Assert
Assert.Equal(string.Empty, result);
}
[Fact]
public void FormatTextSegment()
{
// Arrange
TemplateSegment textSegment = TemplateSegment.FromText("Hello World");
TemplateLine line = new([textSegment]);
// Act
string? result = this.Engine.Format(line);
// Assert
Assert.Equal("Hello World", result);
}
[Fact]
public void FormatExpressionSegment()
{
// Arrange
ExpressionSegment expressionSegment = new(ValueExpression.Expression("1 + 1"));
TemplateLine line = new([expressionSegment]);
// Act
string? result = this.Engine.Format(line);
// Assert
Assert.Equal("2", result);
}
[Fact]
public void FormatVariableSegment()
{
// Arrange
this.State.Set("Source", FormulaValue.New("Hello World"));
this.State.Bind();
ExpressionSegment expressionSegment = new(ValueExpression.Variable(PropertyPath.TopicVariable("Source")));
TemplateLine line = new([expressionSegment]);
// Act
string? result = this.Engine.Format(line);
// Assert
Assert.Equal("Hello World", result);
}
[Fact]
public void FormatExpressionSegmentUndefined()
{
// Arrange
ExpressionSegment expressionSegment = new();
TemplateLine line = new([expressionSegment]);
// Act & Assert
Assert.Throws<DeclarativeModelException>(() => this.Engine.Format(line));
}
[Fact]
public void FormatMultipleSegments()
{
// Arrange
TemplateSegment textSegment = TemplateSegment.FromText("Hello ");
ExpressionSegment expressionSegment = new(ValueExpression.Expression(@"""World"""));
TemplateLine line = new([textSegment, expressionSegment]);
// Act
string? result = this.Engine.Format(line);
// Assert
Assert.Equal("Hello World", result);
}
}

View File

@@ -0,0 +1,552 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Immutable;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.Bot.ObjectModel.Abstractions;
using Microsoft.Bot.ObjectModel.Exceptions;
using Microsoft.PowerFx.Types;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.PowerFx;
public class WorkflowExpressionEngineTests : RecalcEngineTest
{
private static class Variables
{
public const string GlobalValue = nameof(GlobalValue);
public const string BoolValue = nameof(BoolValue);
public const string StringValue = nameof(StringValue);
public const string IntValue = nameof(IntValue);
public const string NumberValue = nameof(NumberValue);
public const string EnumValue = nameof(EnumValue);
public const string ObjectValue = nameof(ObjectValue);
public const string ArrayValue = nameof(ArrayValue);
public const string BlankValue = nameof(BlankValue);
}
public static readonly RecordValue ObjectData = FormulaValue.NewRecordFromFields(new NamedValue(nameof(EnvironmentVariableReference.SchemaName), FormulaValue.New("test")));
public static readonly TableValue TableData = FormulaValue.NewSingleColumnTable(FormulaValue.New("a"), FormulaValue.New("b"));
public WorkflowExpressionEngineTests(ITestOutputHelper output)
: base(output)
{
this.State.Set(Variables.GlobalValue, FormulaValue.New(255), VariableScopeNames.Global);
this.State.Set(Variables.BoolValue, FormulaValue.New(true));
this.State.Set(Variables.StringValue, FormulaValue.New("Hello World"));
this.State.Set(Variables.IntValue, FormulaValue.New(long.MaxValue));
this.State.Set(Variables.NumberValue, FormulaValue.New(33.3));
this.State.Set(Variables.EnumValue, FormulaValue.New(nameof(VariablesToClear.ConversationScopedVariables)));
this.State.Set(Variables.ObjectValue, ObjectData);
this.State.Set(Variables.ArrayValue, TableData);
this.State.Set(Variables.BlankValue, FormulaValue.NewBlank());
this.State.Bind();
}
#region BoolExpression Tests
[Fact]
public void BoolExpressionGetValueForNull() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<ArgumentNullException>((BoolExpression)null!);
[Fact]
public void BoolExpressionGetValueForInvalid() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<InvalidExpressionOutputTypeException>(BoolExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)));
[Fact]
public void BoolExpressionGetValueForLiteral() =>
// Arrange, Act & Assert
this.EvaluateExpression(
BoolExpression.Literal(true),
expectedValue: true);
[Fact]
public void BoolExpressionGetValueForBlank() =>
// Arrange, Act & Assert
this.EvaluateExpression(
BoolExpression.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
expectedValue: false);
[Fact]
public void BoolExpressionGetValueForVariable()
{
// Arrange, Act & Assert
this.EvaluateExpression(
BoolExpression.Variable(PropertyPath.TopicVariable(Variables.BoolValue)),
expectedValue: true);
}
[Fact]
public void BoolExpressionGetValueForFormula() =>
// Arrange, Act & Assert
this.EvaluateExpression(
BoolExpression.Expression("true || false"),
expectedValue: true);
#endregion
#region StringExpression Tests
[Fact]
public void StringExpressionGetValueForNull() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<ArgumentNullException>((StringExpression)null!);
[Fact]
public void StringExpressionGetValueForInvalid() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<InvalidExpressionOutputTypeException>(StringExpression.Variable(PropertyPath.TopicVariable(Variables.BoolValue)));
[Fact]
public void StringExpressionGetValueForStringExpressionBlank() =>
// Arrange, Act & Assert
this.EvaluateExpression(
StringExpression.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
expectedValue: string.Empty);
[Fact]
public void StringExpressionGetValueForLiteral() =>
// Arrange, Act & Assert
this.EvaluateExpression(
StringExpression.Literal("test"),
expectedValue: "test");
[Fact]
public void StringExpressionGetValueForVariable()
{
// Arrange, Act & Assert
this.EvaluateExpression(
StringExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)),
expectedValue: "Hello World");
}
[Fact]
public void StringExpressionGetValueForFormula() =>
// Arrange, Act & Assert
this.EvaluateExpression(
StringExpression.Expression(@"""A"" & ""B"""),
expectedValue: "AB");
[Fact]
public void StringExpressionGetValueForRecord()
{
// 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(
StringExpression.Variable(PropertyPath.Create("Global.TestRecord")),
expectedValue:
"""
{
"test": "value"
}
""".Replace("\n", Environment.NewLine));
}
#endregion
#region IntExpression Tests
[Fact]
public void IntExpressionGetValueForNull() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<ArgumentNullException>((IntExpression)null!);
[Fact]
public void IntExpressionGetValueForInvalid() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<InvalidExpressionOutputTypeException>(IntExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)));
[Fact]
public void IntExpressionGetValueForIntExpressionBlank() =>
// Arrange, Act & Assert
this.EvaluateExpression(
IntExpression.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
expectedValue: 0);
[Fact]
public void IntExpressionGetValueForLiteral() =>
// Arrange, Act & Assert
this.EvaluateExpression(
IntExpression.Literal(7),
expectedValue: 7);
[Fact]
public void IntExpressionGetValueForVariable()
{
// Arrange, Act & Assert
this.EvaluateExpression(
IntExpression.Variable(PropertyPath.TopicVariable(Variables.IntValue)),
expectedValue: long.MaxValue);
}
[Fact]
public void IntExpressionGetValueForFormula() =>
// Arrange, Act & Assert
this.EvaluateExpression(
IntExpression.Expression("1 + 6"),
expectedValue: 7);
#endregion
#region NumberExpression Tests
[Fact]
public void NumberExpressionGetValueForNull() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<ArgumentNullException>((NumberExpression)null!);
[Fact]
public void NumberExpressionGetValueForInvalid() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<InvalidExpressionOutputTypeException>(NumberExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)));
[Fact]
public void NumberExpressionGetValueForBlank() =>
// Arrange, Act & Assert
this.EvaluateExpression(
NumberExpression.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
expectedValue: 0);
[Fact]
public void NumberExpressionGetValueForLiteral() =>
// Arrange, Act & Assert
this.EvaluateExpression(
NumberExpression.Literal(3.14),
expectedValue: 3.14);
[Fact]
public void NumberExpressionGetValueForVariable() =>
// Arrange, Act & Assert
this.EvaluateExpression(
NumberExpression.Variable(PropertyPath.TopicVariable(Variables.NumberValue)),
expectedValue: 33.3);
[Fact]
public void NumberExpressionGetValueForFormula() =>
// Arrange, Act & Assert
this.EvaluateExpression(
NumberExpression.Expression("31.1 + 2.2"),
expectedValue: 33.3);
#endregion
#region DataValueExpression Tests
[Fact]
public void DataValueExpressionGetValueForNull() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<ArgumentNullException>((ValueExpression)null!);
[Fact]
public void DataValueExpressionGetValueForDataValueExpressionBlank() =>
// Arrange, Act & Assert
this.EvaluateExpression(
ValueExpression.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
expectedValue: DataValue.Blank());
[Fact]
public void DataValueExpressionGetValueForLiteral() =>
// Arrange, Act & Assert
this.EvaluateExpression(
ValueExpression.Literal(DataValue.Create("test")),
expectedValue: DataValue.Create("test"));
[Fact]
public void DataValueExpressionGetValueForVariable()
{
// Arrange, Act & Assert
this.EvaluateExpression(
ValueExpression.Variable(PropertyPath.TopicVariable(Variables.StringValue)),
expectedValue: DataValue.Create("Hello World"));
}
[Fact]
public void DataValueExpressionGetValueForFormula() =>
// Arrange, Act & Assert
this.EvaluateExpression(
ValueExpression.Expression(@"""A"" & ""B"""),
expectedValue: DataValue.Create("AB"));
#endregion
#region EnumExpression Tests
[Fact]
public void EnumExpressionGetValueForNull() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<VariablesToClearWrapper, ArgumentNullException>((EnumExpression<VariablesToClearWrapper>)null!);
[Fact]
public void EnumExpressionGetValueForInvalid() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<VariablesToClearWrapper, InvalidExpressionOutputTypeException>(EnumExpression<VariablesToClearWrapper>.Variable(PropertyPath.TopicVariable(Variables.BoolValue)));
[Fact]
public void EnumExpressionGetValueForLiteral() =>
// Arrange, Act & Assert
this.EvaluateExpression(
EnumExpression<VariablesToClearWrapper>.Literal(VariablesToClearWrapper.Get(VariablesToClear.ConversationScopedVariables)),
expectedValue: VariablesToClear.ConversationScopedVariables);
[Fact]
public void EnumExpressionGetValueForBlank() =>
// Arrange, Act & Assert
this.EvaluateExpression(
EnumExpression<VariablesToClearWrapper>.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
expectedValue: VariablesToClear.ConversationScopedVariables);
[Fact]
public void EnumExpressionGetValueForVariable()
{
// Arrange, Act & Assert
this.EvaluateExpression(
EnumExpression<VariablesToClearWrapper>.Variable(PropertyPath.TopicVariable(Variables.EnumValue)),
expectedValue: VariablesToClear.ConversationScopedVariables);
}
[Fact]
public void EnumExpressionGetValueForFormula() =>
// Arrange, Act & Assert
this.EvaluateExpression(
EnumExpression<VariablesToClearWrapper>.Expression(@"""ConversationScoped"" & ""Variables"""),
expectedValue: VariablesToClear.ConversationScopedVariables);
#endregion
#region ObjectExpression Tests
[Fact]
public void ObjectExpressionGetValueForNull() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<RecordDataValue, ArgumentNullException>((ObjectExpression<RecordDataValue>)null!);
[Fact]
public void ObjectExpressionGetValueForInvalid() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<RecordDataValue, InvalidExpressionOutputTypeException>(ObjectExpression<RecordDataValue>.Variable(PropertyPath.TopicVariable(Variables.BoolValue)));
[Fact]
public void ObjectExpressionGetValueForLiteral()
{
// Arrange, Act & Assert
RecordDataValue.Builder recordBuilder = new();
recordBuilder.Properties.Add(nameof(EnvironmentVariableReference.SchemaName), new StringDataValue("test"));
RecordDataValue objectRecord = recordBuilder.Build();
_ = new EnvironmentVariableReference.Builder() { SchemaName = "test" }.Build();
this.EvaluateExpression(
ObjectExpression<RecordDataValue>.Literal(objectRecord),
expectedValue: objectRecord);
}
[Fact]
public void ObjectExpressionGetValueForBlank() =>
// Arrange, Act & Assert
this.EvaluateExpression(
ObjectExpression<RecordDataValue>.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
expectedValue: null);
[Fact]
public void ObjectExpressionGetValueForVariable()
{
// Arrange, Act & Assert
this.EvaluateExpression(
ObjectExpression<RecordDataValue>.Variable(PropertyPath.TopicVariable(Variables.ObjectValue)),
expectedValue: ObjectData.ToRecord());
}
#endregion
#region ArrayExpression Tests
[Fact]
public void ArrayExpressionGetValueForNull() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<string, ArgumentNullException>((ArrayExpression<string>)null!);
[Fact]
public void ArrayExpressionGetValueForInvalid() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<string, InvalidExpressionOutputTypeException>(ArrayExpression<string>.Variable(PropertyPath.TopicVariable(Variables.BoolValue)));
[Fact]
public void ArrayExpressionGetValueForLiteral()
{
// Arrange, Act & Assert
string[] input = ["a", "b"];
this.EvaluateExpression(
ArrayExpression<string>.Literal(input.ToImmutableArray()),
expectedValue: input);
}
[Fact]
public void ArrayExpressionGetValueForBlank() =>
// Arrange, Act & Assert
this.EvaluateExpression(
ArrayExpression<string>.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
expectedValue: []);
[Fact]
public void ArrayExpressionGetValueForVariable()
{
// Arrange, Act & Assert
this.EvaluateExpression(
ArrayExpression<string>.Variable(PropertyPath.TopicVariable(Variables.ArrayValue)),
expectedValue: ["a", "b"]);
}
[Fact]
public void ArrayExpressionGetValueForFormula() =>
// Arrange, Act & Assert
this.EvaluateExpression(
ArrayExpression<string>.Expression(@"[""a"", ""b""]"),
expectedValue: ["a", "b"]);
#endregion
#region ArrayExpressionOnly Tests
[Fact]
public void ArrayExpressionOnlyGetValueForNull() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<string, ArgumentNullException>((ArrayExpressionOnly<string>)null!);
[Fact]
public void ArrayExpressionOnlyGetValueForInvalid() =>
// Arrange, Act & Assert
this.EvaluateInvalidExpression<string, InvalidExpressionOutputTypeException>(ArrayExpressionOnly<string>.Variable(PropertyPath.TopicVariable(Variables.BoolValue)));
[Fact]
public void ArrayExpressionOnlyGetValueForBlank() =>
// Arrange, Act & Assert
this.EvaluateExpression(
ArrayExpressionOnly<string>.Variable(PropertyPath.TopicVariable(Variables.BlankValue)),
expectedValue: []);
[Fact]
public void ArrayExpressionOnlyGetValueForVariable()
{
// Arrange, Act & Assert
this.EvaluateExpression(
ArrayExpressionOnly<string>.Variable(PropertyPath.TopicVariable(Variables.ArrayValue)),
expectedValue: ["a", "b"]);
}
[Fact]
public void ArrayExpressionOnlyGetValueForFormula() =>
// Arrange, Act & Assert
this.EvaluateExpression(
ArrayExpressionOnly<string>.Expression(@"[""a"", ""b""]"),
expectedValue: ["a", "b"]);
#endregion
private EvaluationResult<bool> EvaluateExpression(BoolExpression expression, bool expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
=> this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity);
private void EvaluateInvalidExpression<TException>(BoolExpression expression)
where TException : Exception
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression));
private EvaluationResult<string> EvaluateExpression(StringExpression expression, string expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
=> this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity);
private void EvaluateInvalidExpression<TException>(StringExpression expression)
where TException : Exception
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression));
private EvaluationResult<long> EvaluateExpression(IntExpression expression, long expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
=> this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity);
private void EvaluateInvalidExpression<TException>(IntExpression expression)
where TException : Exception
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression));
private EvaluationResult<double> EvaluateExpression(NumberExpression expression, double expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
=> this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity);
private void EvaluateInvalidExpression<TException>(NumberExpression expression)
where TException : Exception
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression));
private EvaluationResult<DataValue> EvaluateExpression(ValueExpression expression, DataValue expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
=> this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity);
private void EvaluateInvalidExpression<TException>(ValueExpression expression)
where TException : Exception
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression));
private EvaluationResult<TEnum> EvaluateExpression<TEnum>(EnumExpression<TEnum> expression, TEnum expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
where TEnum : EnumWrapper
=> this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity);
private void EvaluateInvalidExpression<TEnum, TException>(EnumExpression<TEnum> expression)
where TEnum : EnumWrapper
where TException : Exception
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression));
private EvaluationResult<TValue?> EvaluateExpression<TValue>(ObjectExpression<TValue> expression, TValue? expectedValue, SensitivityLevel expectedSensitivity = SensitivityLevel.None)
where TValue : BotElement
=> this.EvaluateExpression((evaluator) => evaluator.GetValue(expression), expectedValue, expectedSensitivity);
private void EvaluateInvalidExpression<TValue, TException>(ObjectExpression<TValue> expression)
where TValue : BotElement
where TException : Exception
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression));
private ImmutableArray<TValue> EvaluateExpression<TValue>(ArrayExpression<TValue> expression, TValue[] expectedValue)
=> this.EvaluateArrayExpression((evaluator) => evaluator.GetValue(expression), expectedValue);
private void EvaluateInvalidExpression<TValue, TException>(ArrayExpression<TValue> expression)
where TException : Exception
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression));
private ImmutableArray<TValue> EvaluateExpression<TValue>(ArrayExpressionOnly<TValue> expression, TValue[] expectedValue)
=> this.EvaluateArrayExpression((evaluator) => evaluator.GetValue(expression), expectedValue);
private void EvaluateInvalidExpression<TValue, TException>(ArrayExpressionOnly<TValue> expression)
where TException : Exception
=> this.EvaluateInvalidExpression<TException>((evaluator) => evaluator.GetValue(expression));
private EvaluationResult<TValue> EvaluateExpression<TValue>(
Func<WorkflowExpressionEngine, EvaluationResult<TValue>> evaluator,
TValue? expectedValue,
SensitivityLevel expectedSensitivity = SensitivityLevel.None)
{
// Act
EvaluationResult<TValue> result = evaluator.Invoke(this.State.Evaluator);
// Assert
Assert.Equal(expectedValue, result.Value);
Assert.Equal(expectedSensitivity, result.Sensitivity);
return result;
}
private ImmutableArray<TValue> EvaluateArrayExpression<TValue>(
Func<WorkflowExpressionEngine, ImmutableArray<TValue>> evaluator,
TValue[] expectedValue)
{
// Act
ImmutableArray<TValue> result = evaluator.Invoke(this.State.Evaluator);
// Assert
Assert.Equal(expectedValue.Length, result.Length);
Assert.Equivalent(expectedValue, result);
return result;
}
private void EvaluateInvalidExpression<TException>(Action<WorkflowExpressionEngine> evaluator) where TException : Exception
{
// Act & Assert
Assert.Throws<TException>(() => evaluator.Invoke(this.State.Evaluator));
}
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Microsoft.PowerFx.Types;
namespace Microsoft.Agents.AI.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);
// 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");
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);
this.State.Set("key1", newValue);
// Assert
FormulaValue result = this.State.Get("key1");
Assert.Equal(newValue, result);
}
}

View File

@@ -0,0 +1,73 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.Extensions.Logging;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
public sealed class TestOutputAdapter(ITestOutputHelper output) : TextWriter, ILogger, ILoggerFactory
{
private readonly Stack<string> _scopes = [];
public override Encoding Encoding { get; } = Encoding.UTF8;
public void AddProvider(ILoggerProvider provider) => throw new NotSupportedException();
public ILogger CreateLogger(string categoryName) => this;
public bool IsEnabled(LogLevel logLevel) => true;
public override void WriteLine(object? value) => this.SafeWrite($"{value}");
public override void WriteLine(string? format, params object?[] arg) => this.SafeWrite(string.Format(format ?? string.Empty, arg));
public override void WriteLine(string? value) => this.SafeWrite(value ?? string.Empty);
public override void Write(object? value) => this.SafeWrite($"{value}");
public override void Write(char[]? buffer) => this.SafeWrite(new string(buffer));
public IDisposable BeginScope<TState>(TState state) where TState : notnull
{
this._scopes.Push($"{state}");
return new LoggerScope(() => this._scopes.Pop());
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
string message = formatter(state, exception);
string scope = this._scopes.Count > 0 ? $"[{this._scopes.Peek()}] " : string.Empty;
output.WriteLine($"{scope}{message}");
}
private void SafeWrite(string value)
{
try
{
output.WriteLine(value ?? string.Empty);
}
catch (InvalidOperationException exception) when (exception.Message == "There is no currently active test.")
{
// This exception is thrown when the test output is accessed outside of a test context.
// We can ignore it since we are not in a test context.
}
}
private sealed class LoggerScope(Action action) : IDisposable
{
private bool _disposed;
public void Dispose()
{
if (!this._disposed)
{
action.Invoke();
this._disposed = true;
}
}
}
}

View File

@@ -0,0 +1,7 @@
$generatedCodeFiles = Get-ChildItem -Name -Path .\bin\Debug\net10.0\Workflows -Filter *.g.cs
Write-Output "x$($generatedCodeFiles.Count)"
foreach ($file in $generatedCodeFiles) {
$baselineFile = $file -replace '\.g\.cs$', '.cs'
Write-Output $baselineFile
Copy-Item -Path ".\bin\Debug\net10.0\Workflows\$file" -Destination ".\Workflows\$baselineFile" -Force
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Bot.ObjectModel;
using Xunit.Abstractions;
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
/// <summary>
/// Base class for workflow tests.
/// </summary>
public abstract class WorkflowTest : IDisposable
{
public TestOutputAdapter Output { get; }
protected WorkflowTest(ITestOutputHelper output)
{
this.Output = new TestOutputAdapter(output);
Console.SetOut(this.Output);
SetProduct();
}
public void Dispose()
{
this.Dispose(isDisposing: true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool isDisposing)
{
if (isDisposing)
{
this.Output.Dispose();
}
}
protected static void SetProduct()
{
if (!ProductContext.IsLocalScopeSupported())
{
ProductContext.SetContext(Product.Foundry);
}
}
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 ?? WorkflowFormulaState.DefaultScopeName}.{variableName}";
}

View File

@@ -0,0 +1,119 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
#pragma warning disable IDE0005 // Extra using directive is ok.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Test.WorkflowProviders;
/// <summary>
/// This class provides a factory method to create a <see cref="Workflow" /> instance.
/// </summary>
/// <remarks>
/// The workflow defined here was generated from a declarative workflow definition.
/// Declarative workflows utilize Power FX for defining conditions and expressions.
/// To learn more about Power FX, see:
/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio
/// </remarks>
public static class WorkflowProvider
{
/// <summary>
/// The root executor for a declarative workflow.
/// </summary>
internal sealed class WorkflowTestRootExecutor<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage> inputTransform) :
RootExecutor<TInput>("workflow_test_Root", options, inputTransform)
where TInput : notnull
{
protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
// Initialize variables
await context.QueueStateUpdateAsync("MyMessage1", UnassignedValue.Instance, "Local").ConfigureAwait(false);
await context.QueueStateUpdateAsync("TestInput", UnassignedValue.Instance, "Local").ConfigureAwait(false);
}
}
/// <summary>
/// Adds a new message to the specified agent conversation
/// </summary>
internal sealed class AddMessageExecutor(FormulaSession session, WorkflowAgentProvider agentProvider) : ActionExecutor(id: "add_message", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string? conversationId = await context.ReadStateAsync<string>(key: "ConversationId", scopeName: "System").ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(conversationId))
{
throw new DeclarativeActionException($"Conversation identifier must be defined: {this.Id}");
}
ChatMessage newMessage = new(ChatRole.User, await this.GetContentAsync(context).ConfigureAwait(false)) { AdditionalProperties = this.GetMetadata() };
newMessage = await agentProvider.CreateMessageAsync(conversationId, newMessage, cancellationToken).ConfigureAwait(false);
await context.QueueStateUpdateAsync(key: "MyMessage1", value: newMessage, scopeName: "Local").ConfigureAwait(false);
return default;
}
private async ValueTask<IList<AIContent>> GetContentAsync(IWorkflowContext context)
{
List<AIContent> content = [];
string contentValue1 =
await context.FormatTemplateAsync(
"""
{Local.TestInput}
""");
content.Add(new TextContent(contentValue1));
return content;
}
private AdditionalPropertiesDictionary? GetMetadata()
{
Dictionary<string, object?>? metadata = null;
if (metadata is null)
{
return null;
}
return new AdditionalPropertiesDictionary(metadata);
}
}
public static Workflow CreateWorkflow<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage>? inputTransform = null)
where TInput : notnull
{
// Create root executor to initialize the workflow.
inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message);
WorkflowTestRootExecutor<TInput> workflowTestRoot = new(options, inputTransform);
DelegateExecutor workflowTest = new(id: "workflow_test", workflowTestRoot.Session);
AddMessageExecutor addMessage = new(workflowTestRoot.Session, options.AgentProvider);
// Define the workflow builder
WorkflowBuilder builder = new(workflowTestRoot);
// Connect executors
builder.AddEdge(workflowTestRoot, workflowTest);
builder.AddEdge(workflowTest, addMessage);
// Build the workflow
return builder.Build(validateOrphans: false);
}
}

View File

@@ -0,0 +1,15 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_test
actions:
- kind: AddConversationMessage
id: add_message
message: Local.MyMessage1
role: User
conversationId: =System.ConversationId
content:
- type: Text
value: {Local.TestInput}

View File

@@ -0,0 +1,4 @@
# empty yaml
- id: 1
- id: 2
- id: 3

View File

@@ -0,0 +1,8 @@
kind: Workflow
trigger:
kind: OnConversationStart
actions:
- kind: EndConversation
id: end_all

View File

@@ -0,0 +1,8 @@
kind: ToolDialog
beginDialog:
kind: OnActivity
id: my_workflow
type: Message
actions:
- kind: EndConversation
id: end_all

View File

@@ -0,0 +1,94 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
#pragma warning disable IDE0005 // Extra using directive is ok.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Test.WorkflowProviders;
/// <summary>
/// This class provides a factory method to create a <see cref="Workflow" /> instance.
/// </summary>
/// <remarks>
/// The workflow defined here was generated from a declarative workflow definition.
/// Declarative workflows utilize Power FX for defining conditions and expressions.
/// To learn more about Power FX, see:
/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio
/// </remarks>
public static class WorkflowProvider
{
/// <summary>
/// The root executor for a declarative workflow.
/// </summary>
internal sealed class MyWorkflowRootExecutor<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage> inputTransform) :
RootExecutor<TInput>("my_workflow_Root", options, inputTransform)
where TInput : notnull
{
protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
}
}
/// <summary>
/// Formats a message template and sends an activity event.
/// </summary>
internal sealed class SendActivity1Executor(FormulaSession session) : ActionExecutor(id: "send_activity_1", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string activityText =
await context.FormatTemplateAsync(
"""
NEVER 1!
"""
);
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]);
await context.AddEventAsync(new AgentResponseEvent(this.Id, response)).ConfigureAwait(false);
return default;
}
}
public static Workflow CreateWorkflow<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage>? inputTransform = null)
where TInput : notnull
{
// Create root executor to initialize the workflow.
inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message);
MyWorkflowRootExecutor<TInput> myWorkflowRoot = new(options, inputTransform);
DelegateExecutor myWorkflow = new(id: "my_workflow", myWorkflowRoot.Session);
DelegateExecutor endAll = new(id: "end_all", myWorkflowRoot.Session);
DelegateExecutor endAllRestart = new(id: "end_all_Restart", myWorkflowRoot.Session);
SendActivity1Executor sendActivity1 = new(myWorkflowRoot.Session);
// Define the workflow builder
WorkflowBuilder builder = new(myWorkflowRoot);
// Connect executors
builder.AddEdge(myWorkflowRoot, myWorkflow);
builder.AddEdge(myWorkflow, endAll);
builder.AddEdge(endAllRestart, sendActivity1);
// Build the workflow
return builder.Build(validateOrphans: false);
}
}

View File

@@ -0,0 +1,13 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: my_workflow
actions:
- kind: CancelWorkflow
id: end_all
- kind: SendActivity
id: send_activity_1
activity: NEVER 1!

View File

@@ -0,0 +1,29 @@
kind: WORKFLOW
trigger:
kind: onconversationstart
id: my_workflow
actions:
- kind: SETVARIABLE
id: set_input1
variable: Local.TestValue1
value: =3
- kind: setvariable
id: set_input2
variable: Local.TestValue2
value: =4
- kind: ConditionGroup
id: condition_test
conditions:
- id: condition_match
condition: =Local.TestValue1 + Local.TestValue2 = 7
actions:
- kind: EndWorkflow
id: end_when_match
- kind: SendActivity
id: activity_error
activity: Unexpected

View File

@@ -0,0 +1,85 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
#pragma warning disable IDE0005 // Extra using directive is ok.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Test.WorkflowProviders;
/// <summary>
/// This class provides a factory method to create a <see cref="Workflow" /> instance.
/// </summary>
/// <remarks>
/// The workflow defined here was generated from a declarative workflow definition.
/// Declarative workflows utilize Power FX for defining conditions and expressions.
/// To learn more about Power FX, see:
/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio
/// </remarks>
public static class WorkflowProvider
{
/// <summary>
/// The root executor for a declarative workflow.
/// </summary>
internal sealed class MyWorkflowRootExecutor<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage> inputTransform) :
RootExecutor<TInput>("my_workflow_Root", options, inputTransform)
where TInput : notnull
{
protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
}
}
/// <summary>
/// Reset all the state for the targeted variable scope.
/// </summary>
internal sealed class ClearAllExecutor(FormulaSession session) : ActionExecutor(id: "clear_all", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string? targetScopeName = "Local";
await context.QueueClearScopeAsync(targetScopeName).ConfigureAwait(false);
return default;
}
}
public static Workflow CreateWorkflow<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage>? inputTransform = null)
where TInput : notnull
{
// Create root executor to initialize the workflow.
inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message);
MyWorkflowRootExecutor<TInput> myWorkflowRoot = new(options, inputTransform);
DelegateExecutor myWorkflow = new(id: "my_workflow", myWorkflowRoot.Session);
ClearAllExecutor clearAll = new(myWorkflowRoot.Session);
// Define the workflow builder
WorkflowBuilder builder = new(myWorkflowRoot);
// Connect executors
builder.AddEdge(myWorkflowRoot, myWorkflow);
builder.AddEdge(myWorkflow, clearAll);
// Build the workflow
return builder.Build(validateOrphans: false);
}
}

View File

@@ -0,0 +1,11 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: my_workflow
actions:
- kind: ClearAllVariables
id: clear_all
variables: ConversationScopedVariables

View File

@@ -0,0 +1,201 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
#pragma warning disable IDE0005 // Extra using directive is ok.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Test.WorkflowProviders;
/// <summary>
/// This class provides a factory method to create a <see cref="Workflow" /> instance.
/// </summary>
/// <remarks>
/// The workflow defined here was generated from a declarative workflow definition.
/// Declarative workflows utilize Power FX for defining conditions and expressions.
/// To learn more about Power FX, see:
/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio
/// </remarks>
public static class WorkflowProvider
{
/// <summary>
/// The root executor for a declarative workflow.
/// </summary>
internal sealed class MyWorkflowRootExecutor<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage> inputTransform) :
RootExecutor<TInput>("my_workflow_Root", options, inputTransform)
where TInput : notnull
{
protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
// Initialize variables
await context.QueueStateUpdateAsync("TestValue", UnassignedValue.Instance, "Local").ConfigureAwait(false);
}
}
/// <summary>
/// Assigns an evaluated expression, other variable, or literal value to the "Local.TestValue" variable.
/// </summary>
internal sealed class SetvariableTestExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_test", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
object? evaluatedValue = await context.EvaluateValueAsync<object>("Value(System.LastMessageText)").ConfigureAwait(false);
await context.QueueStateUpdateAsync(key: "TestValue", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false);
return default;
}
}
/// <summary>
/// Conditional branching similar to an if / elseif / elseif / else chain.
/// </summary>
internal sealed class ConditiongroupTestExecutor(FormulaSession session) : ActionExecutor(id: "conditionGroup_test", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
bool condition0 = await context.EvaluateValueAsync<bool>("Mod(Local.TestValue, 2) = 1").ConfigureAwait(false);
if (condition0)
{
return "conditionItem_odd";
}
bool condition1 = await context.EvaluateValueAsync<bool>("Mod(Local.TestValue, 2) = 0").ConfigureAwait(false);
if (condition1)
{
return "conditionItem_even";
}
return "conditionGroup_testElseActions";
}
}
/// <summary>
/// Formats a message template and sends an activity event.
/// </summary>
internal sealed class SendactivityOddExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_odd", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string activityText =
await context.FormatTemplateAsync(
"""
ODD
"""
);
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]);
await context.AddEventAsync(new AgentResponseEvent(this.Id, response)).ConfigureAwait(false);
return default;
}
}
/// <summary>
/// Formats a message template and sends an activity event.
/// </summary>
internal sealed class SendactivityEvenExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_even", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string activityText =
await context.FormatTemplateAsync(
"""
EVEN
"""
);
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]);
await context.AddEventAsync(new AgentResponseEvent(this.Id, response)).ConfigureAwait(false);
return default;
}
}
/// <summary>
/// Formats a message template and sends an activity event.
/// </summary>
internal sealed class ActivityFinalExecutor(FormulaSession session) : ActionExecutor(id: "activity_final", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string activityText =
await context.FormatTemplateAsync(
"""
All done!
"""
);
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]);
await context.AddEventAsync(new AgentResponseEvent(this.Id, response)).ConfigureAwait(false);
return default;
}
}
public static Workflow CreateWorkflow<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage>? inputTransform = null)
where TInput : notnull
{
// Create root executor to initialize the workflow.
inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message);
MyWorkflowRootExecutor<TInput> myWorkflowRoot = new(options, inputTransform);
DelegateExecutor myWorkflow = new(id: "my_workflow", myWorkflowRoot.Session);
SetvariableTestExecutor setVariableTest = new(myWorkflowRoot.Session);
ConditiongroupTestExecutor conditionGroupTest = new(myWorkflowRoot.Session);
DelegateExecutor conditionItemOdd = new(id: "conditionItem_odd", myWorkflowRoot.Session);
DelegateExecutor conditionItemEven = new(id: "conditionItem_even", myWorkflowRoot.Session);
DelegateExecutor conditionItemOddactions = new(id: "conditionItem_oddActions", myWorkflowRoot.Session);
SendactivityOddExecutor sendActivityOdd = new(myWorkflowRoot.Session);
DelegateExecutor conditionItemEvenactions = new(id: "conditionItem_evenActions", myWorkflowRoot.Session);
SendactivityEvenExecutor sendActivityEven = new(myWorkflowRoot.Session);
DelegateExecutor conditionGroupTestPost = new(id: "conditionGroup_test_Post", myWorkflowRoot.Session);
ActivityFinalExecutor activityFinal = new(myWorkflowRoot.Session);
DelegateExecutor conditionItemOddPost = new(id: "conditionItem_odd_Post", myWorkflowRoot.Session);
DelegateExecutor conditionItemEvenPost = new(id: "conditionItem_even_Post", myWorkflowRoot.Session);
DelegateExecutor conditionItemOddactionsPost = new(id: "conditionItem_oddActions_Post", myWorkflowRoot.Session);
DelegateExecutor conditionItemEvenactionsPost = new(id: "conditionItem_evenActions_Post", myWorkflowRoot.Session);
// Define the workflow builder
WorkflowBuilder builder = new(myWorkflowRoot);
// Connect executors
builder.AddEdge(myWorkflowRoot, myWorkflow);
builder.AddEdge(myWorkflow, setVariableTest);
builder.AddEdge(setVariableTest, conditionGroupTest);
builder.AddEdge(conditionGroupTest, conditionItemOdd, (object? result) => ActionExecutor.IsMatch("conditionItem_odd", result));
builder.AddEdge(conditionGroupTest, conditionItemEven, (object? result) => ActionExecutor.IsMatch("conditionItem_even", result));
builder.AddEdge(conditionItemOdd, conditionItemOddactions);
builder.AddEdge(conditionItemOddactions, sendActivityOdd);
builder.AddEdge(conditionItemEven, conditionItemEvenactions);
builder.AddEdge(conditionItemEvenactions, sendActivityEven);
builder.AddEdge(conditionGroupTestPost, activityFinal);
builder.AddEdge(conditionItemOddPost, conditionGroupTestPost);
builder.AddEdge(conditionItemEvenPost, conditionGroupTestPost);
builder.AddEdge(sendActivityOdd, conditionItemOddactionsPost);
builder.AddEdge(conditionItemOddactionsPost, conditionItemOddPost);
builder.AddEdge(sendActivityEven, conditionItemEvenactionsPost);
builder.AddEdge(conditionItemEvenactionsPost, conditionItemEvenPost);
// Build the workflow
return builder.Build(validateOrphans: false);
}
}

View File

@@ -0,0 +1,32 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: my_workflow
actions:
- kind: SetVariable
id: setVariable_test
variable: Local.TestValue
value: =Value(System.LastMessageText)
- kind: ConditionGroup
id: conditionGroup_test
conditions:
- id: conditionItem_odd
condition: =Mod(Local.TestValue, 2) = 1
actions:
- kind: SendActivity
id: sendActivity_odd
activity: ODD
- id: conditionItem_even
condition: =Mod(Local.TestValue, 2) = 0
actions:
- kind: SendActivity
id: sendActivity_even
activity: EVEN
- kind: SendActivity
id: activity_final
activity: All done!

View File

@@ -0,0 +1,193 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
#pragma warning disable IDE0005 // Extra using directive is ok.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Test.WorkflowProviders;
/// <summary>
/// This class provides a factory method to create a <see cref="Workflow" /> instance.
/// </summary>
/// <remarks>
/// The workflow defined here was generated from a declarative workflow definition.
/// Declarative workflows utilize Power FX for defining conditions and expressions.
/// To learn more about Power FX, see:
/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio
/// </remarks>
public static class WorkflowProvider
{
/// <summary>
/// The root executor for a declarative workflow.
/// </summary>
internal sealed class MyWorkflowRootExecutor<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage> inputTransform) :
RootExecutor<TInput>("my_workflow_Root", options, inputTransform)
where TInput : notnull
{
protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
// Initialize variables
await context.QueueStateUpdateAsync("TestValue", UnassignedValue.Instance, "Local").ConfigureAwait(false);
}
}
/// <summary>
/// Assigns an evaluated expression, other variable, or literal value to the "Local.TestValue" variable.
/// </summary>
internal sealed class SetvariableTestExecutor(FormulaSession session) : ActionExecutor(id: "setVariable_test", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
object? evaluatedValue = await context.EvaluateValueAsync<object>("Value(System.LastMessageText)").ConfigureAwait(false);
await context.QueueStateUpdateAsync(key: "TestValue", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false);
return default;
}
}
/// <summary>
/// Conditional branching similar to an if / elseif / elseif / else chain.
/// </summary>
internal sealed class ConditiongroupTestExecutor(FormulaSession session) : ActionExecutor(id: "conditionGroup_test", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
bool condition0 = await context.EvaluateValueAsync<bool>("Mod(Local.TestValue, 2) = 1").ConfigureAwait(false);
if (condition0)
{
return "conditionItem_odd";
}
return "conditionGroup_testElseActions";
}
}
/// <summary>
/// Formats a message template and sends an activity event.
/// </summary>
internal sealed class SendactivityOddExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_odd", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string activityText =
await context.FormatTemplateAsync(
"""
ODD
"""
);
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]);
await context.AddEventAsync(new AgentResponseEvent(this.Id, response)).ConfigureAwait(false);
return default;
}
}
/// <summary>
/// Formats a message template and sends an activity event.
/// </summary>
internal sealed class SendactivityElseExecutor(FormulaSession session) : ActionExecutor(id: "sendActivity_else", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string activityText =
await context.FormatTemplateAsync(
"""
EVEN
"""
);
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]);
await context.AddEventAsync(new AgentResponseEvent(this.Id, response)).ConfigureAwait(false);
return default;
}
}
/// <summary>
/// Formats a message template and sends an activity event.
/// </summary>
internal sealed class ActivityFinalExecutor(FormulaSession session) : ActionExecutor(id: "activity_final", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string activityText =
await context.FormatTemplateAsync(
"""
All done!
"""
);
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]);
await context.AddEventAsync(new AgentResponseEvent(this.Id, response)).ConfigureAwait(false);
return default;
}
}
public static Workflow CreateWorkflow<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage>? inputTransform = null)
where TInput : notnull
{
// Create root executor to initialize the workflow.
inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message);
MyWorkflowRootExecutor<TInput> myWorkflowRoot = new(options, inputTransform);
DelegateExecutor myWorkflow = new(id: "my_workflow", myWorkflowRoot.Session);
SetvariableTestExecutor setVariableTest = new(myWorkflowRoot.Session);
ConditiongroupTestExecutor conditionGroupTest = new(myWorkflowRoot.Session);
DelegateExecutor conditionItemOdd = new(id: "conditionItem_odd", myWorkflowRoot.Session);
DelegateExecutor conditionGroupTestelseactions = new(id: "conditionGroup_testElseActions", myWorkflowRoot.Session);
DelegateExecutor conditionItemOddactions = new(id: "conditionItem_oddActions", myWorkflowRoot.Session);
SendactivityOddExecutor sendActivityOdd = new(myWorkflowRoot.Session);
DelegateExecutor conditionItemOddRestart = new(id: "conditionItem_odd_Restart", myWorkflowRoot.Session);
SendactivityElseExecutor sendActivityElse = new(myWorkflowRoot.Session);
DelegateExecutor conditionGroupTestPost = new(id: "conditionGroup_test_Post", myWorkflowRoot.Session);
ActivityFinalExecutor activityFinal = new(myWorkflowRoot.Session);
DelegateExecutor conditionItemOddPost = new(id: "conditionItem_odd_Post", myWorkflowRoot.Session);
DelegateExecutor conditionItemOddactionsPost = new(id: "conditionItem_oddActions_Post", myWorkflowRoot.Session);
DelegateExecutor conditionGroupTestelseactionsPost = new(id: "conditionGroup_testElseActions_Post", myWorkflowRoot.Session);
// Define the workflow builder
WorkflowBuilder builder = new(myWorkflowRoot);
// Connect executors
builder.AddEdge(myWorkflowRoot, myWorkflow);
builder.AddEdge(myWorkflow, setVariableTest);
builder.AddEdge(setVariableTest, conditionGroupTest);
builder.AddEdge(conditionGroupTest, conditionItemOdd, (object? result) => ActionExecutor.IsMatch("conditionItem_odd", result));
builder.AddEdge(conditionGroupTest, conditionGroupTestelseactions, (object? result) => ActionExecutor.IsMatch("conditionGroup_testElseActions", result));
builder.AddEdge(conditionItemOdd, conditionItemOddactions);
builder.AddEdge(conditionItemOddactions, sendActivityOdd);
builder.AddEdge(conditionItemOddRestart, conditionGroupTestelseactions);
builder.AddEdge(conditionGroupTestelseactions, sendActivityElse);
builder.AddEdge(conditionGroupTestPost, activityFinal);
builder.AddEdge(conditionItemOddPost, conditionGroupTestPost);
builder.AddEdge(sendActivityOdd, conditionItemOddactionsPost);
builder.AddEdge(conditionItemOddactionsPost, conditionItemOddPost);
builder.AddEdge(sendActivityElse, conditionGroupTestelseactionsPost);
builder.AddEdge(conditionGroupTestelseactionsPost, conditionGroupTestPost);
// Build the workflow
return builder.Build(validateOrphans: false);
}
}

View File

@@ -0,0 +1,29 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: my_workflow
actions:
- kind: SetVariable
id: setVariable_test
variable: Local.TestValue
value: =Value(System.LastMessageText)
- kind: ConditionGroup
id: conditionGroup_test
conditions:
- id: conditionItem_odd
condition: =Mod(Local.TestValue, 2) = 1
actions:
- kind: SendActivity
id: sendActivity_odd
activity: ODD
elseActions:
- kind: SendActivity
id: sendActivity_else
activity: EVEN
- kind: SendActivity
id: activity_final
activity: All done!

View File

@@ -0,0 +1,25 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: my_workflow
actions:
- kind: SetVariable
id: setVariable_test
variable: Local.TestValue
value: =Value(System.LastMessageText)
- kind: ConditionGroup
id: conditionGroup_test
conditions:
- id: conditionItem_odd
condition: =Mod(Local.TestValue, 2) = 1
actions:
- kind: SendActivity
id: sendActivity_odd
activity: ODD
- kind: SendActivity
id: activity_final
activity: All done!

View File

@@ -0,0 +1,95 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
#pragma warning disable IDE0005 // Extra using directive is ok.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Test.WorkflowProviders;
/// <summary>
/// This class provides a factory method to create a <see cref="Workflow" /> instance.
/// </summary>
/// <remarks>
/// The workflow defined here was generated from a declarative workflow definition.
/// Declarative workflows utilize Power FX for defining conditions and expressions.
/// To learn more about Power FX, see:
/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio
/// </remarks>
public static class WorkflowProvider
{
/// <summary>
/// The root executor for a declarative workflow.
/// </summary>
internal sealed class WorkflowTestRootExecutor<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage> inputTransform) :
RootExecutor<TInput>("workflow_test_Root", options, inputTransform)
where TInput : notnull
{
protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
}
}
/// <summary>
/// Copies one or more messages into the specified agent conversation.
/// </summary>
internal sealed class CopyMessagesExecutor(FormulaSession session, WorkflowAgentProvider agentProvider) : ActionExecutor(id: "copy_messages", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string? conversationId = await context.ReadStateAsync<string>(key: "ConversationId", scopeName: "System").ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(conversationId))
{
throw new DeclarativeActionException($"Conversation identifier must be defined: {this.Id}");
}
ChatMessage[]? messages = await context.EvaluateValueAsync<ChatMessage[]>("""[UserMessage("Hello, how can I assist you today?")]""").ConfigureAwait(false);
if (messages is not null)
{
foreach (ChatMessage message in messages)
{
await agentProvider.CreateMessageAsync(conversationId, message, cancellationToken).ConfigureAwait(false);
}
}
return default;
}
}
public static Workflow CreateWorkflow<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage>? inputTransform = null)
where TInput : notnull
{
// Create root executor to initialize the workflow.
inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message);
WorkflowTestRootExecutor<TInput> workflowTestRoot = new(options, inputTransform);
DelegateExecutor workflowTest = new(id: "workflow_test", workflowTestRoot.Session);
CopyMessagesExecutor copyMessages = new(workflowTestRoot.Session, options.AgentProvider);
// Define the workflow builder
WorkflowBuilder builder = new(workflowTestRoot);
// Connect executors
builder.AddEdge(workflowTestRoot, workflowTest);
builder.AddEdge(workflowTest, copyMessages);
// Build the workflow
return builder.Build(validateOrphans: false);
}
}

View File

@@ -0,0 +1,12 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_test
actions:
- kind: CopyConversationMessages
id: copy_messages
conversationId: =System.ConversationId
messages: =[UserMessage("Hello, how can I assist you today?")]

View File

@@ -0,0 +1,88 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
#pragma warning disable IDE0005 // Extra using directive is ok.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Test.WorkflowProviders;
/// <summary>
/// This class provides a factory method to create a <see cref="Workflow" /> instance.
/// </summary>
/// <remarks>
/// The workflow defined here was generated from a declarative workflow definition.
/// Declarative workflows utilize Power FX for defining conditions and expressions.
/// To learn more about Power FX, see:
/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio
/// </remarks>
public static class WorkflowProvider
{
/// <summary>
/// The root executor for a declarative workflow.
/// </summary>
internal sealed class WorkflowTestRootExecutor<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage> inputTransform) :
RootExecutor<TInput>("workflow_test_Root", options, inputTransform)
where TInput : notnull
{
protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
// Initialize variables
await context.QueueStateUpdateAsync("PrivateConversationId", UnassignedValue.Instance, "Local").ConfigureAwait(false);
}
}
/// <summary>
/// Creates a new conversation and stores the identifier value to the "Local.PrivateConversationId" variable.
/// </summary>
internal sealed class ConversationCreateExecutor(FormulaSession session, WorkflowAgentProvider agentProvider) : ActionExecutor(id: "conversation_create", session)
{
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string conversationId = await agentProvider.CreateConversationAsync(cancellationToken).ConfigureAwait(false);
await context.QueueStateUpdateAsync(key: "PrivateConversationId", value: conversationId, scopeName: "Local").ConfigureAwait(false);
await context.AddEventAsync(new ConversationUpdateEvent(conversationId)).ConfigureAwait(false);
return default;
}
}
public static Workflow CreateWorkflow<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage>? inputTransform = null)
where TInput : notnull
{
// Create root executor to initialize the workflow.
inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message);
WorkflowTestRootExecutor<TInput> workflowTestRoot = new(options, inputTransform);
DelegateExecutor workflowTest = new(id: "workflow_test", workflowTestRoot.Session);
ConversationCreateExecutor conversationCreate = new(workflowTestRoot.Session, options.AgentProvider);
// Define the workflow builder
WorkflowBuilder builder = new(workflowTestRoot);
// Connect executors
builder.AddEdge(workflowTestRoot, workflowTest);
builder.AddEdge(workflowTest, conversationCreate);
// Build the workflow
return builder.Build(validateOrphans: false);
}
}

View File

@@ -0,0 +1,10 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: workflow_test
actions:
- kind: CreateConversation
id: conversation_create
conversationId: Local.PrivateConversationId

View File

@@ -0,0 +1,87 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
#pragma warning disable IDE0005 // Extra using directive is ok.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Test.WorkflowProviders;
/// <summary>
/// This class provides a factory method to create a <see cref="Workflow" /> instance.
/// </summary>
/// <remarks>
/// The workflow defined here was generated from a declarative workflow definition.
/// Declarative workflows utilize Power FX for defining conditions and expressions.
/// To learn more about Power FX, see:
/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio
/// </remarks>
public static class WorkflowProvider
{
/// <summary>
/// The root executor for a declarative workflow.
/// </summary>
internal sealed class MyWorkflowRootExecutor<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage> inputTransform) :
RootExecutor<TInput>("my_workflow_Root", options, inputTransform)
where TInput : notnull
{
protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
// Initialize variables
await context.QueueStateUpdateAsync("MyTable", UnassignedValue.Instance, "Local").ConfigureAwait(false);
}
}
/// <summary>
/// Assigns an evaluated expression, other variable, or literal value to the "Local.MyTable" variable.
/// </summary>
internal sealed class SetVarExecutor(FormulaSession session) : ActionExecutor(id: "set_var", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
object? evaluatedValue = await context.EvaluateValueAsync<object>("[{id: 3}]").ConfigureAwait(false);
await context.QueueStateUpdateAsync(key: "MyTable", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false);
return default;
}
}
public static Workflow CreateWorkflow<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage>? inputTransform = null)
where TInput : notnull
{
// Create root executor to initialize the workflow.
inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message);
MyWorkflowRootExecutor<TInput> myWorkflowRoot = new(options, inputTransform);
DelegateExecutor myWorkflow = new(id: "my_workflow", myWorkflowRoot.Session);
SetVarExecutor setVar = new(myWorkflowRoot.Session);
// Define the workflow builder
WorkflowBuilder builder = new(myWorkflowRoot);
// Connect executors
builder.AddEdge(myWorkflowRoot, myWorkflow);
builder.AddEdge(myWorkflow, setVar);
// Build the workflow
return builder.Build(validateOrphans: false);
}
}

View File

@@ -0,0 +1,17 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: my_workflow
actions:
- kind: SetVariable
id: set_var
variable: Local.MyTable
value: =[{id: 3}]
- kind: EditTable
id: edit_var
itemsVariable: Local.MyTable
changeType: Add
value: ={id: 7}

View File

@@ -0,0 +1,87 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
#pragma warning disable IDE0005 // Extra using directive is ok.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Test.WorkflowProviders;
/// <summary>
/// This class provides a factory method to create a <see cref="Workflow" /> instance.
/// </summary>
/// <remarks>
/// The workflow defined here was generated from a declarative workflow definition.
/// Declarative workflows utilize Power FX for defining conditions and expressions.
/// To learn more about Power FX, see:
/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio
/// </remarks>
public static class WorkflowProvider
{
/// <summary>
/// The root executor for a declarative workflow.
/// </summary>
internal sealed class MyWorkflowRootExecutor<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage> inputTransform) :
RootExecutor<TInput>("my_workflow_Root", options, inputTransform)
where TInput : notnull
{
protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
// Initialize variables
await context.QueueStateUpdateAsync("MyTable", UnassignedValue.Instance, "Local").ConfigureAwait(false);
}
}
/// <summary>
/// Assigns an evaluated expression, other variable, or literal value to the "Local.MyTable" variable.
/// </summary>
internal sealed class SetVarExecutor(FormulaSession session) : ActionExecutor(id: "set_var", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
object? evaluatedValue = await context.EvaluateValueAsync<object>("[{id: 3}]").ConfigureAwait(false);
await context.QueueStateUpdateAsync(key: "MyTable", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false);
return default;
}
}
public static Workflow CreateWorkflow<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage>? inputTransform = null)
where TInput : notnull
{
// Create root executor to initialize the workflow.
inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message);
MyWorkflowRootExecutor<TInput> myWorkflowRoot = new(options, inputTransform);
DelegateExecutor myWorkflow = new(id: "my_workflow", myWorkflowRoot.Session);
SetVarExecutor setVar = new(myWorkflowRoot.Session);
// Define the workflow builder
WorkflowBuilder builder = new(myWorkflowRoot);
// Connect executors
builder.AddEdge(myWorkflowRoot, myWorkflow);
builder.AddEdge(myWorkflow, setVar);
// Build the workflow
return builder.Build(validateOrphans: false);
}
}

View File

@@ -0,0 +1,18 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: my_workflow
actions:
- kind: SetVariable
id: set_var
variable: Local.MyTable
value: =[{id: 3}]
- kind: EditTableV2
id: edit_var
itemsVariable: Local.MyTable
changeType:
kind: AddItemOperation
value: ={id: 7}

View File

@@ -0,0 +1,94 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
#pragma warning disable IDE0005 // Extra using directive is ok.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Test.WorkflowProviders;
/// <summary>
/// This class provides a factory method to create a <see cref="Workflow" /> instance.
/// </summary>
/// <remarks>
/// The workflow defined here was generated from a declarative workflow definition.
/// Declarative workflows utilize Power FX for defining conditions and expressions.
/// To learn more about Power FX, see:
/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio
/// </remarks>
public static class WorkflowProvider
{
/// <summary>
/// The root executor for a declarative workflow.
/// </summary>
internal sealed class MyWorkflowRootExecutor<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage> inputTransform) :
RootExecutor<TInput>("my_workflow_Root", options, inputTransform)
where TInput : notnull
{
protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
}
}
/// <summary>
/// Formats a message template and sends an activity event.
/// </summary>
internal sealed class SendActivity1Executor(FormulaSession session) : ActionExecutor(id: "send_activity_1", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string activityText =
await context.FormatTemplateAsync(
"""
NEVER 1!
"""
);
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]);
await context.AddEventAsync(new AgentResponseEvent(this.Id, response)).ConfigureAwait(false);
return default;
}
}
public static Workflow CreateWorkflow<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage>? inputTransform = null)
where TInput : notnull
{
// Create root executor to initialize the workflow.
inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message);
MyWorkflowRootExecutor<TInput> myWorkflowRoot = new(options, inputTransform);
DelegateExecutor myWorkflow = new(id: "my_workflow", myWorkflowRoot.Session);
DelegateExecutor endAll = new(id: "end_all", myWorkflowRoot.Session);
DelegateExecutor endAllRestart = new(id: "end_all_Restart", myWorkflowRoot.Session);
SendActivity1Executor sendActivity1 = new(myWorkflowRoot.Session);
// Define the workflow builder
WorkflowBuilder builder = new(myWorkflowRoot);
// Connect executors
builder.AddEdge(myWorkflowRoot, myWorkflow);
builder.AddEdge(myWorkflow, endAll);
builder.AddEdge(endAllRestart, sendActivity1);
// Build the workflow
return builder.Build(validateOrphans: false);
}
}

View File

@@ -0,0 +1,13 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: my_workflow
actions:
- kind: EndConversation
id: end_all
- kind: SendActivity
id: send_activity_1
activity: NEVER 1!

View File

@@ -0,0 +1,94 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
#pragma warning disable IDE0005 // Extra using directive is ok.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Test.WorkflowProviders;
/// <summary>
/// This class provides a factory method to create a <see cref="Workflow" /> instance.
/// </summary>
/// <remarks>
/// The workflow defined here was generated from a declarative workflow definition.
/// Declarative workflows utilize Power FX for defining conditions and expressions.
/// To learn more about Power FX, see:
/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio
/// </remarks>
public static class WorkflowProvider
{
/// <summary>
/// The root executor for a declarative workflow.
/// </summary>
internal sealed class MyWorkflowRootExecutor<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage> inputTransform) :
RootExecutor<TInput>("my_workflow_Root", options, inputTransform)
where TInput : notnull
{
protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
}
}
/// <summary>
/// Formats a message template and sends an activity event.
/// </summary>
internal sealed class SendActivity1Executor(FormulaSession session) : ActionExecutor(id: "send_activity_1", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string activityText =
await context.FormatTemplateAsync(
"""
NEVER 1!
"""
);
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]);
await context.AddEventAsync(new AgentResponseEvent(this.Id, response)).ConfigureAwait(false);
return default;
}
}
public static Workflow CreateWorkflow<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage>? inputTransform = null)
where TInput : notnull
{
// Create root executor to initialize the workflow.
inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message);
MyWorkflowRootExecutor<TInput> myWorkflowRoot = new(options, inputTransform);
DelegateExecutor myWorkflow = new(id: "my_workflow", myWorkflowRoot.Session);
DelegateExecutor endAll = new(id: "end_all", myWorkflowRoot.Session);
DelegateExecutor endAllRestart = new(id: "end_all_Restart", myWorkflowRoot.Session);
SendActivity1Executor sendActivity1 = new(myWorkflowRoot.Session);
// Define the workflow builder
WorkflowBuilder builder = new(myWorkflowRoot);
// Connect executors
builder.AddEdge(myWorkflowRoot, myWorkflow);
builder.AddEdge(myWorkflow, endAll);
builder.AddEdge(endAllRestart, sendActivity1);
// Build the workflow
return builder.Build(validateOrphans: false);
}
}

View File

@@ -0,0 +1,13 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: my_workflow
actions:
- kind: EndWorkflow
id: end_all
- kind: SendActivity
id: send_activity_1
activity: NEVER 1!

View File

@@ -0,0 +1,143 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
#pragma warning disable IDE0005 // Extra using directive is ok.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Test.WorkflowProviders;
/// <summary>
/// This class provides a factory method to create a <see cref="Workflow" /> instance.
/// </summary>
/// <remarks>
/// The workflow defined here was generated from a declarative workflow definition.
/// Declarative workflows utilize Power FX for defining conditions and expressions.
/// To learn more about Power FX, see:
/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio
/// </remarks>
public static class WorkflowProvider
{
/// <summary>
/// The root executor for a declarative workflow.
/// </summary>
internal sealed class MyWorkflowRootExecutor<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage> inputTransform) :
RootExecutor<TInput>("my_workflow_Root", options, inputTransform)
where TInput : notnull
{
protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
}
}
/// <summary>
/// Formats a message template and sends an activity event.
/// </summary>
internal sealed class SendActivity1Executor(FormulaSession session) : ActionExecutor(id: "send_activity_1", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string activityText =
await context.FormatTemplateAsync(
"""
NEVER 1!
"""
);
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]);
await context.AddEventAsync(new AgentResponseEvent(this.Id, response)).ConfigureAwait(false);
return default;
}
}
/// <summary>
/// Formats a message template and sends an activity event.
/// </summary>
internal sealed class SendActivity2Executor(FormulaSession session) : ActionExecutor(id: "send_activity_2", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string activityText =
await context.FormatTemplateAsync(
"""
NEVER 2!
"""
);
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]);
await context.AddEventAsync(new AgentResponseEvent(this.Id, response)).ConfigureAwait(false);
return default;
}
}
/// <summary>
/// Formats a message template and sends an activity event.
/// </summary>
internal sealed class SendActivity3Executor(FormulaSession session) : ActionExecutor(id: "send_activity_3", session)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string activityText =
await context.FormatTemplateAsync(
"""
NEVER 3!
"""
);
AgentResponse response = new([new ChatMessage(ChatRole.Assistant, activityText)]);
await context.AddEventAsync(new AgentResponseEvent(this.Id, response)).ConfigureAwait(false);
return default;
}
}
public static Workflow CreateWorkflow<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage>? inputTransform = null)
where TInput : notnull
{
// Create root executor to initialize the workflow.
inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message);
MyWorkflowRootExecutor<TInput> myWorkflowRoot = new(options, inputTransform);
DelegateExecutor myWorkflow = new(id: "my_workflow", myWorkflowRoot.Session);
DelegateExecutor gotoEnd = new(id: "goto_end", myWorkflowRoot.Session);
DelegateExecutor endAll = new(id: "end_all", myWorkflowRoot.Session);
DelegateExecutor gotoEndRestart = new(id: "goto_end_Restart", myWorkflowRoot.Session);
SendActivity1Executor sendActivity1 = new(myWorkflowRoot.Session);
SendActivity2Executor sendActivity2 = new(myWorkflowRoot.Session);
SendActivity3Executor sendActivity3 = new(myWorkflowRoot.Session);
// Define the workflow builder
WorkflowBuilder builder = new(myWorkflowRoot);
// Connect executors
builder.AddEdge(myWorkflowRoot, myWorkflow);
builder.AddEdge(myWorkflow, gotoEnd);
builder.AddEdge(gotoEnd, endAll);
builder.AddEdge(gotoEndRestart, sendActivity1);
builder.AddEdge(sendActivity1, sendActivity2);
builder.AddEdge(sendActivity2, sendActivity3);
builder.AddEdge(sendActivity3, endAll);
// Build the workflow
return builder.Build(validateOrphans: false);
}
}

View File

@@ -0,0 +1,25 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: my_workflow
actions:
- kind: GotoAction
id: goto_end
actionId: end_all
- kind: SendActivity
id: send_activity_1
activity: NEVER 1!
- kind: SendActivity
id: send_activity_2
activity: NEVER 2!
- kind: SendActivity
id: send_activity_3
activity: NEVER 3!
- kind: EndConversation
id: end_all

View File

@@ -0,0 +1,112 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
#pragma warning disable IDE0005 // Extra using directive is ok.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Extensions.AI;
namespace Test.WorkflowProviders;
/// <summary>
/// This class provides a factory method to create a <see cref="Workflow" /> instance.
/// </summary>
/// <remarks>
/// The workflow defined here was generated from a declarative workflow definition.
/// Declarative workflows utilize Power FX for defining conditions and expressions.
/// To learn more about Power FX, see:
/// https://learn.microsoft.com/power-platform/power-fx/formula-reference-copilot-studio
/// </remarks>
public static class WorkflowProvider
{
/// <summary>
/// The root executor for a declarative workflow.
/// </summary>
internal sealed class MyWorkflowRootExecutor<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage> inputTransform) :
RootExecutor<TInput>("my_workflow_Root", options, inputTransform)
where TInput : notnull
{
protected override async ValueTask ExecuteAsync(TInput message, IWorkflowContext context, CancellationToken cancellationToken)
{
// Set environment variables
await this.InitializeEnvironmentAsync(
context,
"MY_STUDENT").ConfigureAwait(false);
}
}
/// <summary>
/// Invokes an agent to process messages and return a response within a conversation context.
/// </summary>
internal sealed class InvokeAgentExecutor(FormulaSession session, WorkflowAgentProvider agentProvider) : AgentExecutor(id: "invoke_agent", session, agentProvider)
{
// <inheritdoc />
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
{
string? agentName = await context.ReadStateAsync<string>(key: "MY_STUDENT", scopeName: "Env").ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(agentName))
{
throw new DeclarativeActionException($"Agent name must be defined: {this.Id}");
}
string? conversationId = await context.ReadStateAsync<string>(key: "ConversationId", scopeName: "System").ConfigureAwait(false);
bool autoSend = true;
IList<ChatMessage>? inputMessages = await context.EvaluateListAsync<ChatMessage>("[UserMessage(System.LastMessageText)]").ConfigureAwait(false);
AgentResponse agentResponse =
await InvokeAgentAsync(
context,
agentName,
conversationId,
autoSend,
inputMessages,
cancellationToken).ConfigureAwait(false);
if (autoSend)
{
await context.AddEventAsync(new AgentResponseEvent(this.Id, agentResponse)).ConfigureAwait(false);
}
return default;
}
}
public static Workflow CreateWorkflow<TInput>(
DeclarativeWorkflowOptions options,
Func<TInput, ChatMessage>? inputTransform = null)
where TInput : notnull
{
// Create root executor to initialize the workflow.
inputTransform ??= (message) => DeclarativeWorkflowBuilder.DefaultTransform(message);
MyWorkflowRootExecutor<TInput> myWorkflowRoot = new(options, inputTransform);
DelegateExecutor myWorkflow = new(id: "my_workflow", myWorkflowRoot.Session);
InvokeAgentExecutor invokeAgent = new(myWorkflowRoot.Session, options.AgentProvider);
// Define the workflow builder
WorkflowBuilder builder = new(myWorkflowRoot);
// Connect executors
builder.AddEdge(myWorkflowRoot, myWorkflow);
builder.AddEdge(myWorkflow, invokeAgent);
// Build the workflow
return builder.Build(validateOrphans: false);
}
}

View File

@@ -0,0 +1,14 @@
kind: Workflow
trigger:
kind: OnConversationStart
id: my_workflow
actions:
- kind: InvokeAzureAgent
id: invoke_agent
conversationId: =System.ConversationId
agent:
name: =Env.MY_STUDENT
input:
messages: =[UserMessage(System.LastMessageText)]

Some files were not shown because too many files have changed in this diff Show More