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
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:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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[])));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
@@ -0,0 +1,4 @@
|
||||
# empty yaml
|
||||
- id: 1
|
||||
- id: 2
|
||||
- id: 3
|
||||
@@ -0,0 +1,8 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
actions:
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
@@ -0,0 +1,8 @@
|
||||
kind: ToolDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: my_workflow
|
||||
actions:
|
||||
|
||||
- kind: ClearAllVariables
|
||||
id: clear_all
|
||||
variables: ConversationScopedVariables
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
@@ -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!
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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?")]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_test
|
||||
actions:
|
||||
|
||||
- kind: CreateConversation
|
||||
id: conversation_create
|
||||
conversationId: Local.PrivateConversationId
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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!
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user