chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
This commit is contained in:
+51
@@ -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<ResponseAgentProvider> 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<ResponseAgentProvider> 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);
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
|
||||
|
||||
public sealed partial class DeclarativeWorkflowJsonOptionsTests(ITestOutputHelper output)
|
||||
: WorkflowTest(output)
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(typeof(ActionExecutorResult))]
|
||||
[InlineData(typeof(ExternalInputRequest))]
|
||||
[InlineData(typeof(ExternalInputResponse))]
|
||||
[InlineData(typeof(UnassignedValue))]
|
||||
[InlineData(typeof(List<string>))]
|
||||
// Internal approval-snapshot types: catches source-gen regressions from rename/move of the nested types.
|
||||
[InlineData(typeof(InvokeFunctionToolExecutor.ApprovalSnapshot))]
|
||||
[InlineData(typeof(Dictionary<string, InvokeFunctionToolExecutor.ApprovalSnapshot>))]
|
||||
[InlineData(typeof(InvokeMcpToolExecutor.ApprovalSnapshot))]
|
||||
[InlineData(typeof(Dictionary<string, InvokeMcpToolExecutor.ApprovalSnapshot>))]
|
||||
public void DefaultOptions_HasTypeInfoForRegisteredType(Type type)
|
||||
{
|
||||
Assert.True(
|
||||
DeclarativeWorkflowJsonOptions.Default.TryGetTypeInfo(type, out _),
|
||||
$"Default should resolve JsonTypeInfo for {type.FullName}.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_RoundTrip_NullResult()
|
||||
{
|
||||
ActionExecutorResult copy = RoundTrip(new ActionExecutorResult("exec-1"));
|
||||
|
||||
Assert.Equal("exec-1", copy.ExecutorId);
|
||||
Assert.Null(copy.Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExternalInputRequest_RoundTrip_WithApprovalAndFunctionCallContent()
|
||||
{
|
||||
ChatMessage requestMessage = new(
|
||||
ChatRole.Assistant,
|
||||
[
|
||||
new ToolApprovalRequestContent("call1", new FunctionCallContent("call1", "do-something")),
|
||||
new FunctionCallContent("call2", "do-other"),
|
||||
]);
|
||||
ExternalInputRequest copy = RoundTrip(new ExternalInputRequest(new AgentResponse(requestMessage)));
|
||||
|
||||
ChatMessage messageCopy = Assert.Single(copy.AgentResponse.Messages);
|
||||
Assert.Equal(2, messageCopy.Contents.Count);
|
||||
Assert.Contains(messageCopy.Contents, c => c is ToolApprovalRequestContent);
|
||||
Assert.Contains(messageCopy.Contents, c => c is FunctionCallContent);
|
||||
|
||||
// GetInnerRequestContent prefers ToolApprovalRequestContent over FunctionCallContent.
|
||||
AIContent? inner = ((IExternalRequestEnvelope)copy).GetInnerRequestContent();
|
||||
Assert.IsType<ToolApprovalRequestContent>(inner);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExternalInputResponse_RoundTrip()
|
||||
{
|
||||
ExternalInputResponse copy = RoundTrip(new ExternalInputResponse(new ChatMessage(ChatRole.User, "ok")));
|
||||
|
||||
ChatMessage messageCopy = Assert.Single(copy.Messages);
|
||||
Assert.Equal(ChatRole.User, messageCopy.Role);
|
||||
Assert.Equal("ok", messageCopy.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnassignedValue_RoundTrip()
|
||||
{
|
||||
Assert.NotNull(RoundTrip(UnassignedValue.Instance));
|
||||
}
|
||||
|
||||
// Proves declarative types fail under a source-gen-only resolver that lacks the declarative
|
||||
// context. Mirrors AOT runtime behavior in a non-AOT test project.
|
||||
[Fact]
|
||||
public void Serialization_WithoutDeclarativeChain_FailsOnDeclarativeType()
|
||||
{
|
||||
ActionExecutorResult source = new("exec-1");
|
||||
JsonSerializerOptions bareOptions = new() { TypeInfoResolver = EmptyJsonContext.Default };
|
||||
|
||||
Assert.False(
|
||||
bareOptions.TryGetTypeInfo(typeof(ActionExecutorResult), out _),
|
||||
$"Test is meaningless if the bare resolver already covers {nameof(ActionExecutorResult)}.");
|
||||
|
||||
NotSupportedException ex = Assert.Throws<NotSupportedException>(
|
||||
() => JsonSerializer.Serialize(source, bareOptions));
|
||||
|
||||
Assert.Contains(nameof(ActionExecutorResult), ex.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialization_WithDeclarativeChain_SucceedsOnDeclarativeType()
|
||||
{
|
||||
string text = JsonSerializer.Serialize(new ActionExecutorResult("exec-1"), DeclarativeWorkflowJsonOptions.Default);
|
||||
|
||||
Assert.Contains("exec-1", text, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// Mirrors the documented public usage: CheckpointManager.CreateJson(store, DeclarativeWorkflowJsonOptions.Default).
|
||||
[Fact]
|
||||
public void CheckpointManager_CreateJson_WithDefault_SmokeTest()
|
||||
{
|
||||
InMemoryJsonStore store = new();
|
||||
|
||||
CheckpointManager manager = CheckpointManager.CreateJson(store, DeclarativeWorkflowJsonOptions.Default);
|
||||
|
||||
Assert.NotNull(manager);
|
||||
}
|
||||
|
||||
// Empty context for the negative test; `string` is harmless filler required for a valid context.
|
||||
[JsonSerializable(typeof(string))]
|
||||
internal sealed partial class EmptyJsonContext : JsonSerializerContext;
|
||||
|
||||
private sealed class InMemoryJsonStore : JsonCheckpointStore
|
||||
{
|
||||
private readonly Dictionary<CheckpointInfo, JsonElement> _store = [];
|
||||
|
||||
public override ValueTask<CheckpointInfo> CreateCheckpointAsync(
|
||||
string sessionId, JsonElement value, CheckpointInfo? parent = null)
|
||||
{
|
||||
CheckpointInfo key = new(sessionId, Guid.NewGuid().ToString("N"));
|
||||
this._store[key] = value;
|
||||
return new(key);
|
||||
}
|
||||
|
||||
public override ValueTask<JsonElement> RetrieveCheckpointAsync(
|
||||
string sessionId, CheckpointInfo key)
|
||||
=> new(this._store[key]);
|
||||
|
||||
public override ValueTask<IEnumerable<CheckpointInfo>> RetrieveIndexAsync(
|
||||
string sessionId, CheckpointInfo? withParent = null)
|
||||
{
|
||||
List<CheckpointInfo> matches = [];
|
||||
foreach (CheckpointInfo k in this._store.Keys)
|
||||
{
|
||||
if (k.SessionId == sessionId)
|
||||
{
|
||||
matches.Add(k);
|
||||
}
|
||||
}
|
||||
return new(matches);
|
||||
}
|
||||
}
|
||||
|
||||
private static T RoundTrip<T>(T source) where T : notnull
|
||||
{
|
||||
JsonSerializerOptions options = DeclarativeWorkflowJsonOptions.Default;
|
||||
string text = JsonSerializer.Serialize(source, options);
|
||||
T? copy = JsonSerializer.Deserialize<T>(text, options);
|
||||
Assert.NotNull(copy);
|
||||
return copy!;
|
||||
}
|
||||
}
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="DeclarativeWorkflowOptions"/> telemetry configuration.
|
||||
/// </summary>
|
||||
[Collection("DeclarativeWorkflowOptionsTest")]
|
||||
public sealed class DeclarativeWorkflowOptionsTest : IDisposable
|
||||
{
|
||||
// These constants mirror Microsoft.Agents.AI.Workflows.Observability.ActivityNames
|
||||
// which is internal and not accessible from this test project.
|
||||
private const string WorkflowBuildActivityName = "workflow.build";
|
||||
private const string WorkflowRunActivityName = "workflow_invoke";
|
||||
|
||||
// The default activity source name used by the workflow telemetry context.
|
||||
private const string DefaultTelemetrySourceName = "Microsoft.Agents.AI.Workflows";
|
||||
|
||||
private const string SimpleWorkflowYaml = """
|
||||
kind: Workflow
|
||||
trigger:
|
||||
kind: OnConversationStart
|
||||
id: test_workflow
|
||||
actions:
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
""";
|
||||
|
||||
private readonly ActivitySource _activitySource = new("TestSource");
|
||||
private readonly ActivityListener _activityListener;
|
||||
private readonly ConcurrentBag<Activity> _capturedActivities = [];
|
||||
|
||||
public DeclarativeWorkflowOptionsTest()
|
||||
{
|
||||
this._activityListener = new ActivityListener
|
||||
{
|
||||
ShouldListenTo = source =>
|
||||
source.Name == DefaultTelemetrySourceName ||
|
||||
source.Name == "TestSource",
|
||||
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData,
|
||||
ActivityStarted = activity => this._capturedActivities.Add(activity),
|
||||
};
|
||||
ActivitySource.AddActivityListener(this._activityListener);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._activityListener.Dispose();
|
||||
this._activitySource.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureTelemetry_DefaultIsNull()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ResponseAgentProvider> mockProvider = CreateMockProvider();
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Null(options.ConfigureTelemetry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureTelemetry_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ResponseAgentProvider> mockProvider = CreateMockProvider();
|
||||
bool callbackInvoked = false;
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
ConfigureTelemetry = opt =>
|
||||
{
|
||||
callbackInvoked = true;
|
||||
opt.EnableSensitiveData = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(options.ConfigureTelemetry);
|
||||
WorkflowTelemetryOptions telemetryOptions = new();
|
||||
options.ConfigureTelemetry(telemetryOptions);
|
||||
Assert.True(callbackInvoked);
|
||||
Assert.True(telemetryOptions.EnableSensitiveData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TelemetryActivitySource_DefaultIsNull()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ResponseAgentProvider> mockProvider = CreateMockProvider();
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Null(options.TelemetryActivitySource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TelemetryActivitySource_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
Mock<ResponseAgentProvider> mockProvider = CreateMockProvider();
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
TelemetryActivitySource = this._activitySource
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Same(this._activitySource, options.TelemetryActivitySource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithDefaultTelemetry_AppliesTelemetryAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("DefaultTelemetryTest").Start()!;
|
||||
Mock<ResponseAgentProvider> mockProvider = CreateMockProvider();
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
ConfigureTelemetry = _ => { },
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId && a.Source.Name == DefaultTelemetrySourceName)
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(capturedActivities);
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowBuildActivityName, StringComparison.Ordinal));
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowRunActivityName, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithTelemetryActivitySource_AppliesTelemetryAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("TelemetryActivitySourceTest").Start()!;
|
||||
Mock<ResponseAgentProvider> mockProvider = CreateMockProvider();
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
TelemetryActivitySource = this._activitySource,
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId && a.Source.Name == "TestSource")
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(capturedActivities);
|
||||
Assert.All(capturedActivities, a => Assert.Equal("TestSource", a.Source.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithConfigureTelemetry_AppliesConfigurationAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("ConfigureTelemetryTest").Start()!;
|
||||
Mock<ResponseAgentProvider> mockProvider = CreateMockProvider();
|
||||
bool configureInvoked = false;
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
ConfigureTelemetry = opt =>
|
||||
{
|
||||
configureInvoked = true;
|
||||
opt.EnableSensitiveData = true;
|
||||
},
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert
|
||||
Assert.True(configureInvoked);
|
||||
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId && a.Source.Name == DefaultTelemetrySourceName)
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(capturedActivities);
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowBuildActivityName, StringComparison.Ordinal));
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowRunActivityName, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithoutTelemetry_DoesNotCreateActivitiesAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("NoTelemetryTest").Start()!;
|
||||
Mock<ResponseAgentProvider> mockProvider = CreateMockProvider();
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert - No workflow activities should be created when telemetry is disabled
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId &&
|
||||
(a.OperationName.StartsWith(WorkflowBuildActivityName, StringComparison.Ordinal) ||
|
||||
a.OperationName.StartsWith(WorkflowRunActivityName, StringComparison.Ordinal)))
|
||||
.ToArray();
|
||||
|
||||
Assert.Empty(capturedActivities);
|
||||
}
|
||||
|
||||
private static Mock<ResponseAgentProvider> CreateMockProvider()
|
||||
{
|
||||
Mock<ResponseAgentProvider> 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, "Test response")));
|
||||
return mockAgentProvider;
|
||||
}
|
||||
}
|
||||
+408
@@ -0,0 +1,408 @@
|
||||
// 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.Agents.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Moq;
|
||||
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", isDiscrete: false);
|
||||
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", isDiscrete: false);
|
||||
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", isAction: false);
|
||||
this.AssertExecuted("sendActivity_even");
|
||||
this.AssertNotExecuted("conditionItem_odd");
|
||||
this.AssertNotExecuted("sendActivity_odd");
|
||||
this.AssertMessage("EVEN");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AssertExecuted("conditionItem_odd", isAction: false);
|
||||
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", isAction: false);
|
||||
this.AssertNotExecuted("conditionItem_odd");
|
||||
this.AssertNotExecuted("sendActivity_odd");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AssertExecuted("conditionItem_odd", isAction: false);
|
||||
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", isAction: false);
|
||||
if (input % 2 == 0)
|
||||
{
|
||||
this.AssertNotExecuted("conditionItem_odd");
|
||||
this.AssertNotExecuted("sendActivity_odd");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AssertExecuted("conditionItem_odd", isAction: false);
|
||||
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")]
|
||||
[InlineData("HttpRequest.yaml", 1, "http_request")]
|
||||
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(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<ResponseAgentProvider> 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")]
|
||||
[InlineData("HttpRequest.yaml", "http_request")]
|
||||
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.RunStreamingAsync(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 isAction = true, bool isDiscrete = true)
|
||||
{
|
||||
Assert.Contains(this.WorkflowEvents.OfType<ExecutorInvokedEvent>(), e => e.ExecutorId == executorId);
|
||||
Assert.Contains(this.WorkflowEvents.OfType<ExecutorCompletedEvent>(), e => e.ExecutorId == executorId);
|
||||
if (isAction)
|
||||
{
|
||||
Assert.Contains(this.WorkflowEvents.OfType<DeclarativeActionInvokedEvent>(), e => e.ActionId == executorId);
|
||||
if (isDiscrete)
|
||||
{
|
||||
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.RunStreamingAsync(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<ResponseAgentProvider> mockAgentProvider = CreateMockProvider($"{workflowInput}");
|
||||
DeclarativeWorkflowOptions workflowContext =
|
||||
new(mockAgentProvider.Object)
|
||||
{
|
||||
LoggerFactory = this.Output,
|
||||
HttpRequestHandler = CreateMockHttpRequestHandler().Object,
|
||||
};
|
||||
return DeclarativeWorkflowBuilder.Build<TInput>(yamlReader, workflowContext);
|
||||
}
|
||||
|
||||
private static Mock<ResponseAgentProvider> CreateMockProvider(string input)
|
||||
{
|
||||
Mock<ResponseAgentProvider> 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;
|
||||
}
|
||||
|
||||
private static Mock<IHttpRequestHandler> CreateMockHttpRequestHandler()
|
||||
{
|
||||
Mock<IHttpRequestHandler> mockHandler = new(MockBehavior.Loose);
|
||||
mockHandler
|
||||
.Setup(handler => handler.SendAsync(It.IsAny<HttpRequestInfo>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(() => Task.FromResult(new HttpRequestResult
|
||||
{
|
||||
StatusCode = 200,
|
||||
IsSuccessStatusCode = true,
|
||||
Body = "{\"ok\":true}",
|
||||
}));
|
||||
return mockHandler;
|
||||
}
|
||||
}
|
||||
+510
@@ -0,0 +1,510 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="DefaultHttpRequestHandler"/>.
|
||||
/// </summary>
|
||||
public sealed class DefaultHttpRequestHandlerTests
|
||||
{
|
||||
private static readonly string[] s_setCookieValues = ["a=1", "b=2"];
|
||||
|
||||
private const string TestUrl = "https://api.example.test/resource";
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ConstructorWithNoParametersCreatesInstanceAsync()
|
||||
{
|
||||
// Act
|
||||
await using DefaultHttpRequestHandler handler = new();
|
||||
|
||||
// Assert
|
||||
handler.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConstructorWithNullProviderCreatesInstanceAsync()
|
||||
{
|
||||
// Act
|
||||
await using DefaultHttpRequestHandler handler = new(httpClientProvider: null);
|
||||
|
||||
// Assert
|
||||
handler.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorWithNullHttpClientThrows()
|
||||
{
|
||||
// Act
|
||||
Action act = () => _ = new DefaultHttpRequestHandler((HttpClient)null!);
|
||||
|
||||
// Assert
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConstructorWithHttpClientUsesSuppliedClientForAllRequestsAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestHttpMessageHandler messageHandler = new((req, _) =>
|
||||
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("ok", Encoding.UTF8, "text/plain"),
|
||||
}));
|
||||
using HttpClient suppliedClient = new(messageHandler);
|
||||
await using DefaultHttpRequestHandler handler = new(suppliedClient);
|
||||
HttpRequestInfo request = new() { Method = "GET", Url = TestUrl };
|
||||
|
||||
// Act
|
||||
HttpRequestResult result = await handler.SendAsync(request);
|
||||
|
||||
// Assert - the supplied HttpClient's underlying handler saw the request
|
||||
messageHandler.LastRequest.Should().NotBeNull();
|
||||
messageHandler.LastRequest!.RequestUri!.ToString().Should().Be(TestUrl);
|
||||
result.Body.Should().Be("ok");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsyncDoesNotDisposeCallerSuppliedHttpClientAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestHttpMessageHandler messageHandler = new((req, _) =>
|
||||
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
|
||||
using HttpClient suppliedClient = new(messageHandler);
|
||||
|
||||
// Act
|
||||
DefaultHttpRequestHandler handler = new(suppliedClient);
|
||||
await handler.DisposeAsync();
|
||||
|
||||
// Assert - supplied client remains usable (not disposed)
|
||||
Func<Task> act = async () => await suppliedClient.GetAsync(new Uri(TestUrl));
|
||||
await act.Should().NotThrowAsync<ObjectDisposedException>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Argument Validation Tests
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsyncWithNullRequestThrowsAsync()
|
||||
{
|
||||
// Arrange
|
||||
await using DefaultHttpRequestHandler handler = new();
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await handler.SendAsync(null!);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsyncWithEmptyUrlThrowsAsync()
|
||||
{
|
||||
// Arrange
|
||||
await using DefaultHttpRequestHandler handler = new();
|
||||
HttpRequestInfo request = new() { Method = "GET", Url = "" };
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await handler.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsyncWithEmptyMethodThrowsAsync()
|
||||
{
|
||||
// Arrange
|
||||
await using DefaultHttpRequestHandler handler = new();
|
||||
HttpRequestInfo request = new() { Method = "", Url = TestUrl };
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await handler.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<ArgumentException>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Send Behavior Tests
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsyncUsesProvidedHttpClientAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestHttpMessageHandler messageHandler = new((req, _) =>
|
||||
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("hello", Encoding.UTF8, "text/plain"),
|
||||
}));
|
||||
|
||||
await using DefaultHttpRequestHandler handler = new((_, _) => Task.FromResult<HttpClient?>(new HttpClient(messageHandler)));
|
||||
|
||||
HttpRequestInfo request = new() { Method = "GET", Url = TestUrl };
|
||||
|
||||
// Act
|
||||
HttpRequestResult result = await handler.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
messageHandler.LastRequest.Should().NotBeNull();
|
||||
messageHandler.LastRequest!.Method.Should().Be(HttpMethod.Get);
|
||||
messageHandler.LastRequest.RequestUri!.ToString().Should().Be(TestUrl);
|
||||
result.StatusCode.Should().Be(200);
|
||||
result.IsSuccessStatusCode.Should().BeTrue();
|
||||
result.Body.Should().Be("hello");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsyncMapsAllKnownMethodsAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestHttpMessageHandler messageHandler = new((req, _) =>
|
||||
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
|
||||
|
||||
await using DefaultHttpRequestHandler handler = new((_, _) => Task.FromResult<HttpClient?>(new HttpClient(messageHandler)));
|
||||
|
||||
foreach (string method in new[] { "GET", "POST", "PUT", "PATCH", "DELETE", "CUSTOM" })
|
||||
{
|
||||
HttpRequestInfo request = new() { Method = method, Url = TestUrl };
|
||||
|
||||
// Act
|
||||
await handler.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
messageHandler.LastRequest!.Method.Method.Should().Be(method);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsyncNormalizesWhitespaceAroundCustomMethodAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestHttpMessageHandler messageHandler = new((req, _) =>
|
||||
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
|
||||
await using DefaultHttpRequestHandler handler = new((_, _) => Task.FromResult<HttpClient?>(new HttpClient(messageHandler)));
|
||||
HttpRequestInfo request = new() { Method = " custom ", Url = TestUrl };
|
||||
|
||||
// Act
|
||||
await handler.SendAsync(request);
|
||||
|
||||
// Assert - fallback path should apply the same Trim/ToUpperInvariant normalization.
|
||||
messageHandler.LastRequest!.Method.Method.Should().Be("CUSTOM");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsyncAppliesBodyAndContentTypeAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestHttpMessageHandler messageHandler = new((req, _) =>
|
||||
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
|
||||
|
||||
await using DefaultHttpRequestHandler handler = new((_, _) => Task.FromResult<HttpClient?>(new HttpClient(messageHandler)));
|
||||
|
||||
HttpRequestInfo request = new()
|
||||
{
|
||||
Method = "POST",
|
||||
Url = TestUrl,
|
||||
Body = "{\"hello\":\"world\"}",
|
||||
BodyContentType = "application/json",
|
||||
};
|
||||
|
||||
// Act
|
||||
await handler.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
messageHandler.LastRequestBody.Should().Be("{\"hello\":\"world\"}");
|
||||
messageHandler.LastRequestContentType.Should().Be("application/json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsyncAppliesRequestHeadersAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestHttpMessageHandler messageHandler = new((req, _) =>
|
||||
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
|
||||
|
||||
await using DefaultHttpRequestHandler handler = new((_, _) => Task.FromResult<HttpClient?>(new HttpClient(messageHandler)));
|
||||
|
||||
HttpRequestInfo request = new()
|
||||
{
|
||||
Method = "GET",
|
||||
Url = TestUrl,
|
||||
Headers = new Dictionary<string, string>
|
||||
{
|
||||
["Authorization"] = "Bearer secret",
|
||||
["Accept"] = "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
await handler.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
messageHandler.LastRequest!.Headers.Authorization!.ToString().Should().Be("Bearer secret");
|
||||
messageHandler.LastRequest.Headers.Accept.Should().Contain(mediaType => mediaType.MediaType == "application/json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsyncRoutesContentHeadersToBodyAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestHttpMessageHandler messageHandler = new((req, _) =>
|
||||
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
|
||||
|
||||
await using DefaultHttpRequestHandler handler = new((_, _) => Task.FromResult<HttpClient?>(new HttpClient(messageHandler)));
|
||||
|
||||
HttpRequestInfo request = new()
|
||||
{
|
||||
Method = "POST",
|
||||
Url = TestUrl,
|
||||
Body = "raw",
|
||||
BodyContentType = "text/plain",
|
||||
Headers = new Dictionary<string, string>
|
||||
{
|
||||
["Content-Language"] = "en-US",
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
await handler.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
messageHandler.LastRequest!.Content!.Headers.ContentLanguage.Should().Contain("en-US");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsyncCapturesResponseHeadersAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestHttpMessageHandler messageHandler = new((req, _) =>
|
||||
{
|
||||
#pragma warning disable CA2025
|
||||
HttpResponseMessage response = new(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("ok", Encoding.UTF8, "text/plain"),
|
||||
};
|
||||
response.Headers.Add("X-Request-Id", "request-1");
|
||||
response.Headers.Add("Set-Cookie", s_setCookieValues);
|
||||
return Task.FromResult(response);
|
||||
#pragma warning restore CA2025
|
||||
});
|
||||
|
||||
await using DefaultHttpRequestHandler handler = new((_, _) => Task.FromResult<HttpClient?>(new HttpClient(messageHandler)));
|
||||
|
||||
HttpRequestInfo request = new() { Method = "GET", Url = TestUrl };
|
||||
|
||||
// Act
|
||||
HttpRequestResult result = await handler.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
result.Headers.Should().NotBeNull();
|
||||
result.Headers!.Should().ContainKey("X-Request-Id");
|
||||
result.Headers!["Set-Cookie"].Should().BeEquivalentTo(s_setCookieValues);
|
||||
// Content headers also flattened in.
|
||||
result.Headers!.Should().ContainKey("Content-Type");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsyncReturnsFailureStatusWithoutThrowingAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestHttpMessageHandler messageHandler = new((req, _) =>
|
||||
Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||
{
|
||||
Content = new StringContent("bad request", Encoding.UTF8, "text/plain"),
|
||||
}));
|
||||
|
||||
await using DefaultHttpRequestHandler handler = new((_, _) => Task.FromResult<HttpClient?>(new HttpClient(messageHandler)));
|
||||
|
||||
HttpRequestInfo request = new() { Method = "GET", Url = TestUrl };
|
||||
|
||||
// Act
|
||||
HttpRequestResult result = await handler.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
result.IsSuccessStatusCode.Should().BeFalse();
|
||||
result.StatusCode.Should().Be(400);
|
||||
result.Body.Should().Be("bad request");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsyncTimeoutCancelsRequestAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestHttpMessageHandler messageHandler = new(async (req, ct) =>
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false);
|
||||
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||
});
|
||||
|
||||
await using DefaultHttpRequestHandler handler = new((_, _) => Task.FromResult<HttpClient?>(new HttpClient(messageHandler)));
|
||||
|
||||
HttpRequestInfo request = new()
|
||||
{
|
||||
Method = "GET",
|
||||
Url = TestUrl,
|
||||
Timeout = TimeSpan.FromMilliseconds(50),
|
||||
};
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await handler.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<OperationCanceledException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsyncFallsBackToOwnedClientWhenProviderReturnsNullAsync()
|
||||
{
|
||||
// Arrange
|
||||
int providerCallCount = 0;
|
||||
await using DefaultHttpRequestHandler handler = new((_, _) =>
|
||||
{
|
||||
providerCallCount++;
|
||||
return Task.FromResult<HttpClient?>(null);
|
||||
});
|
||||
|
||||
HttpRequestInfo request = new() { Method = "GET", Url = "http://127.0.0.1:1/" };
|
||||
|
||||
// Act - owned client will attempt real network and fail, but provider path should have been consulted first.
|
||||
Func<Task> act = async () => await handler.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
await act.Should().ThrowAsync<Exception>();
|
||||
providerCallCount.Should().Be(1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DisposeAsync
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsyncCompletesAsync()
|
||||
{
|
||||
// Arrange
|
||||
DefaultHttpRequestHandler handler = new();
|
||||
|
||||
// Act
|
||||
Func<Task> act = async () => await handler.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
await act.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsyncCalledMultipleTimesSucceedsAsync()
|
||||
{
|
||||
// Arrange
|
||||
DefaultHttpRequestHandler handler = new();
|
||||
|
||||
// Act
|
||||
await handler.DisposeAsync();
|
||||
Func<Task> second = async () => await handler.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
await second.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Query Parameters and Connection Tests
|
||||
|
||||
[Fact]
|
||||
public async Task QueryParametersAreAppendedToUrlAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestHttpMessageHandler fake = new(static (req, _) =>
|
||||
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) }));
|
||||
await using DefaultHttpRequestHandler handler = new((_, _) => Task.FromResult<HttpClient?>(new HttpClient(fake)));
|
||||
|
||||
HttpRequestInfo info = new()
|
||||
{
|
||||
Method = "GET",
|
||||
Url = TestUrl,
|
||||
QueryParameters = new Dictionary<string, string>
|
||||
{
|
||||
["filter"] = "active items",
|
||||
["ids"] = "1,2,3",
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
await handler.SendAsync(info);
|
||||
|
||||
// Assert
|
||||
fake.LastRequest.Should().NotBeNull();
|
||||
string? query = fake.LastRequest!.RequestUri!.Query;
|
||||
query.Should().Contain("filter=active%20items");
|
||||
query.Should().Contain("ids=1%2C2%2C3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryParametersPreserveExistingQueryStringAsync()
|
||||
{
|
||||
// Arrange
|
||||
TestHttpMessageHandler fake = new(static (req, _) =>
|
||||
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(string.Empty) }));
|
||||
await using DefaultHttpRequestHandler handler = new((_, _) => Task.FromResult<HttpClient?>(new HttpClient(fake)));
|
||||
|
||||
HttpRequestInfo info = new()
|
||||
{
|
||||
Method = "GET",
|
||||
Url = TestUrl + "?existing=yes",
|
||||
QueryParameters = new Dictionary<string, string>
|
||||
{
|
||||
["added"] = "true",
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
await handler.SendAsync(info);
|
||||
|
||||
// Assert
|
||||
fake.LastRequest!.RequestUri!.Query.Should().Be("?existing=yes&added=true");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private sealed class TestHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _responseFactory;
|
||||
|
||||
public TestHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> responseFactory)
|
||||
{
|
||||
this._responseFactory = responseFactory;
|
||||
}
|
||||
|
||||
public HttpRequestMessage? LastRequest { get; private set; }
|
||||
|
||||
public string? LastRequestBody { get; private set; }
|
||||
|
||||
public string? LastRequestContentType { get; private set; }
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
this.LastRequest = request;
|
||||
if (request.Content is not null)
|
||||
{
|
||||
#if NET
|
||||
this.LastRequestBody = await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
this.LastRequestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
this.LastRequestContentType = request.Content.Headers.ContentType?.MediaType;
|
||||
}
|
||||
return await this._responseFactory(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Entities;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+758
@@ -0,0 +1,758 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Entities;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
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,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
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 ToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")),
|
||||
new ToolApprovalRequestContent("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);
|
||||
|
||||
List<ToolApprovalRequestContent> approvalRequests = messageCopy.Contents.OfType<ToolApprovalRequestContent>().ToList();
|
||||
Assert.Equal(2, approvalRequests.Count);
|
||||
|
||||
ToolApprovalRequestContent mcpRequest = approvalRequests[0];
|
||||
Assert.Equal("call1", mcpRequest.RequestId);
|
||||
|
||||
ToolApprovalRequestContent functionRequest = approvalRequests[1];
|
||||
Assert.Equal("call2", functionRequest.RequestId);
|
||||
|
||||
FunctionCallContent functionCall = AssertContent<FunctionCallContent>(messageCopy);
|
||||
Assert.Equal("call3", functionCall.CallId);
|
||||
|
||||
TextContent textContent = AssertContent<TextContent>(messageCopy);
|
||||
Assert.Equal("Heya", textContent.Text);
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
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 ToolApprovalRequestContent("call1", new McpServerToolCallContent("call1", "testmcp", "server-name")).CreateResponse(approved: true),
|
||||
new ToolApprovalRequestContent("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);
|
||||
|
||||
List<ToolApprovalResponseContent> approvalResponses = responseMessage.Contents.OfType<ToolApprovalResponseContent>().ToList();
|
||||
Assert.Equal(2, approvalResponses.Count);
|
||||
|
||||
ToolApprovalResponseContent mcpApproval = approvalResponses[0];
|
||||
Assert.Equal("call1", mcpApproval.RequestId);
|
||||
|
||||
ToolApprovalResponseContent functionApproval = approvalResponses[1];
|
||||
Assert.Equal("call2", functionApproval.RequestId);
|
||||
|
||||
FunctionResultContent functionResult = AssertContent<FunctionResultContent>(responseMessage);
|
||||
Assert.Equal("call3", functionResult.CallId);
|
||||
|
||||
TextContent textContent = AssertContent<TextContent>(responseMessage);
|
||||
Assert.Equal("Heya", textContent.Text);
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="AgentProviderExtensions.InvokeAgentAsync"/>.
|
||||
/// </summary>
|
||||
public sealed class AgentProviderExtensionsTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
private const string WorkflowConversationId = "workflow-conv-id";
|
||||
private const string AgentName = "test-agent";
|
||||
|
||||
[Fact]
|
||||
public Task AutoSendFalseOnWorkflowConversationSuppressesResponseEventsAsync() =>
|
||||
this.RunAsync(autoSend: false, conversationId: WorkflowConversationId, expectResponseEvents: false);
|
||||
|
||||
[Fact]
|
||||
public Task AutoSendTrueOnWorkflowConversationEmitsResponseEventsAsync() =>
|
||||
this.RunAsync(autoSend: true, conversationId: WorkflowConversationId, expectResponseEvents: true);
|
||||
|
||||
[Fact]
|
||||
public Task AutoSendFalseOnExternalConversationSuppressesResponseEventsAsync() =>
|
||||
this.RunAsync(autoSend: false, conversationId: "other-conv-id", expectResponseEvents: false);
|
||||
|
||||
[Fact]
|
||||
public Task AutoSendTrueOnExternalConversationEmitsResponseEventsAndCopiesMessagesAsync() =>
|
||||
this.RunAsync(
|
||||
autoSend: true,
|
||||
conversationId: "other-conv-id",
|
||||
expectResponseEvents: true,
|
||||
expectCrossConversationCopy: true);
|
||||
|
||||
private async Task RunAsync(
|
||||
bool autoSend,
|
||||
string conversationId,
|
||||
bool expectResponseEvents,
|
||||
bool expectCrossConversationCopy = false)
|
||||
{
|
||||
// Arrange: seed the workflow conversation id so IsWorkflowConversation can recognize it.
|
||||
this.State.Set(
|
||||
SystemScope.Names.ConversationId,
|
||||
FormulaValue.New(WorkflowConversationId),
|
||||
VariableScopeNames.System);
|
||||
|
||||
MockAgentProvider mockProvider = new();
|
||||
AgentResponseUpdate[] updates =
|
||||
[
|
||||
new(ChatRole.Assistant, "hello "),
|
||||
new(ChatRole.Assistant, "world"),
|
||||
];
|
||||
mockProvider
|
||||
.Setup(p => p.InvokeAgentAsync(
|
||||
AgentName,
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<string?>(),
|
||||
It.IsAny<IEnumerable<ChatMessage>?>(),
|
||||
It.IsAny<IDictionary<string, object?>?>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns(ToAsyncEnumerableAsync(updates));
|
||||
|
||||
List<(string ConversationId, ChatMessage Message)> copiedMessages = [];
|
||||
mockProvider
|
||||
.Setup(p => p.CreateMessageAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<ChatMessage>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Returns<string, ChatMessage, CancellationToken>(
|
||||
(convId, msg, _) =>
|
||||
{
|
||||
copiedMessages.Add((convId, msg));
|
||||
return Task.FromResult(msg);
|
||||
});
|
||||
|
||||
string actionId = this.CreateActionId().Value;
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events =
|
||||
await this.ExecuteAsync(
|
||||
actionId,
|
||||
async (IWorkflowContext context, ActionExecutorResult _, CancellationToken cancellationToken) =>
|
||||
{
|
||||
await mockProvider.Object.InvokeAgentAsync(
|
||||
actionId,
|
||||
context,
|
||||
AgentName,
|
||||
conversationId,
|
||||
autoSend,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
// Assert
|
||||
int updateEventCount = events.OfType<AgentResponseUpdateEvent>().Count();
|
||||
int responseEventCount = events.OfType<AgentResponseEvent>().Count();
|
||||
|
||||
if (expectResponseEvents)
|
||||
{
|
||||
Assert.Equal(updates.Length, updateEventCount);
|
||||
Assert.Equal(1, responseEventCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Equal(0, updateEventCount);
|
||||
Assert.Equal(0, responseEventCount);
|
||||
}
|
||||
|
||||
if (expectCrossConversationCopy)
|
||||
{
|
||||
Assert.NotEmpty(copiedMessages);
|
||||
Assert.All(copiedMessages, c => Assert.Equal(WorkflowConversationId, c.ConversationId));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Empty(copiedMessages);
|
||||
}
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<AgentResponseUpdate> ToAsyncEnumerableAsync(IEnumerable<AgentResponseUpdate> updates)
|
||||
{
|
||||
foreach (AgentResponseUpdate update in updates)
|
||||
{
|
||||
yield return update;
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
+933
@@ -0,0 +1,933 @@
|
||||
// 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.Agents.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.MessageContent.RecordType,
|
||||
FormulaValue.NewRecordFromFields(
|
||||
new NamedValue(TypeSchema.MessageContent.Fields.Type, TypeSchema.MessageContent.ContentTypes.Text.ToFormula()),
|
||||
new NamedValue(TypeSchema.MessageContent.Fields.Value, 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.MessageContent.Fields.Type, StringDataValue.Create(TypeSchema.MessageContent.ContentTypes.Text)),
|
||||
new KeyValuePair<string, DataValue>(TypeSchema.MessageContent.Fields.Value, 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());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTripChatMessageAsRecord()
|
||||
{
|
||||
// Arrange
|
||||
ChatMessage message =
|
||||
new(ChatRole.User,
|
||||
[
|
||||
new TextContent("Test message"),
|
||||
new UriContent("https://example.com/image.jpg", "image/jpeg"),
|
||||
new HostedFileContent("file_123abc"),
|
||||
new DataContent(new byte[] { 1, 2, 3, 4, 5 }, "application/pdf"),
|
||||
])
|
||||
{
|
||||
MessageId = "msg-001"
|
||||
};
|
||||
|
||||
// Act
|
||||
RecordValue result = message.ToRecord();
|
||||
DataValue resultValue = result.ToDataValue();
|
||||
ChatMessage? messageCopy = resultValue.ToChatMessage();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(messageCopy);
|
||||
Assert.Equal(message.Role, messageCopy.Role);
|
||||
Assert.Equal(message.MessageId, messageCopy.MessageId);
|
||||
Assert.Equal(message.Contents.Count, messageCopy.Contents.Count);
|
||||
foreach (AIContent contentCopy in messageCopy.Contents)
|
||||
{
|
||||
AIContent sourceContent = Assert.Single(message.Contents, c => c.GetType() == contentCopy.GetType());
|
||||
AssertAIContentEquivalent(sourceContent, contentCopy);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTripChatMessageAsTable()
|
||||
{
|
||||
// Arrange
|
||||
ChatMessage message =
|
||||
new(ChatRole.User,
|
||||
[
|
||||
new TextContent("Test message"),
|
||||
new UriContent("https://example.com/image.jpg", "image/jpeg"),
|
||||
new HostedFileContent("file_123abc"),
|
||||
new DataContent(new byte[] { 1, 2, 3, 4, 5 }, "application/pdf"),
|
||||
])
|
||||
{
|
||||
MessageId = "msg-001"
|
||||
};
|
||||
|
||||
IEnumerable<ChatMessage> messages = [message];
|
||||
|
||||
// Act
|
||||
TableValue result = messages.ToTable();
|
||||
TableDataValue resultValue = result.ToTable();
|
||||
ChatMessage[] messagesCopy = resultValue.ToChatMessages().ToArray();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(messagesCopy);
|
||||
ChatMessage messageCopy = Assert.Single(messagesCopy);
|
||||
Assert.Equal(message.Role, messageCopy.Role);
|
||||
Assert.Equal(message.MessageId, messageCopy.MessageId);
|
||||
Assert.Equal(message.Contents.Count, messageCopy.Contents.Count);
|
||||
foreach (AIContent contentCopy in messageCopy.Contents)
|
||||
{
|
||||
AIContent sourceContent = Assert.Single(message.Contents, c => c.GetType() == contentCopy.GetType());
|
||||
AssertAIContentEquivalent(sourceContent, contentCopy);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two AIContent instances for equivalence without using Assert.Equivalent,
|
||||
/// which fails on .NET Framework 4.7.2 due to ReadOnlySpan.GetHashCode() not being supported.
|
||||
/// </summary>
|
||||
private static void AssertAIContentEquivalent(AIContent expected, AIContent actual)
|
||||
{
|
||||
Assert.Equal(expected.GetType(), actual.GetType());
|
||||
|
||||
switch (expected)
|
||||
{
|
||||
case TextContent expectedText:
|
||||
TextContent actualText = Assert.IsType<TextContent>(actual);
|
||||
Assert.Equal(expectedText.Text, actualText.Text);
|
||||
break;
|
||||
case UriContent expectedUri:
|
||||
UriContent actualUri = Assert.IsType<UriContent>(actual);
|
||||
Assert.Equal(expectedUri.Uri, actualUri.Uri);
|
||||
Assert.Equal(expectedUri.MediaType, actualUri.MediaType);
|
||||
break;
|
||||
case HostedFileContent expectedFile:
|
||||
HostedFileContent actualFile = Assert.IsType<HostedFileContent>(actual);
|
||||
Assert.Equal(expectedFile.FileId, actualFile.FileId);
|
||||
break;
|
||||
case DataContent expectedData:
|
||||
DataContent actualData = Assert.IsType<DataContent>(actual);
|
||||
Assert.Equal(expectedData.MediaType, actualData.MediaType);
|
||||
Assert.Equal(expectedData.Data.ToArray(), actualData.Data.ToArray());
|
||||
break;
|
||||
default:
|
||||
Assert.Fail($"Unexpected AIContent type: {expected.GetType().Name}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeForLastMessageReturnsInputWhenInputMessageIsNull()
|
||||
{
|
||||
// Arrange
|
||||
ChatMessage input = new(ChatRole.User, "hello") { MessageId = "local" };
|
||||
|
||||
// Act
|
||||
ChatMessage result = input.MergeForLastMessage(null);
|
||||
|
||||
// Assert
|
||||
Assert.Same(input, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeForLastMessageReturnsSameInstanceAsRoundTripped()
|
||||
{
|
||||
// Arrange: returning the round-tripped instance keeps the merge forward-compatible
|
||||
// with future ChatMessage properties (e.g., new metadata fields) without explicit copies.
|
||||
ChatMessage input = new(ChatRole.User, "original");
|
||||
ChatMessage roundTripped = new(ChatRole.User, "stripped") { MessageId = "server" };
|
||||
|
||||
// Act
|
||||
ChatMessage result = input.MergeForLastMessage(roundTripped);
|
||||
|
||||
// Assert
|
||||
Assert.Same(roundTripped, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeForLastMessagePrefersOriginalTextOverRoundTrippedText()
|
||||
{
|
||||
// Arrange
|
||||
ChatMessage input = new(ChatRole.User, "original text");
|
||||
ChatMessage roundTripped = new(ChatRole.User, "stripped") { MessageId = "server-id" };
|
||||
|
||||
// Act
|
||||
ChatMessage result = input.MergeForLastMessage(roundTripped);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("server-id", result.MessageId);
|
||||
Assert.Equal("original text", result.Text);
|
||||
TextContent text = Assert.IsType<TextContent>(Assert.Single(result.Contents));
|
||||
Assert.Equal("original text", text.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeForLastMessageReplacesTextInPlaceAndKeepsServerMedia()
|
||||
{
|
||||
// Arrange
|
||||
HostedFileContent serverRef = new("file-abc");
|
||||
ChatMessage input = new(ChatRole.User, [new TextContent("look at this:"), new DataContent("data:image/jpeg;base64,QUJD", "image/jpeg")]);
|
||||
ChatMessage roundTripped = new(ChatRole.User, [new TextContent("stripped"), serverRef]) { MessageId = "server-id" };
|
||||
|
||||
// Act
|
||||
ChatMessage result = input.MergeForLastMessage(roundTripped);
|
||||
|
||||
// Assert: server's text slot is replaced with original text; server's media reference is preserved.
|
||||
Assert.Equal("server-id", result.MessageId);
|
||||
Assert.Collection(result.Contents,
|
||||
c => Assert.Equal("look at this:", Assert.IsType<TextContent>(c).Text),
|
||||
c => Assert.Same(serverRef, c));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeForLastMessageAppendsOriginalTextWhenRoundTripHasNoTextSlot()
|
||||
{
|
||||
// Arrange: round-tripped message has only media (no text slot to replace).
|
||||
HostedFileContent serverRef = new("file-1");
|
||||
ChatMessage input = new(ChatRole.User, [new TextContent("middle"), new DataContent("data:image/jpeg;base64,QUE=", "image/jpeg")]);
|
||||
ChatMessage roundTripped = new(ChatRole.User, [serverRef]) { MessageId = "id" };
|
||||
|
||||
// Act
|
||||
ChatMessage result = input.MergeForLastMessage(roundTripped);
|
||||
|
||||
// Assert: media kept; original text appended at end.
|
||||
Assert.Collection(result.Contents,
|
||||
c => Assert.Same(serverRef, c),
|
||||
c => Assert.Equal("middle", Assert.IsType<TextContent>(c).Text));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeForLastMessageReplacesMultipleTextSlotsInOrder()
|
||||
{
|
||||
// Arrange: input has two text items; round-tripped has two text slots interleaved with media.
|
||||
HostedFileContent firstRef = new("file-1");
|
||||
HostedFileContent secondRef = new("file-2");
|
||||
ChatMessage input = new(ChatRole.User, [new TextContent("first"), new TextContent("second")]);
|
||||
ChatMessage roundTripped = new(ChatRole.User, [firstRef, new TextContent("a"), secondRef, new TextContent("b")]) { MessageId = "id" };
|
||||
|
||||
// Act
|
||||
ChatMessage result = input.MergeForLastMessage(roundTripped);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(result.Contents,
|
||||
c => Assert.Same(firstRef, c),
|
||||
c => Assert.Equal("first", Assert.IsType<TextContent>(c).Text),
|
||||
c => Assert.Same(secondRef, c),
|
||||
c => Assert.Equal("second", Assert.IsType<TextContent>(c).Text));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeForLastMessageFallsBackToInputTextWhenInputHasNoTextContent()
|
||||
{
|
||||
// Arrange: ChatMessage(role, "string") populates Text but no explicit TextContent
|
||||
// when Contents is initially empty in some construction paths. Verify we still
|
||||
// recover the original Text via input.Text.
|
||||
ChatMessage input = new(ChatRole.User, "fallback text");
|
||||
ChatMessage roundTripped = new(ChatRole.User, [new TextContent("stripped")]) { MessageId = "id" };
|
||||
|
||||
// Act
|
||||
ChatMessage result = input.MergeForLastMessage(roundTripped);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("fallback text", Assert.IsType<TextContent>(Assert.Single(result.Contents)).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeForLastMessagePreservesServerAuthoredProperties()
|
||||
{
|
||||
// Arrange: server (round-trip) is authoritative for metadata. Returning the
|
||||
// round-tripped instance means any future ChatMessage property is automatically
|
||||
// preserved without code changes here.
|
||||
ChatMessage input = new(ChatRole.User, "hi")
|
||||
{
|
||||
AuthorName = "client-side",
|
||||
AdditionalProperties = new AdditionalPropertiesDictionary { ["client"] = "value" },
|
||||
};
|
||||
ChatMessage roundTripped = new(ChatRole.User, [new TextContent("stripped")])
|
||||
{
|
||||
MessageId = "server",
|
||||
AuthorName = "server-side",
|
||||
AdditionalProperties = new AdditionalPropertiesDictionary { ["server"] = "value" },
|
||||
};
|
||||
|
||||
// Act
|
||||
ChatMessage result = input.MergeForLastMessage(roundTripped);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("server", result.MessageId);
|
||||
Assert.Equal("server-side", result.AuthorName);
|
||||
Assert.NotNull(result.AdditionalProperties);
|
||||
Assert.True(result.AdditionalProperties.ContainsKey("server"));
|
||||
Assert.False(result.AdditionalProperties.ContainsKey("client"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeForLastMessageHandlesEmptyInputContents()
|
||||
{
|
||||
// Arrange
|
||||
ChatMessage input = new(ChatRole.User, new List<AIContent>());
|
||||
HostedFileContent serverRef = new("file-only");
|
||||
ChatMessage roundTripped = new(ChatRole.User, [serverRef]) { MessageId = "id" };
|
||||
|
||||
// Act
|
||||
ChatMessage result = input.MergeForLastMessage(roundTripped);
|
||||
|
||||
// Assert: nothing to splice; round-tripped returned unchanged.
|
||||
Assert.Same(roundTripped, result);
|
||||
Assert.Equal("file-only", Assert.IsType<HostedFileContent>(Assert.Single(result.Contents)).FileId);
|
||||
}
|
||||
}
|
||||
+845
@@ -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.Agents.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);
|
||||
}
|
||||
}
|
||||
+69
@@ -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<ResponseAgentProvider> providerMock = new(MockBehavior.Strict);
|
||||
return
|
||||
new(providerMock.Object)
|
||||
{
|
||||
MaximumExpressionLength = maximumExpressionLength,
|
||||
MaximumCallDepth = maximumCallDepth
|
||||
};
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.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);
|
||||
}
|
||||
}
|
||||
+217
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+215
@@ -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.Agents.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"));
|
||||
}
|
||||
}
|
||||
+764
@@ -0,0 +1,764 @@
|
||||
// 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[])));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test for #4195: When a JSON object contains an array of objects
|
||||
/// and is parsed with <c>VariableType.RecordType</c> (no schema), the nested
|
||||
/// object properties must be preserved. Before the fix, DetermineElementType()
|
||||
/// created an empty-schema VariableType, causing ParseRecord to take the
|
||||
/// ParseSchema path (zero fields) and return empty dictionaries.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseRecord_ObjectWithArrayOfObjects_NoSchema_PreservesNestedProperties()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
{
|
||||
"items": [
|
||||
{ "name": "Alice", "role": "Engineer" },
|
||||
{ "name": "Bob", "role": "Designer" },
|
||||
{ "name": "Carol", "role": "PM" }
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
// Act
|
||||
Dictionary<string, object?> result = document.ParseRecord(VariableType.RecordType);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.ContainsKey("items"));
|
||||
List<object?> items = Assert.IsType<List<object?>>(result["items"]);
|
||||
Assert.Equal(3, items.Count);
|
||||
|
||||
Dictionary<string, object?> first = Assert.IsType<Dictionary<string, object?>>(items[0]);
|
||||
Assert.Equal("Alice", first["name"]);
|
||||
Assert.Equal("Engineer", first["role"]);
|
||||
|
||||
Dictionary<string, object?> second = Assert.IsType<Dictionary<string, object?>>(items[1]);
|
||||
Assert.Equal("Bob", second["name"]);
|
||||
Assert.Equal("Designer", second["role"]);
|
||||
|
||||
Dictionary<string, object?> third = Assert.IsType<Dictionary<string, object?>>(items[2]);
|
||||
Assert.Equal("Carol", third["name"]);
|
||||
Assert.Equal("PM", third["role"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test for #4195: When a JSON array of objects is parsed directly
|
||||
/// via <c>ParseList</c> with <c>VariableType.ListType</c> (no schema), all
|
||||
/// object properties must be preserved in each element.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ParseList_ArrayOfObjects_NoSchema_PreservesProperties()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
[
|
||||
{ "name": "Alice", "role": "Engineer" },
|
||||
{ "name": "Bob", "role": "Designer" }
|
||||
]
|
||||
""");
|
||||
|
||||
// Act
|
||||
List<object?> result = document.ParseList(VariableType.ListType);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
|
||||
Dictionary<string, object?> first = Assert.IsType<Dictionary<string, object?>>(result[0]);
|
||||
Assert.Equal("Alice", first["name"]);
|
||||
Assert.Equal("Engineer", first["role"]);
|
||||
|
||||
Dictionary<string, object?> second = Assert.IsType<Dictionary<string, object?>>(result[1]);
|
||||
Assert.Equal("Bob", second["name"]);
|
||||
Assert.Equal("Designer", second["role"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListTypeFromJson_EmptyArray_ReturnsFallbackListType()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse("[]");
|
||||
|
||||
// Act
|
||||
VariableType result = document.RootElement.GetListTypeFromJson();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VariableType.ListType, result.Type);
|
||||
Assert.False(result.HasSchema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListTypeFromJson_ArrayOfPrimitives_ReturnsFallbackListType()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse("[1, 2, 3]");
|
||||
|
||||
// Act
|
||||
VariableType result = document.RootElement.GetListTypeFromJson();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(VariableType.ListType, result.Type);
|
||||
Assert.False(result.HasSchema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListTypeFromJson_ObjectWithStringField_InfersStringType()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
[{ "name": "hello" }]
|
||||
""");
|
||||
|
||||
// Act
|
||||
VariableType result = document.RootElement.GetListTypeFromJson();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasSchema);
|
||||
Assert.True(result.Schema!.ContainsKey("name"));
|
||||
Assert.Equal(typeof(string), result.Schema["name"].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListTypeFromJson_ObjectWithNumberField_InfersDecimalType()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
[{ "value": 42 }]
|
||||
""");
|
||||
|
||||
// Act
|
||||
VariableType result = document.RootElement.GetListTypeFromJson();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasSchema);
|
||||
Assert.True(result.Schema!.ContainsKey("value"));
|
||||
Assert.Equal(typeof(decimal), result.Schema["value"].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListTypeFromJson_ObjectWithBooleanTrueField_InfersBoolType()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
[{ "flag": true }]
|
||||
""");
|
||||
|
||||
// Act
|
||||
VariableType result = document.RootElement.GetListTypeFromJson();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasSchema);
|
||||
Assert.True(result.Schema!.ContainsKey("flag"));
|
||||
Assert.Equal(typeof(bool), result.Schema["flag"].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListTypeFromJson_ObjectWithBooleanFalseField_InfersBoolType()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
[{ "flag": false }]
|
||||
""");
|
||||
|
||||
// Act
|
||||
VariableType result = document.RootElement.GetListTypeFromJson();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasSchema);
|
||||
Assert.True(result.Schema!.ContainsKey("flag"));
|
||||
Assert.Equal(typeof(bool), result.Schema["flag"].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListTypeFromJson_ObjectWithNestedObjectField_InfersRecordType()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
[{ "child": { "inner": 1 } }]
|
||||
""");
|
||||
|
||||
// Act
|
||||
VariableType result = document.RootElement.GetListTypeFromJson();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasSchema);
|
||||
Assert.True(result.Schema!.ContainsKey("child"));
|
||||
Assert.Equal(VariableType.RecordType, result.Schema["child"].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListTypeFromJson_ObjectWithNestedArrayField_InfersListType()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
[{ "items": [1, 2, 3] }]
|
||||
""");
|
||||
|
||||
// Act
|
||||
VariableType result = document.RootElement.GetListTypeFromJson();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasSchema);
|
||||
Assert.True(result.Schema!.ContainsKey("items"));
|
||||
Assert.Equal(VariableType.ListType, result.Schema["items"].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListTypeFromJson_ObjectWithNullField_InfersStringTypeDefault()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
[{ "missing": null }]
|
||||
""");
|
||||
|
||||
// Act
|
||||
VariableType result = document.RootElement.GetListTypeFromJson();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasSchema);
|
||||
Assert.True(result.Schema!.ContainsKey("missing"));
|
||||
Assert.Equal(typeof(string), result.Schema["missing"].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListTypeFromJson_SkipsNonObjectElements_InfersFromFirstObject()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
[1, "text", { "id": 99 }]
|
||||
""");
|
||||
|
||||
// Act
|
||||
VariableType result = document.RootElement.GetListTypeFromJson();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasSchema);
|
||||
Assert.True(result.Schema!.ContainsKey("id"));
|
||||
Assert.Equal(typeof(decimal), result.Schema["id"].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetListTypeFromJson_ObjectWithAllFieldTypes_InfersCorrectTypes()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse(
|
||||
"""
|
||||
[{
|
||||
"text": "hello",
|
||||
"count": 5,
|
||||
"enabled": true,
|
||||
"disabled": false,
|
||||
"nested": { "x": 1 },
|
||||
"list": [1, 2],
|
||||
"empty": null
|
||||
}]
|
||||
""");
|
||||
|
||||
// Act
|
||||
VariableType result = document.RootElement.GetListTypeFromJson();
|
||||
|
||||
// Assert
|
||||
Assert.True(result.HasSchema);
|
||||
Assert.Equal(7, result.Schema!.Count);
|
||||
Assert.Equal(typeof(string), result.Schema["text"].Type);
|
||||
Assert.Equal(typeof(decimal), result.Schema["count"].Type);
|
||||
Assert.Equal(typeof(bool), result.Schema["enabled"].Type);
|
||||
Assert.Equal(typeof(bool), result.Schema["disabled"].Type);
|
||||
Assert.Equal(VariableType.RecordType, result.Schema["nested"].Type);
|
||||
Assert.Equal(VariableType.ListType, result.Schema["list"].Type);
|
||||
Assert.Equal(typeof(string), result.Schema["empty"].Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseJsonValue_Object_ReturnsRecord()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse("""{ "a": "alpha", "b": "beta" }""");
|
||||
|
||||
// Act
|
||||
object? result = document.ParseJsonValue("""{ "a": "alpha", "b": "beta" }""");
|
||||
|
||||
// Assert
|
||||
Dictionary<string, object?> record = Assert.IsType<Dictionary<string, object?>>(result);
|
||||
Assert.Equal("alpha", record["a"]);
|
||||
Assert.Equal("beta", record["b"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseJsonValue_Array_ReturnsList()
|
||||
{
|
||||
// Arrange
|
||||
const string Json = """["alpha","beta"]""";
|
||||
JsonDocument document = JsonDocument.Parse(Json);
|
||||
|
||||
// Act
|
||||
object? result = document.ParseJsonValue(Json);
|
||||
|
||||
// Assert
|
||||
List<object?> list = Assert.IsType<List<object?>>(result);
|
||||
Assert.Equal(new object?[] { "alpha", "beta" }, list);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseJsonValue_EmptyArray_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse("[]");
|
||||
|
||||
// Act
|
||||
object? result = document.ParseJsonValue("[]");
|
||||
|
||||
// Assert
|
||||
List<object?> list = Assert.IsType<List<object?>>(result);
|
||||
Assert.Empty(list);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseJsonValue_String_ReturnsString()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse("\"hello\"");
|
||||
|
||||
// Act
|
||||
object? result = document.ParseJsonValue("\"hello\"");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("hello", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseJsonValue_Number_ReturnsNumericValue()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse("42");
|
||||
|
||||
// Act
|
||||
object? result = document.ParseJsonValue("42");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(42d, Assert.IsType<double>(result));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("true", true)]
|
||||
[InlineData("false", false)]
|
||||
public void ParseJsonValue_Boolean_ReturnsBoolean(string json, bool expected)
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse(json);
|
||||
|
||||
// Act
|
||||
object? result = document.ParseJsonValue(json);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseJsonValue_Null_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
JsonDocument document = JsonDocument.Parse("null");
|
||||
|
||||
// Act
|
||||
object? result = document.ParseJsonValue("null");
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
}
|
||||
+124
@@ -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));
|
||||
}
|
||||
}
|
||||
+127
@@ -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;
|
||||
}
|
||||
}
|
||||
+162
@@ -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);
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.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);
|
||||
}
|
||||
}
|
||||
+65
@@ -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; }
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Kit;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that edge predicates correctly handle PortableValue-wrapped messages,
|
||||
/// which occur after checkpoint restore (JSON round-trip).
|
||||
/// </summary>
|
||||
public sealed class PortableValuePredicateTests
|
||||
{
|
||||
#region ActionExecutorResult.ThrowIfNot
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithDirectActionExecutorResult_ReturnsResult()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test-executor");
|
||||
|
||||
// Act
|
||||
ActionExecutorResult actual = ActionExecutorResult.ThrowIfNot(result);
|
||||
|
||||
// Assert
|
||||
actual.Should().BeSameAs(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithPortableValueWrappedActionExecutorResult_Unwraps()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test-executor");
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act
|
||||
ActionExecutorResult actual = ActionExecutorResult.ThrowIfNot(wrapped);
|
||||
|
||||
// Assert
|
||||
actual.ExecutorId.Should().Be("test-executor");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithNonActionExecutorResult_Throws()
|
||||
{
|
||||
// Arrange
|
||||
object message = "not an ActionExecutorResult";
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => ActionExecutorResult.ThrowIfNot(message));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithNull_Throws()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => ActionExecutorResult.ThrowIfNot(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActionExecutorResult_ThrowIfNot_WithPortableValueWrappedNonResult_Throws()
|
||||
{
|
||||
// Arrange
|
||||
PortableValue wrapped = new("not an ActionExecutorResult");
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<DeclarativeActionException>(() => ActionExecutorResult.ThrowIfNot(wrapped));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InvokeAzureAgentExecutor Predicates
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresInput_WithDirectExternalInputRequest_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest request = new("test prompt");
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresInput(request).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresInput_WithPortableValueWrappedRequest_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest request = new("test prompt");
|
||||
PortableValue wrapped = new(request);
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresInput(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresInput_WithActionExecutorResult_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test");
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresInput(result).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresNothing_WithDirectActionExecutorResult_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test");
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresNothing(result).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresNothing_WithPortableValueWrappedResult_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test");
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresNothing(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeAzureAgentExecutor_RequiresNothing_WithExternalInputRequest_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest request = new("test prompt");
|
||||
|
||||
// Act & Assert
|
||||
InvokeAzureAgentExecutor.RequiresNothing(request).Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InvokeMcpToolExecutor Predicates
|
||||
|
||||
[Fact]
|
||||
public void InvokeMcpToolExecutor_RequiresInput_WithPortableValueWrappedRequest_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ExternalInputRequest request = new("test prompt");
|
||||
PortableValue wrapped = new(request);
|
||||
|
||||
// Act & Assert
|
||||
InvokeMcpToolExecutor.RequiresInput(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvokeMcpToolExecutor_RequiresNothing_WithPortableValueWrappedResult_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
ActionExecutorResult result = new("test");
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act & Assert
|
||||
InvokeMcpToolExecutor.RequiresNothing(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region QuestionExecutor.IsComplete
|
||||
|
||||
[Fact]
|
||||
public void QuestionExecutor_IsComplete_WithPortableValueWrappedResult_NullResult_ReturnsTrue()
|
||||
{
|
||||
// Arrange - result with null Result property means "complete"
|
||||
ActionExecutorResult result = new("test", result: null);
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act & Assert
|
||||
QuestionExecutor.IsComplete(wrapped).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuestionExecutor_IsComplete_WithPortableValueWrappedResult_NonNullResult_ReturnsFalse()
|
||||
{
|
||||
// Arrange - result with non-null Result property means "not complete"
|
||||
ActionExecutorResult result = new("test", result: true);
|
||||
PortableValue wrapped = new(result);
|
||||
|
||||
// Act & Assert
|
||||
QuestionExecutor.IsComplete(wrapped).Should().BeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+166
@@ -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)));
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedIntegrationTestCode>true</InjectSharedIntegrationTestCode>
|
||||
</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.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,89 @@
|
||||
// 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="ResponseAgentProvider"/> for unit testing purposes.
|
||||
/// </summary>
|
||||
internal sealed class MockAgentProvider : Mock<ResponseAgentProvider>
|
||||
{
|
||||
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<string, ChatMessage, CancellationToken>((conversationId, message, cancellationToken) => Task.FromResult(this.CaptureChatMessage(message)));
|
||||
}
|
||||
|
||||
private string CreateConversationId()
|
||||
{
|
||||
string newConversationId = Guid.NewGuid().ToString("N");
|
||||
this.ExistingConversationIds.Add(newConversationId);
|
||||
|
||||
return newConversationId;
|
||||
}
|
||||
|
||||
private ChatMessage CaptureChatMessage(ChatMessage message)
|
||||
{
|
||||
this.TestMessages.Add(message);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(AgentMessageRole.User)]
|
||||
[InlineData(AgentMessageRole.Agent)]
|
||||
public async Task AddMessageToWorkflowAsync(AgentMessageRole role)
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set(SystemScope.Names.ConversationId, FormulaValue.New("WorkflowConversationId"), VariableScopeNames.System);
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(AddMessageToWorkflowAsync),
|
||||
variableName: "TestMessage",
|
||||
role: AgentMessageRoleWrapper.Get(role),
|
||||
conversationId: "WorkflowConversationId",
|
||||
messageText: $"Hello from {role}");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(AgentMessageRole.User)]
|
||||
[InlineData(AgentMessageRole.Agent)]
|
||||
public async Task AddMessageWithMetadataAsync(AgentMessageRole role)
|
||||
{
|
||||
// Arrange
|
||||
Dictionary<string, string> metadataValues =
|
||||
new()
|
||||
{
|
||||
["Key1"] = "Value1",
|
||||
["Key2"] = "Value2",
|
||||
};
|
||||
RecordDataValue metadataRecord = metadataValues.ToRecordValue();
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(AddMessageWithMetadataAsync),
|
||||
variableName: "TestMessage",
|
||||
role: AgentMessageRoleWrapper.Get(role),
|
||||
messageText: $"Hello from {role}",
|
||||
metadata: metadataRecord);
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync(
|
||||
string displayName,
|
||||
string variableName,
|
||||
AgentMessageRoleWrapper role,
|
||||
string messageText,
|
||||
string? conversationId = null,
|
||||
RecordDataValue? metadata = null)
|
||||
{
|
||||
// Arrange
|
||||
MockAgentProvider mockAgentProvider = new();
|
||||
AddConversationMessage model =
|
||||
this.CreateModel(
|
||||
this.FormatDisplayName(displayName),
|
||||
FormatVariablePath(variableName),
|
||||
conversationId ?? "TestConversationId",
|
||||
role,
|
||||
messageText,
|
||||
metadata);
|
||||
|
||||
AddConversationMessageExecutor action = new(model, mockAgentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
ChatMessage? testMessage = mockAgentProvider.TestMessages?.LastOrDefault();
|
||||
Assert.NotNull(testMessage);
|
||||
VerifyModel(model, action);
|
||||
this.VerifyState(variableName, testMessage.ToRecord());
|
||||
if (metadata is not null)
|
||||
{
|
||||
Assert.NotNull(testMessage.AdditionalProperties);
|
||||
Assert.NotEmpty(testMessage.AdditionalProperties);
|
||||
}
|
||||
}
|
||||
|
||||
private AddConversationMessage CreateModel(
|
||||
string displayName,
|
||||
string messageVariable,
|
||||
string conversationId,
|
||||
AgentMessageRoleWrapper role,
|
||||
string messageText,
|
||||
RecordDataValue? metadata)
|
||||
{
|
||||
ObjectExpression<RecordDataValue>.Builder? metadataExpression = null;
|
||||
if (metadata is not null)
|
||||
{
|
||||
metadataExpression = ObjectExpression<RecordDataValue>.Literal(metadata).ToBuilder();
|
||||
}
|
||||
|
||||
AddConversationMessage.Builder actionBuilder =
|
||||
new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
Message = PropertyPath.Create(messageVariable),
|
||||
ConversationId = StringExpression.Literal(conversationId),
|
||||
Role = role,
|
||||
Metadata = metadataExpression,
|
||||
};
|
||||
|
||||
actionBuilder.Content.Add(new AddConversationMessageContent.Builder
|
||||
{
|
||||
Type = AgentMessageContentType.Text,
|
||||
Value = TemplateLine.Parse(messageText)
|
||||
});
|
||||
|
||||
return AssignParent<AddConversationMessage>(actionBuilder);
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
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 ClearGlobalScopeAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set("GlobalVar", FormulaValue.New("Old value"), VariableScopeNames.Global);
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
this.FormatDisplayName(nameof(ClearGlobalScopeAsync)),
|
||||
VariablesToClear.AllGlobalVariables,
|
||||
"GlobalVar",
|
||||
VariableScopeNames.Global);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearWorkflowScopeAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set("LocalVar", FormulaValue.New("Old value"));
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
this.FormatDisplayName(nameof(ClearWorkflowScopeAsync)),
|
||||
VariablesToClear.ConversationScopedVariables,
|
||||
"LocalVar");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearUserScopeAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set("LocalVar", FormulaValue.New("Old value"));
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
this.FormatDisplayName(nameof(ClearUserScopeAsync)),
|
||||
VariablesToClear.UserScopedVariables,
|
||||
"LocalVar",
|
||||
expectedValue: FormulaValue.New("Old value"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearWorkflowHistoryAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set("LocalVar", FormulaValue.New("Old value"));
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
this.FormatDisplayName(nameof(ClearWorkflowHistoryAsync)),
|
||||
VariablesToClear.ConversationHistory,
|
||||
"LocalVar",
|
||||
expectedValue: FormulaValue.New("Old value"));
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync(
|
||||
string displayName,
|
||||
VariablesToClear scope,
|
||||
string variableName,
|
||||
string variableScope = VariableScopeNames.Local,
|
||||
FormulaValue? expectedValue = null)
|
||||
{
|
||||
// Arrange
|
||||
ClearAllVariables model = this.CreateModel(
|
||||
this.FormatDisplayName(displayName),
|
||||
scope);
|
||||
|
||||
ClearAllVariablesExecutor action = new(model, this.State);
|
||||
|
||||
this.State.Bind();
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
this.VerifyUndefined("NoVar");
|
||||
if (expectedValue is null)
|
||||
{
|
||||
this.VerifyUndefined(variableName, variableScope);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.VerifyState(variableName, variableScope, expectedValue);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ConditionGroupExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class ConditionGroupExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void ConditionGroupThrowsWhenModelInvalid() =>
|
||||
// Arrange, Act & Assert
|
||||
Assert.Throws<DeclarativeModelException>(() => new ConditionGroupExecutor(new ConditionGroup(), this.State));
|
||||
|
||||
[Fact]
|
||||
public void ConditionGroupDefaultNaming()
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupDefaultNaming), [false], includeElse: true, defineActionIds: false);
|
||||
ConditionItem condition = model.Conditions[0];
|
||||
|
||||
// Act
|
||||
string conditionStepId = ConditionGroupExecutor.Steps.Item(model, condition);
|
||||
string elseStepId = ConditionGroupExecutor.Steps.Else(model);
|
||||
|
||||
// Assert
|
||||
Assert.Equal($"{model.Id}_Items0", conditionStepId);
|
||||
Assert.Equal(model.ElseActions.Id.Value, elseStepId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionGroupExplicitNaming()
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupExplicitNaming), [false], includeElse: true);
|
||||
ConditionItem condition = model.Conditions[0];
|
||||
|
||||
// Act
|
||||
string conditionStepId = ConditionGroupExecutor.Steps.Item(model, condition);
|
||||
string elseStepId = ConditionGroupExecutor.Steps.Else(model);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(condition.Id, conditionStepId);
|
||||
Assert.Equal(model.ElseActions.Id.Value, elseStepId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConditionGroupFirstConditionTrueAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ConditionGroupFirstConditionTrueAsync),
|
||||
conditions: [true, false]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConditionGroupSecondConditionTrueAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ConditionGroupSecondConditionTrueAsync),
|
||||
conditions: [false, true]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConditionGroupFirstConditionNullAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ConditionGroupFirstConditionNullAsync),
|
||||
conditions: [null, true]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConditionGroupElseBranchAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ConditionGroupElseBranchAsync),
|
||||
conditions: [false, false],
|
||||
includeElse: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConditionGroupDoneAsync()
|
||||
{
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupDoneAsync), [true]);
|
||||
ConditionGroupExecutor action = new(model, this.State);
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events = await this.ExecuteAsync("condition_done_id", action.DoneAsync);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
|
||||
Assert.NotEmpty(events);
|
||||
VerifyCompletionEvent(events);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionGroupIsMatchTrue()
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupIsMatchTrue), [true]);
|
||||
ConditionItem firstCondition = model.Conditions[0];
|
||||
ConditionGroupExecutor executor = new(model, this.State);
|
||||
ActionExecutorResult result = new(executor.Id, ConditionGroupExecutor.Steps.Item(model, firstCondition));
|
||||
|
||||
// Act
|
||||
bool isMatch = executor.IsMatch(firstCondition, result);
|
||||
|
||||
// Assert
|
||||
Assert.True(isMatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionGroupIsMatchFalse()
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupIsMatchFalse), [true, false]);
|
||||
ConditionItem firstCondition = model.Conditions[0];
|
||||
ConditionItem secondCondition = model.Conditions[1];
|
||||
ConditionGroupExecutor executor = new(model, this.State);
|
||||
ActionExecutorResult result = new(executor.Id, ConditionGroupExecutor.Steps.Item(model, secondCondition));
|
||||
|
||||
// Act
|
||||
bool isMatch = executor.IsMatch(firstCondition, result);
|
||||
|
||||
// Assert
|
||||
Assert.False(isMatch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionGroupIsElseTrue()
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupIsElseTrue), [false]);
|
||||
ConditionGroupExecutor executor = new(model, this.State);
|
||||
ActionExecutorResult result = new(executor.Id, ConditionGroupExecutor.Steps.Else(model));
|
||||
|
||||
// Act
|
||||
bool isElse = executor.IsElse(result);
|
||||
|
||||
// Assert
|
||||
Assert.True(isElse);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConditionGroupIsElseFalse()
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(nameof(ConditionGroupIsElseFalse), [false]);
|
||||
ConditionGroupExecutor executor = new(model, this.State);
|
||||
ActionExecutorResult result = new(executor.Id, "different_step");
|
||||
|
||||
// Act
|
||||
bool isElse = executor.IsElse(result);
|
||||
|
||||
// Assert
|
||||
Assert.False(isElse);
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync(
|
||||
string displayName,
|
||||
bool?[] conditions,
|
||||
bool includeElse = false)
|
||||
{
|
||||
// Arrange
|
||||
ConditionGroup model = this.CreateModel(displayName, conditions, includeElse);
|
||||
ConditionGroupExecutor action = new(model, this.State);
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events = await this.ExecuteAsync(action, isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
|
||||
Assert.NotEmpty(events);
|
||||
VerifyInvocationEvent(events);
|
||||
|
||||
VerifyIsDiscrete(action, isDiscrete: false);
|
||||
}
|
||||
|
||||
private ConditionGroup CreateModel(
|
||||
string displayName,
|
||||
bool?[] conditions,
|
||||
bool includeElse = false,
|
||||
bool defineActionIds = true)
|
||||
{
|
||||
ConditionGroup.Builder actionBuilder = new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
};
|
||||
|
||||
for (int index = 0; index < conditions.Length; ++index)
|
||||
{
|
||||
bool? condition = conditions[index];
|
||||
|
||||
ConditionItem.Builder conditionBuilder = new()
|
||||
{
|
||||
Id = defineActionIds ? $"condition_{index}" : null,
|
||||
Actions = this.CreateActions(defineActionIds ? $"condition_actions_{index}" : null),
|
||||
Condition = condition is null ? null : BoolExpression.Literal(condition.Value).ToBuilder(),
|
||||
};
|
||||
|
||||
actionBuilder.Conditions.Add(conditionBuilder);
|
||||
}
|
||||
|
||||
if (includeElse)
|
||||
{
|
||||
actionBuilder.ElseActions = this.CreateActions(defineActionIds ? "else_actions" : null);
|
||||
}
|
||||
|
||||
return AssignParent<ConditionGroup>(actionBuilder);
|
||||
}
|
||||
|
||||
private ActionScope.Builder CreateActions(string? actionScopeId)
|
||||
{
|
||||
ActionScope.Builder actions = [];
|
||||
|
||||
if (actionScopeId is not null)
|
||||
{
|
||||
actions.Id = new ActionId(actionScopeId);
|
||||
}
|
||||
|
||||
actions.Actions.Add(
|
||||
new SendActivity.Builder
|
||||
{
|
||||
Id = $"{actionScopeId ?? "action"}_send_activity",
|
||||
Activity = new MessageActivityTemplate(),
|
||||
});
|
||||
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="CopyConversationMessagesExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class CopyConversationMessagesExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public async Task CopyMessagesWithSingleStringMessageAsync()
|
||||
{
|
||||
// Arrange, Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(CopyMessagesWithSingleStringMessageAsync),
|
||||
conversationId: "TestConversationId",
|
||||
messages: ValueExpression.Literal(StringDataValue.Create("Hello, how can I help you?")),
|
||||
expectedMessageCount: 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyMessagesWithSingleRecordMessageAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatMessage testMessage = new(ChatRole.User, "Test message content");
|
||||
DataValue messageDataValue = testMessage.ToRecord().ToDataValue();
|
||||
Assert.IsType<RecordDataValue>(messageDataValue);
|
||||
RecordDataValue messageRecord = (RecordDataValue)messageDataValue;
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(CopyMessagesWithSingleRecordMessageAsync),
|
||||
conversationId: "TestConversationId",
|
||||
messages: ValueExpression.Literal(messageRecord),
|
||||
expectedMessageCount: 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyMessagesWithMultipleMessagesAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> testMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "First message"),
|
||||
new ChatMessage(ChatRole.Assistant, "Second message"),
|
||||
new ChatMessage(ChatRole.User, "Third message")
|
||||
];
|
||||
DataValue messagesDataValue = testMessages.ToTable().ToDataValue();
|
||||
Assert.IsType<TableDataValue>(messagesDataValue);
|
||||
TableDataValue messagesTable = (TableDataValue)messagesDataValue;
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(CopyMessagesWithMultipleMessagesAsync),
|
||||
conversationId: "TestConversationId",
|
||||
messages: ValueExpression.Literal(messagesTable),
|
||||
expectedMessageCount: 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyMessagesWithVariableExpressionAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage> testMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Message from variable")
|
||||
];
|
||||
TableValue messagesTable = testMessages.ToTable();
|
||||
this.State.Set("SourceMessages", messagesTable);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(CopyMessagesWithVariableExpressionAsync),
|
||||
conversationId: "TestConversationId",
|
||||
messages: ValueExpression.Variable(PropertyPath.TopicVariable("SourceMessages")),
|
||||
expectedMessageCount: 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyMessagesToWorkflowConversationAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set(SystemScope.Names.ConversationId, FormulaValue.New("WorkflowConversationId"), VariableScopeNames.System);
|
||||
|
||||
List<ChatMessage> testMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Message to workflow conversation")
|
||||
];
|
||||
DataValue messagesDataValue = testMessages.ToTable().ToDataValue();
|
||||
Assert.IsType<TableDataValue>(messagesDataValue);
|
||||
TableDataValue messagesTable = (TableDataValue)messagesDataValue;
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(CopyMessagesToWorkflowConversationAsync),
|
||||
conversationId: "WorkflowConversationId",
|
||||
messages: ValueExpression.Literal(messagesTable),
|
||||
expectedMessageCount: 1,
|
||||
expectWorkflowEvent: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyMessagesToNonWorkflowConversationAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set(SystemScope.Names.ConversationId, FormulaValue.New("WorkflowConversationId"), VariableScopeNames.System);
|
||||
|
||||
List<ChatMessage> testMessages =
|
||||
[
|
||||
new ChatMessage(ChatRole.User, "Message to non-workflow conversation")
|
||||
];
|
||||
DataValue messagesDataValue = testMessages.ToTable().ToDataValue();
|
||||
Assert.IsType<TableDataValue>(messagesDataValue);
|
||||
TableDataValue messagesTable = (TableDataValue)messagesDataValue;
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(CopyMessagesToNonWorkflowConversationAsync),
|
||||
conversationId: "DifferentConversationId",
|
||||
messages: ValueExpression.Literal(messagesTable),
|
||||
expectedMessageCount: 1,
|
||||
expectWorkflowEvent: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyMessagesWithBlankDataValueAsync()
|
||||
{
|
||||
// Arrange, Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(CopyMessagesWithBlankDataValueAsync),
|
||||
conversationId: "TestConversationId",
|
||||
messages: ValueExpression.Literal(DataValue.Blank()),
|
||||
expectedMessageCount: 0);
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync(
|
||||
string displayName,
|
||||
string conversationId,
|
||||
ValueExpression messages,
|
||||
int expectedMessageCount,
|
||||
bool expectWorkflowEvent = false)
|
||||
{
|
||||
// Arrange
|
||||
MockAgentProvider mockAgentProvider = new();
|
||||
mockAgentProvider.TestMessages.Clear();
|
||||
|
||||
CopyConversationMessages model = this.CreateModel(
|
||||
this.FormatDisplayName(displayName),
|
||||
conversationId,
|
||||
messages);
|
||||
|
||||
CopyConversationMessagesExecutor action = new(model, mockAgentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events = await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedMessageCount, mockAgentProvider.TestMessages.Count);
|
||||
VerifyModel(model, action);
|
||||
|
||||
AgentResponseEvent[] responseEvents = events.OfType<AgentResponseEvent>().ToArray();
|
||||
if (expectWorkflowEvent && expectedMessageCount > 0)
|
||||
{
|
||||
Assert.NotEmpty(responseEvents);
|
||||
AgentResponseEvent responseEvent = responseEvents.First();
|
||||
Assert.Equal(action.Id, responseEvent.ExecutorId);
|
||||
Assert.NotNull(responseEvent.Response);
|
||||
Assert.Equal(expectedMessageCount, responseEvent.Response.Messages.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Empty(responseEvents);
|
||||
}
|
||||
}
|
||||
|
||||
private CopyConversationMessages CreateModel(
|
||||
string displayName,
|
||||
string conversationId,
|
||||
ValueExpression messages)
|
||||
{
|
||||
CopyConversationMessages.Builder actionBuilder = new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
ConversationId = StringExpression.Literal(conversationId),
|
||||
Messages = messages
|
||||
};
|
||||
|
||||
return AssignParent<CopyConversationMessages>(actionBuilder);
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
// 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.Agents.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="DefaultActionExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class DefaultActionExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteDefaultActionAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
this.FormatDisplayName(nameof(ExecuteDefaultActionAsync)));
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync(string displayName)
|
||||
{
|
||||
// Arrange
|
||||
ResetVariable model = this.CreateModel(displayName);
|
||||
|
||||
// Act
|
||||
DefaultActionExecutor action = new(model, this.State);
|
||||
WorkflowEvent[] events = await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
Assert.NotEmpty(events);
|
||||
}
|
||||
|
||||
private ResetVariable CreateModel(string displayName)
|
||||
{
|
||||
// Use a simple concrete action type since DialogAction.Builder is abstract
|
||||
ResetVariable.Builder actionBuilder =
|
||||
new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
Variable = PropertyPath.Create(FormatVariablePath("TestVariable")),
|
||||
};
|
||||
|
||||
return AssignParent<ResetVariable>(actionBuilder);
|
||||
}
|
||||
}
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="EditTableExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class EditTableExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void InvalidModelNullItemsVariable() =>
|
||||
// Arrange, Act, Assert
|
||||
Assert.Throws<DeclarativeModelException>(() => new EditTableExecutor(new EditTable(), this.State));
|
||||
|
||||
[Fact]
|
||||
public async Task AddItemToTableAsync()
|
||||
{
|
||||
// Arrange - Initialize table using Power FX expression
|
||||
FormulaValue tableValue = this.State.Engine.Eval("[{id: 3}]");
|
||||
this.State.Set("MyTable", tableValue);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(AddItemToTableAsync),
|
||||
variableName: "MyTable",
|
||||
changeType: TableChangeType.Add,
|
||||
value: new RecordDataValue([new("id", new NumberDataValue(7))]));
|
||||
|
||||
// Verify the variable now contains the added record
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
RecordValue resultRecord = Assert.IsAssignableFrom<RecordValue>(resultValue);
|
||||
DecimalValue idValue = Assert.IsType<DecimalValue>(resultRecord.GetField("id"));
|
||||
Assert.Equal(7, idValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddItemWithMultipleFieldsAsync()
|
||||
{
|
||||
// Arrange - Initialize table using Power FX expression
|
||||
FormulaValue tableValue = this.State.Engine.Eval("[{id: 1, name: \"First\"}]");
|
||||
this.State.Set("MyTable", tableValue);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(AddItemWithMultipleFieldsAsync),
|
||||
variableName: "MyTable",
|
||||
changeType: TableChangeType.Add,
|
||||
value: new RecordDataValue([
|
||||
new("id", new NumberDataValue(2)),
|
||||
new("name", new StringDataValue("Second"))
|
||||
]));
|
||||
|
||||
// Verify the variable now contains the added record
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
RecordValue resultRecord = Assert.IsAssignableFrom<RecordValue>(resultValue);
|
||||
DecimalValue idValue = Assert.IsType<DecimalValue>(resultRecord.GetField("id"));
|
||||
Assert.Equal(2, idValue.Value);
|
||||
StringValue nameValue = Assert.IsType<StringValue>(resultRecord.GetField("name"));
|
||||
Assert.Equal("Second", nameValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddItemToEmptyTableAsync()
|
||||
{
|
||||
// Arrange - Initialize empty table using Power FX expression with schema
|
||||
FormulaValue tableValue = this.State.Engine.Eval("Table({id: 1})");
|
||||
TableValue table = Assert.IsAssignableFrom<TableValue>(tableValue);
|
||||
// Clear the table to make it empty but preserve schema
|
||||
await table.ClearAsync(CancellationToken.None);
|
||||
this.State.Set("MyTable", table);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(AddItemToEmptyTableAsync),
|
||||
variableName: "MyTable",
|
||||
changeType: TableChangeType.Add,
|
||||
value: new RecordDataValue([new("id", new NumberDataValue(1))]));
|
||||
|
||||
// Verify the variable now contains the added record
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
RecordValue resultRecord = Assert.IsAssignableFrom<RecordValue>(resultValue);
|
||||
DecimalValue idValue = Assert.IsType<DecimalValue>(resultRecord.GetField("id"));
|
||||
Assert.Equal(1, idValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveItemFromTableAsync()
|
||||
{
|
||||
// Arrange - Initialize table using Power FX expression
|
||||
FormulaValue tableValue = this.State.Engine.Eval("[{id: 3}, {id: 7}]");
|
||||
this.State.Set("MyTable", tableValue);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(RemoveItemFromTableAsync),
|
||||
variableName: "MyTable",
|
||||
changeType: TableChangeType.Remove,
|
||||
value: new TableDataValue([new RecordDataValue([new("id", new NumberDataValue(3))])]));
|
||||
|
||||
// Verify the variable now contains an empty record
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
RecordValue resultRecord = Assert.IsAssignableFrom<RecordValue>(resultValue);
|
||||
// Empty record should have no fields
|
||||
Assert.Empty(resultRecord.Fields);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveMultipleItemsFromTableAsync()
|
||||
{
|
||||
// Arrange - Initialize table using Power FX expression
|
||||
FormulaValue tableValue = this.State.Engine.Eval("[{id: 1}, {id: 2}, {id: 3}]");
|
||||
this.State.Set("MyTable", tableValue);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(RemoveMultipleItemsFromTableAsync),
|
||||
variableName: "MyTable",
|
||||
changeType: TableChangeType.Remove,
|
||||
value: new TableDataValue([
|
||||
new RecordDataValue([new("id", new NumberDataValue(1))]),
|
||||
new RecordDataValue([new("id", new NumberDataValue(3))])
|
||||
]));
|
||||
|
||||
// Verify the variable now contains an empty record
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
RecordValue resultRecord = Assert.IsAssignableFrom<RecordValue>(resultValue);
|
||||
// Empty record should have no fields
|
||||
Assert.Empty(resultRecord.Fields);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearTableAsync()
|
||||
{
|
||||
// Arrange - Initialize table using Power FX expression
|
||||
FormulaValue tableValue = this.State.Engine.Eval("[{id: 1}, {id: 2}]");
|
||||
this.State.Set("MyTable", tableValue);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ClearTableAsync),
|
||||
variableName: "MyTable",
|
||||
changeType: TableChangeType.Clear,
|
||||
value: null);
|
||||
|
||||
// Verify table is cleared
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
Assert.IsType<BlankValue>(resultValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearEmptyTableAsync()
|
||||
{
|
||||
// Arrange - Initialize empty table using Power FX expression with schema
|
||||
FormulaValue tableValue = this.State.Engine.Eval("Table({id: 1})");
|
||||
TableValue table = Assert.IsAssignableFrom<TableValue>(tableValue);
|
||||
// Clear the table to make it empty but preserve schema
|
||||
await table.ClearAsync(CancellationToken.None);
|
||||
this.State.Set("MyTable", table);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ClearEmptyTableAsync),
|
||||
variableName: "MyTable",
|
||||
changeType: TableChangeType.Clear,
|
||||
value: null);
|
||||
|
||||
// Verify table is blank
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
Assert.IsType<BlankValue>(resultValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TakeFirstItemAsync()
|
||||
{
|
||||
// Arrange - Initialize table using Power FX expression
|
||||
FormulaValue tableValue = this.State.Engine.Eval("[{id: 10}, {id: 20}, {id: 30}]");
|
||||
this.State.Set("MyTable", tableValue);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(TakeFirstItemAsync),
|
||||
variableName: "MyTable",
|
||||
changeType: TableChangeType.TakeFirst,
|
||||
value: null);
|
||||
|
||||
// Verify the variable now contains the first record that was taken
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
RecordValue resultRecord = Assert.IsAssignableFrom<RecordValue>(resultValue);
|
||||
DecimalValue idValue = Assert.IsType<DecimalValue>(resultRecord.GetField("id"));
|
||||
Assert.Equal(10, idValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TakeFirstFromEmptyTableAsync()
|
||||
{
|
||||
// Arrange - Initialize empty table using Power FX expression with schema
|
||||
FormulaValue tableValue = this.State.Engine.Eval("Table({id: 1})");
|
||||
TableValue table = Assert.IsAssignableFrom<TableValue>(tableValue);
|
||||
// Clear the table to make it empty but preserve schema
|
||||
await table.ClearAsync(CancellationToken.None);
|
||||
this.State.Set("MyTable", table);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(TakeFirstFromEmptyTableAsync),
|
||||
variableName: "MyTable",
|
||||
changeType: TableChangeType.TakeFirst,
|
||||
value: null);
|
||||
|
||||
// Verify table is still empty (nothing was taken, variable remains unchanged)
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
TableValue resultTable = Assert.IsAssignableFrom<TableValue>(resultValue);
|
||||
Assert.Empty(resultTable.Rows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TakeLastItemAsync()
|
||||
{
|
||||
// Arrange - Initialize table using Power FX expression
|
||||
FormulaValue tableValue = this.State.Engine.Eval("[{id: 10}, {id: 20}, {id: 30}]");
|
||||
this.State.Set("MyTable", tableValue);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(TakeLastItemAsync),
|
||||
variableName: "MyTable",
|
||||
changeType: TableChangeType.TakeLast,
|
||||
value: null);
|
||||
|
||||
// Verify the variable now contains the last record that was taken
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
RecordValue resultRecord = Assert.IsAssignableFrom<RecordValue>(resultValue);
|
||||
DecimalValue idValue = Assert.IsType<DecimalValue>(resultRecord.GetField("id"));
|
||||
Assert.Equal(30, idValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TakeLastFromEmptyTableAsync()
|
||||
{
|
||||
// Arrange - Initialize empty table using Power FX expression with schema
|
||||
FormulaValue tableValue = this.State.Engine.Eval("Table({id: 1})");
|
||||
TableValue table = Assert.IsAssignableFrom<TableValue>(tableValue);
|
||||
// Clear the table to make it empty but preserve schema
|
||||
await table.ClearAsync(CancellationToken.None);
|
||||
this.State.Set("MyTable", table);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(TakeLastFromEmptyTableAsync),
|
||||
variableName: "MyTable",
|
||||
changeType: TableChangeType.TakeLast,
|
||||
value: null);
|
||||
|
||||
// Verify table is still empty (nothing was taken, variable remains unchanged)
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
TableValue resultTable = Assert.IsAssignableFrom<TableValue>(resultValue);
|
||||
Assert.Empty(resultTable.Rows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TakeFirstFromSingleItemTableAsync()
|
||||
{
|
||||
// Arrange - Initialize table using Power FX expression
|
||||
FormulaValue tableValue = this.State.Engine.Eval("[{id: 100}]");
|
||||
this.State.Set("MyTable", tableValue);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(TakeFirstFromSingleItemTableAsync),
|
||||
variableName: "MyTable",
|
||||
changeType: TableChangeType.TakeFirst,
|
||||
value: null);
|
||||
|
||||
// Verify variable contains the record that was taken
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
RecordValue resultRecord = Assert.IsAssignableFrom<RecordValue>(resultValue);
|
||||
DecimalValue idValue = Assert.IsType<DecimalValue>(resultRecord.GetField("id"));
|
||||
Assert.Equal(100, idValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TakeLastFromSingleItemTableAsync()
|
||||
{
|
||||
// Arrange - Initialize table using Power FX expression
|
||||
FormulaValue tableValue = this.State.Engine.Eval("[{id: 100}]");
|
||||
this.State.Set("MyTable", tableValue);
|
||||
|
||||
// Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(TakeLastFromSingleItemTableAsync),
|
||||
variableName: "MyTable",
|
||||
changeType: TableChangeType.TakeLast,
|
||||
value: null);
|
||||
|
||||
// Verify variable contains the record that was taken
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
RecordValue resultRecord = Assert.IsAssignableFrom<RecordValue>(resultValue);
|
||||
DecimalValue idValue = Assert.IsType<DecimalValue>(resultRecord.GetField("id"));
|
||||
Assert.Equal(100, idValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ErrorWhenVariableIsNotTableAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set("NotATable", FormulaValue.New("This is a string, not a table"));
|
||||
|
||||
EditTable model = this.CreateModel(
|
||||
nameof(ErrorWhenVariableIsNotTableAsync),
|
||||
"NotATable",
|
||||
TableChangeType.Add,
|
||||
new RecordDataValue([new("id", new NumberDataValue(1))]));
|
||||
|
||||
// Act
|
||||
EditTableExecutor action = new(model, this.State);
|
||||
|
||||
// Assert - Should throw an exception for non-table variable
|
||||
DeclarativeActionException exception = await Assert.ThrowsAsync<DeclarativeActionException>(
|
||||
async () => await this.ExecuteAsync(action));
|
||||
Assert.NotNull(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddWithExpressionAsync()
|
||||
{
|
||||
// Arrange - Initialize table using Power FX expression
|
||||
FormulaValue tableValue = this.State.Engine.Eval("[{id: 5}]");
|
||||
this.State.Set("MyTable", tableValue);
|
||||
this.State.Set("NewId", FormulaValue.New(10));
|
||||
|
||||
EditTable model = this.CreateModel(
|
||||
nameof(AddWithExpressionAsync),
|
||||
"MyTable",
|
||||
TableChangeType.Add,
|
||||
ValueExpression.Expression("{id: Local.NewId}"));
|
||||
|
||||
// Act
|
||||
EditTableExecutor action = new(model, this.State);
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert - Variable should contain the newly added record
|
||||
VerifyModel(model, action);
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
RecordValue resultRecord = Assert.IsAssignableFrom<RecordValue>(resultValue);
|
||||
DecimalValue idValue = Assert.IsType<DecimalValue>(resultRecord.GetField("id"));
|
||||
Assert.Equal(10, idValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveWithNonTableValueAsync()
|
||||
{
|
||||
// Arrange - Initialize table using Power FX expression
|
||||
FormulaValue tableValue = this.State.Engine.Eval("[{id: 1}, {id: 2}]");
|
||||
this.State.Set("MyTable", tableValue);
|
||||
|
||||
// Try to remove using a non-table value (should not throw, just not remove anything)
|
||||
EditTable model = this.CreateModel(
|
||||
nameof(RemoveWithNonTableValueAsync),
|
||||
"MyTable",
|
||||
TableChangeType.Remove,
|
||||
new RecordDataValue([new("id", new NumberDataValue(1))]));
|
||||
|
||||
// Act
|
||||
EditTableExecutor action = new(model, this.State);
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert - table should remain unchanged since value is not a TableDataValue
|
||||
VerifyModel(model, action);
|
||||
FormulaValue resultValue = this.State.Get("MyTable");
|
||||
TableValue resultTable = Assert.IsAssignableFrom<TableValue>(resultValue);
|
||||
Assert.Equal(2, resultTable.Rows.Count());
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync(
|
||||
string displayName,
|
||||
string variableName,
|
||||
TableChangeType changeType,
|
||||
DataValue? value)
|
||||
{
|
||||
// Arrange
|
||||
EditTable model = this.CreateModel(displayName, variableName, changeType, value);
|
||||
|
||||
// Act
|
||||
EditTableExecutor action = new(model, this.State);
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
}
|
||||
|
||||
private EditTable CreateModel(
|
||||
string displayName,
|
||||
string variableName,
|
||||
TableChangeType changeType,
|
||||
DataValue? value)
|
||||
{
|
||||
ValueExpression.Builder? valueExpressionBuilder = value switch
|
||||
{
|
||||
null => null,
|
||||
_ => new ValueExpression.Builder(ValueExpression.Literal(value))
|
||||
};
|
||||
|
||||
return this.CreateModel(displayName, variableName, changeType, valueExpressionBuilder);
|
||||
}
|
||||
|
||||
private EditTable CreateModel(
|
||||
string displayName,
|
||||
string variableName,
|
||||
TableChangeType changeType,
|
||||
ValueExpression valueExpression)
|
||||
{
|
||||
ValueExpression.Builder valueExpressionBuilder = new(valueExpression);
|
||||
return this.CreateModel(displayName, variableName, changeType, valueExpressionBuilder);
|
||||
}
|
||||
|
||||
private EditTable CreateModel(
|
||||
string displayName,
|
||||
string variableName,
|
||||
TableChangeType changeType,
|
||||
ValueExpression.Builder? valueExpression)
|
||||
{
|
||||
EditTable.Builder actionBuilder = new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
ItemsVariable = PropertyPath.Create(FormatVariablePath(variableName)),
|
||||
ChangeType = TableChangeTypeWrapper.Get(changeType),
|
||||
Value = valueExpression,
|
||||
};
|
||||
|
||||
return AssignParent<EditTable>(actionBuilder);
|
||||
}
|
||||
}
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="EditTableV2Executor"/>.
|
||||
/// </summary>
|
||||
public sealed class EditTableV2ExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void InvalidModelNullItemsVariable()
|
||||
{
|
||||
// Arrange
|
||||
EditTableV2 model = new EditTableV2.Builder
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(nameof(InvalidModelNullItemsVariable)),
|
||||
ItemsVariable = null,
|
||||
ChangeType = new AddItemOperation.Builder
|
||||
{
|
||||
Value = new ValueExpression.Builder(ValueExpression.Literal(new StringDataValue("test")))
|
||||
}.Build()
|
||||
}.Build();
|
||||
|
||||
// Act, Assert
|
||||
DeclarativeModelException exception = Assert.Throws<DeclarativeModelException>(() => new EditTableV2Executor(model, this.State));
|
||||
Assert.Contains("required", exception.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvalidModelVariableNotTableAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set("NotATable", FormulaValue.New("I am a string"));
|
||||
|
||||
EditTableV2 model = this.CreateModel(
|
||||
nameof(InvalidModelVariableNotTableAsync),
|
||||
"NotATable",
|
||||
new AddItemOperation.Builder
|
||||
{
|
||||
Value = new ValueExpression.Builder(ValueExpression.Literal(new StringDataValue("test")))
|
||||
}.Build());
|
||||
|
||||
EditTableV2Executor action = new(model, this.State);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<DeclarativeActionException>(async () => await this.ExecuteAsync(action));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvalidModelAddItemOperationNullValueAsync()
|
||||
{
|
||||
// Arrange
|
||||
EditTableV2 model = new EditTableV2.Builder
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(nameof(InvalidModelAddItemOperationNullValueAsync)),
|
||||
ItemsVariable = PropertyPath.Create(FormatVariablePath("TestTable")),
|
||||
ChangeType = new AddItemOperation.Builder
|
||||
{
|
||||
Value = null
|
||||
}.Build()
|
||||
}.Build();
|
||||
|
||||
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
||||
TableValue tableValue = FormulaValue.NewTable(recordType);
|
||||
this.State.Set("TestTable", tableValue);
|
||||
|
||||
// Act, Assert
|
||||
EditTableV2Executor action = new(model, this.State);
|
||||
await Assert.ThrowsAsync<DeclarativeActionException>(async () => await this.ExecuteAsync(action));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvalidModelRemoveItemOperationNullValueAsync()
|
||||
{
|
||||
// Arrange
|
||||
EditTableV2 model = new EditTableV2.Builder
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(nameof(InvalidModelRemoveItemOperationNullValueAsync)),
|
||||
ItemsVariable = PropertyPath.Create(FormatVariablePath("TestTable")),
|
||||
ChangeType = new RemoveItemOperation.Builder
|
||||
{
|
||||
Value = null
|
||||
}.Build()
|
||||
}.Build();
|
||||
|
||||
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
||||
TableValue tableValue = FormulaValue.NewTable(recordType);
|
||||
this.State.Set("TestTable", tableValue);
|
||||
|
||||
// Act, Assert
|
||||
EditTableV2Executor action = new(model, this.State);
|
||||
await Assert.ThrowsAsync<DeclarativeActionException>(async () => await this.ExecuteAsync(action));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveItemOperationNonTableValueAsync()
|
||||
{
|
||||
// Arrange
|
||||
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
||||
RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1")));
|
||||
TableValue tableValue = FormulaValue.NewTable(recordType, record1);
|
||||
this.State.Set("TestTable", tableValue);
|
||||
|
||||
// Set a string value instead of a table for removal
|
||||
this.State.Set("RemoveItems", FormulaValue.New("NotATable"));
|
||||
|
||||
EditTableV2 model = new EditTableV2.Builder
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(nameof(RemoveItemOperationNonTableValueAsync)),
|
||||
ItemsVariable = PropertyPath.Create(FormatVariablePath("TestTable")),
|
||||
ChangeType = new RemoveItemOperation.Builder
|
||||
{
|
||||
Value = new ValueExpression.Builder(ValueExpression.Variable(PropertyPath.TopicVariable("RemoveItems")))
|
||||
}.Build()
|
||||
}.Build();
|
||||
|
||||
// Act
|
||||
EditTableV2Executor action = new(model, this.State);
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert: When the remove value is not a table, no removal occurs, so the table should be unchanged
|
||||
FormulaValue value = this.State.Get("TestTable");
|
||||
Assert.IsAssignableFrom<TableValue>(value);
|
||||
TableValue resultTable = (TableValue)value;
|
||||
Assert.Single(resultTable.Rows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddItemOperationWithSingleFieldRecordAsync()
|
||||
{
|
||||
// Arrange: Create an empty table with single field
|
||||
RecordType recordType = RecordType.Empty().Add("Name", FormulaType.String);
|
||||
TableValue tableValue = FormulaValue.NewTable(recordType);
|
||||
this.State.Set("TestTable", tableValue);
|
||||
|
||||
// Arrange, Act, Assert
|
||||
await this.ExecuteTestAsync<RecordValue>(
|
||||
displayName: nameof(AddItemOperationWithSingleFieldRecordAsync),
|
||||
variableName: "TestTable",
|
||||
changeType: this.CreateAddItemOperation(new RecordDataValue.Builder
|
||||
{
|
||||
Properties =
|
||||
{
|
||||
["Name"] = new StringDataValue("John")
|
||||
}
|
||||
}.Build()),
|
||||
verifyAction: (variableName, recordValue) =>
|
||||
Assert.Equal("John", recordValue.GetField("Name").ToObject())
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddItemOperationWithScalarValueAsync()
|
||||
{
|
||||
// Arrange: Create an empty table with single field
|
||||
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
||||
TableValue tableValue = FormulaValue.NewTable(recordType);
|
||||
this.State.Set("TestTable", tableValue);
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync<RecordValue>(
|
||||
displayName: nameof(AddItemOperationWithScalarValueAsync),
|
||||
variableName: "TestTable",
|
||||
changeType: this.CreateAddItemOperation(new StringDataValue("TestValue")),
|
||||
verifyAction: (variableName, recordValue) =>
|
||||
Assert.Equal("TestValue", recordValue.GetField("Value").ToObject())
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearItemsOperationAsync()
|
||||
{
|
||||
// Arrange: Create a table with some items
|
||||
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
||||
RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1")));
|
||||
RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2")));
|
||||
TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2);
|
||||
this.State.Set("TestTable", tableValue);
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync<BlankValue>(
|
||||
displayName: nameof(ClearItemsOperationAsync),
|
||||
variableName: "TestTable",
|
||||
changeType: new ClearItemsOperation.Builder().Build());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RemoveItemOperationAsync()
|
||||
{
|
||||
// Arrange: Create a table with some items
|
||||
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
||||
RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1")));
|
||||
RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2")));
|
||||
TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2);
|
||||
this.State.Set("TestTable", tableValue);
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync<BlankValue>(
|
||||
displayName: nameof(RemoveItemOperationAsync),
|
||||
variableName: "TestTable",
|
||||
changeType: this.CreateRemoveItemOperation("Item1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TakeLastItemOperationWithItemsAsync()
|
||||
{
|
||||
// Arrange: Create a table with some items
|
||||
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
||||
RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1")));
|
||||
RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2")));
|
||||
RecordValue record3 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item3")));
|
||||
TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2, record3);
|
||||
this.State.Set("TestTable", tableValue);
|
||||
|
||||
// Arrange, Act, Assert
|
||||
await this.ExecuteTestAsync<RecordValue>(
|
||||
displayName: nameof(TakeLastItemOperationWithItemsAsync),
|
||||
variableName: "TestTable",
|
||||
changeType: new TakeLastItemOperation.Builder().Build(),
|
||||
verifyAction: (variableName, recordValue) =>
|
||||
Assert.Equal("Item3", recordValue.GetField("Value").ToObject())
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TakeLastItemOperationEmptyTableAsync()
|
||||
{
|
||||
// Arrange: Create an empty table
|
||||
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
||||
TableValue tableValue = FormulaValue.NewTable(recordType);
|
||||
this.State.Set("TestTable", tableValue);
|
||||
|
||||
// Arrange, Act, Assert
|
||||
await this.ExecuteTestAsync<TableValue>(
|
||||
displayName: nameof(TakeLastItemOperationEmptyTableAsync),
|
||||
variableName: "TestTable",
|
||||
changeType: new TakeLastItemOperation.Builder().Build());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TakeFirstItemOperationWithItemsAsync()
|
||||
{
|
||||
// Arrange: Create a table with some items
|
||||
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
||||
RecordValue record1 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item1")));
|
||||
RecordValue record2 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item2")));
|
||||
RecordValue record3 = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New("Item3")));
|
||||
TableValue tableValue = FormulaValue.NewTable(recordType, record1, record2, record3);
|
||||
this.State.Set("TestTable", tableValue);
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync<RecordValue>(
|
||||
displayName: nameof(TakeFirstItemOperationWithItemsAsync),
|
||||
variableName: "TestTable",
|
||||
changeType: new TakeFirstItemOperation.Builder().Build(),
|
||||
verifyAction: (variableName, recordValue) =>
|
||||
Assert.Equal("Item1", recordValue.GetField("Value").ToObject())
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TakeFirstItemOperationEmptyTableAsync()
|
||||
{
|
||||
// Arrange: Create an empty table
|
||||
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
||||
TableValue tableValue = FormulaValue.NewTable(recordType);
|
||||
this.State.Set("TestTable", tableValue);
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync<TableValue>(
|
||||
displayName: nameof(TakeFirstItemOperationEmptyTableAsync),
|
||||
variableName: "TestTable",
|
||||
changeType: new TakeFirstItemOperation.Builder().Build());
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync<TValue>(
|
||||
string displayName,
|
||||
string variableName,
|
||||
EditTableOperation changeType,
|
||||
Action<string, TValue>? verifyAction = null) where TValue : FormulaValue
|
||||
{
|
||||
// Arrange
|
||||
EditTableV2 model = this.CreateModel(displayName, variableName, changeType);
|
||||
|
||||
EditTableV2Executor action = new(model, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
FormulaValue value = this.State.Get(variableName);
|
||||
TValue typedValue = Assert.IsAssignableFrom<TValue>(value);
|
||||
verifyAction?.Invoke(variableName, typedValue);
|
||||
}
|
||||
|
||||
private EditTableV2 CreateModel(string displayName, string variableName, EditTableOperation changeType)
|
||||
{
|
||||
EditTableV2.Builder actionBuilder = new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
ItemsVariable = PropertyPath.Create(FormatVariablePath(variableName)),
|
||||
ChangeType = changeType
|
||||
};
|
||||
|
||||
return AssignParent<EditTableV2>(actionBuilder);
|
||||
}
|
||||
|
||||
private AddItemOperation CreateAddItemOperation(DataValue value)
|
||||
{
|
||||
return new AddItemOperation.Builder
|
||||
{
|
||||
Value = new ValueExpression.Builder(ValueExpression.Literal(value))
|
||||
}.Build();
|
||||
}
|
||||
|
||||
private RemoveItemOperation CreateRemoveItemOperation(string itemValue)
|
||||
{
|
||||
// Create a table with the item to remove
|
||||
RecordType recordType = RecordType.Empty().Add("Value", FormulaType.String);
|
||||
RecordValue recordToRemove = FormulaValue.NewRecordFromFields(recordType, new NamedValue("Value", FormulaValue.New(itemValue)));
|
||||
TableValue tableToRemove = FormulaValue.NewTable(recordType, recordToRemove);
|
||||
|
||||
// Store in state for expression evaluation
|
||||
this.State.Set("RemoveItems", tableToRemove);
|
||||
this.State.Bind();
|
||||
|
||||
return new RemoveItemOperation.Builder
|
||||
{
|
||||
Value = new ValueExpression.Builder(ValueExpression.Variable(PropertyPath.TopicVariable("RemoveItems")))
|
||||
}.Build();
|
||||
}
|
||||
}
|
||||
+625
@@ -0,0 +1,625 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="ForeachExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class ForeachExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void ForeachThrowsWhenModelInvalid() =>
|
||||
// Arrange, Act & Assert
|
||||
Assert.Throws<DeclarativeModelException>(() => new ForeachExecutor(new Foreach(), this.State));
|
||||
|
||||
[Fact]
|
||||
public void ForeachNamingConvention()
|
||||
{
|
||||
// Arrange
|
||||
string testId = this.CreateActionId().Value;
|
||||
|
||||
// Act
|
||||
string startStep = ForeachExecutor.Steps.Start(testId);
|
||||
string nextStep = ForeachExecutor.Steps.Next(testId);
|
||||
string endStep = ForeachExecutor.Steps.End(testId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal($"{testId}_{nameof(ForeachExecutor.Steps.Start)}", startStep);
|
||||
Assert.Equal($"{testId}_{nameof(ForeachExecutor.Steps.Next)}", nextStep);
|
||||
Assert.Equal($"{testId}_{nameof(ForeachExecutor.Steps.End)}", endStep);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForeachInvokedWithSingleValueAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SetVariableState("CurrentValue");
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ForeachInvokedWithSingleValueAsync),
|
||||
items: ValueExpression.Literal(new NumberDataValue(42)),
|
||||
valueName: "CurrentValue",
|
||||
indexName: null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForeachInvokedWithTableValueAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SetVariableState("CurrentValue");
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ForeachInvokedWithTableValueAsync),
|
||||
items: ValueExpression.Literal(DataValue.EmptyTable),
|
||||
valueName: "CurrentValue",
|
||||
indexName: null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForeachInvokedWithIndexAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SetVariableState("CurrentValue", "CurrentIndex");
|
||||
TableDataValue tableValue = DataValue.TableFromRecords(
|
||||
DataValue.RecordFromFields(new KeyValuePair<string, DataValue>("item", new NumberDataValue(1))),
|
||||
DataValue.RecordFromFields(new KeyValuePair<string, DataValue>("item", new NumberDataValue(2))),
|
||||
DataValue.RecordFromFields(new KeyValuePair<string, DataValue>("item", new NumberDataValue(3))));
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ForeachInvokedWithIndexAsync),
|
||||
items: ValueExpression.Literal(tableValue),
|
||||
valueName: "CurrentValue",
|
||||
indexName: "CurrentIndex");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForeachInvokedWithExpressionAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SetVariableState("CurrentValue");
|
||||
this.State.Set("SourceArray", FormulaValue.NewTable(RecordType.Empty()));
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ForeachInvokedWithExpressionAsync),
|
||||
items: ValueExpression.Variable(PropertyPath.TopicVariable("SourceArray")),
|
||||
valueName: "CurrentValue",
|
||||
indexName: null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForeachTakeNextAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SetVariableState("CurrentValue");
|
||||
this.State.Set(
|
||||
"SourceArray",
|
||||
FormulaValue.NewTable(
|
||||
RecordType.Empty(),
|
||||
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(10))),
|
||||
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(20))),
|
||||
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(30)))));
|
||||
|
||||
// Act & Assert
|
||||
await this.TakeNextTestAsync(
|
||||
displayName: nameof(ForeachTakeNextAsync),
|
||||
items: ValueExpression.Variable(PropertyPath.TopicVariable("SourceArray")),
|
||||
valueName: "CurrentValue",
|
||||
indexName: null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForeachTakeNextWithIndexAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SetVariableState("CurrentValue", "CurrentIndex");
|
||||
this.State.Set(
|
||||
"SourceArray",
|
||||
FormulaValue.NewTable(
|
||||
RecordType.Empty(),
|
||||
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(10))),
|
||||
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(20))),
|
||||
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(30)))));
|
||||
|
||||
// Act & Assert
|
||||
await this.TakeNextTestAsync(
|
||||
displayName: nameof(ForeachTakeNextWithIndexAsync),
|
||||
items: ValueExpression.Variable(PropertyPath.TopicVariable("SourceArray")),
|
||||
valueName: "CurrentValue",
|
||||
indexName: "CurrentIndex");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForeachTakeNextWithMultiFieldRecordAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string CurrentValueName = "CurrentValue";
|
||||
this.SetVariableState(CurrentValueName);
|
||||
|
||||
TableDataValue tableValue = DataValue.TableFromRecords(
|
||||
DataValue.RecordFromFields(
|
||||
new KeyValuePair<string, DataValue>("name", new StringDataValue("Alice")),
|
||||
new KeyValuePair<string, DataValue>("role", new StringDataValue("Engineer"))));
|
||||
|
||||
Foreach model = this.CreateModel(
|
||||
displayName: nameof(ForeachTakeNextWithMultiFieldRecordAsync),
|
||||
items: ValueExpression.Literal(tableValue),
|
||||
valueName: CurrentValueName,
|
||||
indexName: null);
|
||||
ForeachExecutor action = new(model, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action, ForeachExecutor.Steps.Next(action.Id), action.TakeNextAsync);
|
||||
|
||||
// Assert
|
||||
RecordValue currentValue = Assert.IsType<RecordValue>(this.State.Get(CurrentValueName), exactMatch: false);
|
||||
Assert.Equal("Alice", currentValue.GetField("name").ToObject());
|
||||
Assert.Equal("Engineer", currentValue.GetField("role").ToObject());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Power Fx wraps scalar array literals such as <c>=[1, 2, 3]</c> as <c>Table({Value: 1}, ...)</c>;
|
||||
/// the loop value must expose the bare scalar, not the single-column wrapper record.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ForeachTakeNextWithSingleColumnValueRecordAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string CurrentValueName = "CurrentValue";
|
||||
this.SetVariableState(CurrentValueName);
|
||||
|
||||
TableDataValue tableValue = DataValue.TableFromRecords(
|
||||
DataValue.RecordFromFields(new KeyValuePair<string, DataValue>("Value", new NumberDataValue(1))),
|
||||
DataValue.RecordFromFields(new KeyValuePair<string, DataValue>("Value", new NumberDataValue(2))),
|
||||
DataValue.RecordFromFields(new KeyValuePair<string, DataValue>("Value", new NumberDataValue(3))));
|
||||
|
||||
Foreach model = this.CreateModel(
|
||||
displayName: nameof(ForeachTakeNextWithSingleColumnValueRecordAsync),
|
||||
items: ValueExpression.Literal(tableValue),
|
||||
valueName: CurrentValueName,
|
||||
indexName: null);
|
||||
ForeachExecutor action = new(model, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action, ForeachExecutor.Steps.Next(action.Id), action.TakeNextAsync);
|
||||
|
||||
// Assert
|
||||
FormulaValue currentValue = this.State.Get(CurrentValueName);
|
||||
Assert.IsNotType<RecordValue>(currentValue, exactMatch: false);
|
||||
Assert.Equal(1m, currentValue.ToObject());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single-field records whose only field is NOT named <c>Value</c> are not Power Fx auto-wraps;
|
||||
/// they are preserved as records so the field name remains accessible inside the loop body.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ForeachTakeNextWithSingleFieldNonValueRecordAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string CurrentValueName = "CurrentValue";
|
||||
this.SetVariableState(CurrentValueName);
|
||||
|
||||
TableDataValue tableValue = DataValue.TableFromRecords(
|
||||
DataValue.RecordFromFields(new KeyValuePair<string, DataValue>("name", new StringDataValue("Alice"))));
|
||||
|
||||
Foreach model = this.CreateModel(
|
||||
displayName: nameof(ForeachTakeNextWithSingleFieldNonValueRecordAsync),
|
||||
items: ValueExpression.Literal(tableValue),
|
||||
valueName: CurrentValueName,
|
||||
indexName: null);
|
||||
ForeachExecutor action = new(model, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action, ForeachExecutor.Steps.Next(action.Id), action.TakeNextAsync);
|
||||
|
||||
// Assert
|
||||
RecordValue currentValue = Assert.IsType<RecordValue>(this.State.Get(CurrentValueName), exactMatch: false);
|
||||
Assert.Equal("Alice", currentValue.GetField("name").ToObject());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForeachTakeLastAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SetVariableState("CurrentValue");
|
||||
this.State.Set(
|
||||
"SourceArray",
|
||||
FormulaValue.NewTable(
|
||||
RecordType.Empty(),
|
||||
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(10)))));
|
||||
|
||||
// Act & Assert
|
||||
await this.TakeNextTestAsync(
|
||||
displayName: nameof(ForeachTakeLastAsync),
|
||||
items: ValueExpression.Variable(PropertyPath.TopicVariable("SourceArray")),
|
||||
valueName: "CurrentValue",
|
||||
indexName: null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForeachTakeNextWhenDoneAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SetVariableState("CurrentValue");
|
||||
|
||||
// Act & Assert
|
||||
await this.TakeNextTestAsync(
|
||||
displayName: nameof(ForeachTakeNextWhenDoneAsync),
|
||||
items: ValueExpression.Literal(DataValue.EmptyTable),
|
||||
valueName: "CurrentValue",
|
||||
indexName: null,
|
||||
expectValue: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForeachCompletedWithoutIndexAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SetVariableState("CurrentValue");
|
||||
|
||||
// Act & Assert
|
||||
await this.CompletedTestAsync(
|
||||
displayName: nameof(ForeachCompletedWithoutIndexAsync),
|
||||
valueName: "CurrentValue",
|
||||
indexName: null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForeachCompletedWithIndexAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SetVariableState("CurrentValue", "CurrentIndex");
|
||||
|
||||
// Act & Assert
|
||||
await this.CompletedTestAsync(
|
||||
displayName: nameof(ForeachCompletedWithIndexAsync),
|
||||
valueName: "CurrentValue",
|
||||
indexName: "CurrentIndex");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test for GH-5009: a <see cref="ForeachExecutor"/> that is re-instantiated
|
||||
/// during checkpoint restore (e.g. cross-process resume after a <c>Question</c> inside the
|
||||
/// loop body) must continue iterating from where it left off, not exit after the first
|
||||
/// iteration.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ForeachStateRestoredAcrossCheckpointAsync()
|
||||
{
|
||||
// Arrange — a 3-item source table and a freshly-bound foreach executor (instance A).
|
||||
const string SourceVariableName = "SourceArray";
|
||||
this.SetVariableState("CurrentValue");
|
||||
this.State.Set(
|
||||
SourceVariableName,
|
||||
FormulaValue.NewTable(
|
||||
RecordType.Empty(),
|
||||
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(10))),
|
||||
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(20))),
|
||||
FormulaValue.NewRecordFromFields(new NamedValue("value", FormulaValue.New(30)))));
|
||||
|
||||
Foreach model = this.CreateModel(
|
||||
displayName: nameof(ForeachStateRestoredAcrossCheckpointAsync),
|
||||
items: ValueExpression.Variable(PropertyPath.TopicVariable(SourceVariableName)),
|
||||
valueName: "CurrentValue",
|
||||
indexName: null);
|
||||
|
||||
ForeachExecutor instanceA = new(model, this.State);
|
||||
|
||||
// Drive instance A through ExecuteAsync (initializes _values/_index) and one TakeNextAsync
|
||||
// so that _index advances to 1 and HasValue is true — the state at the point a Question
|
||||
// inside the loop body would pause the workflow and trigger a checkpoint.
|
||||
await this.ExecuteAsync(instanceA, ForeachExecutor.Steps.Next(instanceA.Id), instanceA.TakeNextAsync);
|
||||
Assert.True(instanceA.HasValue, "Instance A should have a current item after the first TakeNextAsync.");
|
||||
|
||||
// Act 1 — instance A persists checkpoint state.
|
||||
InMemoryWorkflowContext checkpoint = new();
|
||||
await InvokeOnCheckpointingAsync(instanceA, checkpoint);
|
||||
|
||||
// Act 2 — a fresh instance B (simulating cross-process resume) restores from the checkpoint.
|
||||
ForeachExecutor instanceB = new(model, this.State);
|
||||
await InvokeOnCheckpointRestoredAsync(instanceB, checkpoint);
|
||||
|
||||
// Assert — HasValue carries over so the routing predicate after loopId continues to take
|
||||
// the "loop body" edge instead of falling through to the loop continuation.
|
||||
Assert.True(instanceB.HasValue, "Restored instance should report HasValue == true at the checkpointed cursor.");
|
||||
|
||||
// Drive iteration 2 and 3 through instance B; both should succeed.
|
||||
await instanceB.TakeNextAsync(checkpoint, _: null, CancellationToken.None);
|
||||
Assert.True(instanceB.HasValue, "Restored instance should advance to iteration 2 (value=20).");
|
||||
|
||||
await instanceB.TakeNextAsync(checkpoint, _: null, CancellationToken.None);
|
||||
Assert.True(instanceB.HasValue, "Restored instance should advance to iteration 3 (value=30).");
|
||||
|
||||
// Driving past the end exits the loop normally.
|
||||
await instanceB.TakeNextAsync(checkpoint, _: null, CancellationToken.None);
|
||||
Assert.False(instanceB.HasValue, "Restored instance should report HasValue == false after exhausting all items.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When no checkpoint state has been written for the executor (e.g. first run), the restore
|
||||
/// hook must be a no-op and leave constructor defaults in place.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ForeachRestoreWithNoSavedStateAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SetVariableState("CurrentValue");
|
||||
Foreach model = this.CreateModel(
|
||||
displayName: nameof(ForeachRestoreWithNoSavedStateAsync),
|
||||
items: ValueExpression.Literal(DataValue.EmptyTable),
|
||||
valueName: "CurrentValue",
|
||||
indexName: null);
|
||||
|
||||
ForeachExecutor executor = new(model, this.State);
|
||||
InMemoryWorkflowContext emptyContext = new();
|
||||
|
||||
// Act — restoring against an empty context must not throw and must leave the executor
|
||||
// in its constructor-default state.
|
||||
await InvokeOnCheckpointRestoredAsync(executor, emptyContext);
|
||||
|
||||
// Assert
|
||||
Assert.False(executor.HasValue);
|
||||
|
||||
// A subsequent TakeNextAsync (without a prior ExecuteAsync) should report no value
|
||||
// because _values is still the empty constructor default.
|
||||
await executor.TakeNextAsync(emptyContext, _: null, CancellationToken.None);
|
||||
Assert.False(executor.HasValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checkpoint/restore around a foreach over an empty source must roundtrip cleanly
|
||||
/// (zero-length <c>PortableValue[]</c> snapshot).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ForeachStateSurvivesEmptyValuesAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.SetVariableState("CurrentValue");
|
||||
Foreach model = this.CreateModel(
|
||||
displayName: nameof(ForeachStateSurvivesEmptyValuesAsync),
|
||||
items: ValueExpression.Literal(DataValue.EmptyTable),
|
||||
valueName: "CurrentValue",
|
||||
indexName: null);
|
||||
|
||||
ForeachExecutor instanceA = new(model, this.State);
|
||||
|
||||
// Run ExecuteAsync (which sets _values = []) followed by one TakeNextAsync (which sets
|
||||
// HasValue = false on an empty source).
|
||||
await this.ExecuteAsync(instanceA, ForeachExecutor.Steps.Next(instanceA.Id), instanceA.TakeNextAsync);
|
||||
Assert.False(instanceA.HasValue);
|
||||
|
||||
// Act — checkpoint and restore into a fresh instance.
|
||||
InMemoryWorkflowContext checkpoint = new();
|
||||
await InvokeOnCheckpointingAsync(instanceA, checkpoint);
|
||||
|
||||
ForeachExecutor instanceB = new(model, this.State);
|
||||
await InvokeOnCheckpointRestoredAsync(instanceB, checkpoint);
|
||||
|
||||
// Assert — restored instance must agree that the source is empty and HasValue is false.
|
||||
Assert.False(instanceB.HasValue);
|
||||
|
||||
await instanceB.TakeNextAsync(checkpoint, _: null, CancellationToken.None);
|
||||
Assert.False(instanceB.HasValue);
|
||||
}
|
||||
|
||||
private void SetVariableState(string valueName, string? indexName = null, FormulaValue? valueState = null)
|
||||
{
|
||||
this.State.Set(valueName, valueState ?? FormulaValue.New("something"));
|
||||
if (indexName is not null)
|
||||
{
|
||||
this.State.Set(indexName, FormulaValue.New(33));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync(
|
||||
string displayName,
|
||||
ValueExpression items,
|
||||
string valueName,
|
||||
string? indexName,
|
||||
bool expectValue = false)
|
||||
{
|
||||
// Arrange
|
||||
Foreach model = this.CreateModel(displayName, items, valueName, indexName);
|
||||
ForeachExecutor action = new(model, this.State);
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events = await this.ExecuteAsync(action, isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
VerifyInvocationEvent(events);
|
||||
|
||||
// IsDiscreteAction should be false for Foreach
|
||||
VerifyIsDiscrete(action, isDiscrete: false);
|
||||
|
||||
// Verify HasValue state after execution
|
||||
Assert.Equal(expectValue, action.HasValue);
|
||||
|
||||
// Verify value was reset at the end
|
||||
this.VerifyUndefined(valueName);
|
||||
|
||||
// Verify index was reset at the end if it was used
|
||||
if (indexName is not null)
|
||||
{
|
||||
this.VerifyUndefined(indexName);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TakeNextTestAsync(
|
||||
string displayName,
|
||||
ValueExpression items,
|
||||
string valueName,
|
||||
string? indexName,
|
||||
bool expectValue = true)
|
||||
{
|
||||
// Arrange
|
||||
Foreach model = this.CreateModel(displayName, items, valueName, indexName);
|
||||
ForeachExecutor action = new(model, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action, ForeachExecutor.Steps.Next(action.Id), action.TakeNextAsync);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
|
||||
// Verify HasValue state after execution
|
||||
Assert.Equal(expectValue, action.HasValue);
|
||||
}
|
||||
|
||||
private async Task CompletedTestAsync(
|
||||
string displayName,
|
||||
string valueName,
|
||||
string? indexName)
|
||||
{
|
||||
// Arrange
|
||||
Foreach model = this.CreateModel(displayName, ValueExpression.Literal(DataValue.EmptyTable), valueName, indexName);
|
||||
ForeachExecutor action = new(model, this.State);
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events = await this.ExecuteAsync(ForeachExecutor.Steps.End(action.Id), action.CompleteAsync);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
VerifyCompletionEvent(events);
|
||||
|
||||
// Verify HasValue state after completion
|
||||
Assert.False(action.HasValue);
|
||||
|
||||
// Verify value was reset at the end
|
||||
this.VerifyUndefined(valueName);
|
||||
|
||||
// Verify index was reset at the end if it was used
|
||||
if (indexName is not null)
|
||||
{
|
||||
this.VerifyUndefined(indexName);
|
||||
}
|
||||
}
|
||||
|
||||
private Foreach CreateModel(
|
||||
string displayName,
|
||||
ValueExpression items,
|
||||
string valueName,
|
||||
string? indexName)
|
||||
{
|
||||
Foreach.Builder actionBuilder = new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
Items = items,
|
||||
Value = PropertyPath.Create(FormatVariablePath(valueName)),
|
||||
};
|
||||
|
||||
if (indexName is not null)
|
||||
{
|
||||
actionBuilder.Index = PropertyPath.Create(FormatVariablePath(indexName));
|
||||
}
|
||||
|
||||
return AssignParent<Foreach>(actionBuilder);
|
||||
}
|
||||
|
||||
// Reflection helpers used to invoke the `protected internal` checkpoint hooks on the executor
|
||||
// base class from this test project (which is in a different assembly than Microsoft.Agents.AI.Workflows
|
||||
// and is not granted InternalsVisibleTo there).
|
||||
private static Task InvokeOnCheckpointingAsync(Executor executor, IWorkflowContext context) =>
|
||||
InvokeProtectedCheckpointHookAsync(executor, context, methodName: "OnCheckpointingAsync");
|
||||
|
||||
private static Task InvokeOnCheckpointRestoredAsync(Executor executor, IWorkflowContext context) =>
|
||||
InvokeProtectedCheckpointHookAsync(executor, context, methodName: "OnCheckpointRestoredAsync");
|
||||
|
||||
private static async Task InvokeProtectedCheckpointHookAsync(Executor executor, IWorkflowContext context, string methodName)
|
||||
{
|
||||
MethodInfo method = typeof(Executor).GetMethod(
|
||||
methodName,
|
||||
BindingFlags.Instance | BindingFlags.NonPublic,
|
||||
binder: null,
|
||||
types: new[] { typeof(IWorkflowContext), typeof(CancellationToken) },
|
||||
modifiers: null) ?? throw new InvalidOperationException($"Could not locate {methodName} on Executor.");
|
||||
|
||||
ValueTask invocation = (ValueTask)method.Invoke(executor, new object[] { context, CancellationToken.None })!;
|
||||
await invocation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal in-memory <see cref="IWorkflowContext"/> implementation used to drive the
|
||||
/// checkpoint/restore overrides on <see cref="ForeachExecutor"/> directly from a unit test.
|
||||
/// Records state writes in a (scope, key) dictionary and serves matching reads back. Only the
|
||||
/// state-related members are exercised by the checkpoint hooks; the other members are stubbed.
|
||||
/// </summary>
|
||||
private sealed class InMemoryWorkflowContext : IWorkflowContext
|
||||
{
|
||||
private readonly Dictionary<(string? scope, string key), object?> _store = [];
|
||||
|
||||
public bool ConcurrentRunsEnabled => false;
|
||||
|
||||
public IReadOnlyDictionary<string, string>? TraceContext => null;
|
||||
|
||||
public ValueTask QueueStateUpdateAsync<T>(string key, T? value, string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._store[(scopeName, key)] = value;
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask<T?> ReadStateAsync<T>(string key, string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._store.TryGetValue((scopeName, key), out object? stored) && stored is T typed)
|
||||
{
|
||||
return new ValueTask<T?>(typed);
|
||||
}
|
||||
|
||||
return new ValueTask<T?>(default(T));
|
||||
}
|
||||
|
||||
public ValueTask<T> ReadOrInitStateAsync<T>(string key, Func<T> initialStateFactory, string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this._store.TryGetValue((scopeName, key), out object? stored) && stored is T typed)
|
||||
{
|
||||
return new ValueTask<T>(typed);
|
||||
}
|
||||
|
||||
T initial = initialStateFactory();
|
||||
this._store[(scopeName, key)] = initial;
|
||||
return new ValueTask<T>(initial);
|
||||
}
|
||||
|
||||
public ValueTask<HashSet<string>> ReadStateKeysAsync(string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
HashSet<string> keys = new(
|
||||
this._store.Keys
|
||||
.Where(slot => string.Equals(slot.scope, scopeName, StringComparison.Ordinal))
|
||||
.Select(slot => slot.key));
|
||||
return new ValueTask<HashSet<string>>(keys);
|
||||
}
|
||||
|
||||
public ValueTask QueueClearScopeAsync(string? scopeName = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach ((string? scope, string key) slot in this._store.Keys.Where(slot => string.Equals(slot.scope, scopeName, StringComparison.Ordinal)).ToArray())
|
||||
{
|
||||
this._store.Remove(slot);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default) => default;
|
||||
|
||||
public ValueTask SendMessageAsync(object message, string? targetId, CancellationToken cancellationToken = default) => default;
|
||||
|
||||
public ValueTask YieldOutputAsync(object output, CancellationToken cancellationToken = default) => default;
|
||||
|
||||
public ValueTask RequestHaltAsync() => default;
|
||||
}
|
||||
}
|
||||
+759
@@ -0,0 +1,759 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="HttpRequestExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class HttpRequestExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
private const string TestUrl = "https://api.example.com/data";
|
||||
|
||||
private readonly Mock<ResponseAgentProvider> _agentProvider = new(MockBehavior.Loose);
|
||||
|
||||
[Fact]
|
||||
public void InvalidModel()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IHttpRequestHandler> mockHandler = new();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<DeclarativeModelException>(() => new HttpRequestExecutor(
|
||||
new HttpRequestAction(),
|
||||
mockHandler.Object,
|
||||
this._agentProvider.Object,
|
||||
this.State));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HttpRequestIsDiscreteAction()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IHttpRequestHandler> mockHandler = new();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestIsDiscreteAction),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get);
|
||||
HttpRequestExecutor action = new(model, mockHandler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act & Assert — IsDiscreteAction should be true for HttpRequest (single-step action).
|
||||
VerifyIsDiscrete(action, isDiscrete: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpGetReturnsJsonObjectAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
const string ResponseVar = "Result";
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpGetReturnsJsonObjectAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
responseVariable: ResponseVar);
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("{\"key\":\"value\",\"number\":42}"));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
Assert.IsType<RecordValue>(this.State.Get(ResponseVar), exactMatch: false);
|
||||
handler.VerifySent(info => info.Method == "GET" && info.Url == TestUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpGetReturnsPlainStringAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
const string ResponseVar = "Result";
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpGetReturnsPlainStringAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
responseVariable: ResponseVar);
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("not-json content"));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
this.VerifyState(ResponseVar, FormulaValue.New("not-json content"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpGetWithEmptyBodyYieldsBlankAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
const string ResponseVar = "Result";
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpGetWithEmptyBodyYieldsBlankAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
responseVariable: ResponseVar);
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult(null));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
this.VerifyUndefined(ResponseVar);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpGetForwardsHeadersAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpGetForwardsHeadersAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
headers: new Dictionary<string, string>
|
||||
{
|
||||
["Authorization"] = "Bearer token",
|
||||
["Accept"] = "application/json",
|
||||
});
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("{}"));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
handler.VerifySent(info =>
|
||||
info.Headers?["Authorization"] == "Bearer token" &&
|
||||
info.Headers?["Accept"] == "application/json");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpPostWithJsonBodyAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpPostWithJsonBodyAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Post,
|
||||
jsonBody: new StringDataValue("hello"));
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("{}"));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
handler.VerifySent(info =>
|
||||
info.Method == "POST" &&
|
||||
info.BodyContentType == "application/json" &&
|
||||
info.Body == "\"hello\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpPostWithRawBodyAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpPostWithRawBodyAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Post,
|
||||
rawBody: "raw body content",
|
||||
rawContentType: "text/plain");
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult(""));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
handler.VerifySent(info =>
|
||||
info.BodyContentType == "text/plain" &&
|
||||
info.Body == "raw body content");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestRaisesOnErrorByDefaultAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestRaisesOnErrorByDefaultAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get);
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("server error", statusCode: 500, isSuccess: false));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<DeclarativeActionException>(() => this.ExecuteAsync(action));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestFailureExceptionTruncatesLongBodyAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestFailureExceptionTruncatesLongBodyAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get);
|
||||
|
||||
string longBody = new('x', 10_000);
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult(longBody, statusCode: 500, isSuccess: false));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
DeclarativeActionException exception =
|
||||
await Assert.ThrowsAsync<DeclarativeActionException>(() => this.ExecuteAsync(action));
|
||||
|
||||
// Assert - message contains status and truncation marker, bounded in length, never the full body.
|
||||
Assert.Contains("500", exception.Message);
|
||||
Assert.Contains("[truncated]", exception.Message);
|
||||
Assert.DoesNotContain(longBody, exception.Message);
|
||||
Assert.True(exception.Message.Length < 512, $"Exception message too long: {exception.Message.Length} chars.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestFailureExceptionOmitsEmptyBodyAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestFailureExceptionOmitsEmptyBodyAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get);
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult(body: null, statusCode: 404, isSuccess: false));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
DeclarativeActionException exception =
|
||||
await Assert.ThrowsAsync<DeclarativeActionException>(() => this.ExecuteAsync(action));
|
||||
|
||||
// Assert - status present, no stray "Body: ''" noise.
|
||||
Assert.Contains("404", exception.Message);
|
||||
Assert.DoesNotContain("Body:", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestFailureExceptionSanitizesControlCharsAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestFailureExceptionSanitizesControlCharsAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get);
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("line1\r\nline2\tend", statusCode: 400, isSuccess: false));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
DeclarativeActionException exception =
|
||||
await Assert.ThrowsAsync<DeclarativeActionException>(() => this.ExecuteAsync(action));
|
||||
|
||||
// Assert - CR/LF/TAB collapsed to spaces so the message stays on one line.
|
||||
Assert.DoesNotContain("\r", exception.Message);
|
||||
Assert.DoesNotContain("\n", exception.Message);
|
||||
Assert.DoesNotContain("\t", exception.Message);
|
||||
Assert.Contains("line1", exception.Message);
|
||||
Assert.Contains("line2", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestPassesTimeoutToHandlerAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestPassesTimeoutToHandlerAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
timeoutMilliseconds: 1500);
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("{}"));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
handler.VerifySent(info =>
|
||||
info.Timeout is not null &&
|
||||
info.Timeout.Value == TimeSpan.FromMilliseconds(1500));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestTimeoutRaisesDeclarativeExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestTimeoutRaisesDeclarativeExceptionAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get);
|
||||
|
||||
MockHttpRequestHandler handler = new(
|
||||
HttpRequestResult("{}"),
|
||||
throwOnSend: new OperationCanceledException());
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<DeclarativeActionException>(() => this.ExecuteAsync(action));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestTransportFailureRaisesDeclarativeExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestTransportFailureRaisesDeclarativeExceptionAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get);
|
||||
|
||||
MockHttpRequestHandler handler = new(
|
||||
HttpRequestResult("{}"),
|
||||
throwOnSend: new InvalidOperationException("transport failure"));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<DeclarativeActionException>(() => this.ExecuteAsync(action));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestStoresResponseHeadersAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
const string HeaderVar = "Headers";
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestStoresResponseHeadersAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
responseHeadersVariable: HeaderVar);
|
||||
|
||||
Dictionary<string, IReadOnlyList<string>> responseHeaders = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["X-Request-Id"] = ["abc-123"],
|
||||
["Set-Cookie"] = ["a=1", "b=2"],
|
||||
};
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("{}", headers: responseHeaders));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
FormulaValue storedHeaders = this.State.Get(HeaderVar);
|
||||
Assert.IsType<RecordValue>(storedHeaders, exactMatch: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestForwardsQueryParametersAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestForwardsQueryParametersAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
queryParameters: new Dictionary<string, DataValue>
|
||||
{
|
||||
["filter"] = StringDataValue.Create("active"),
|
||||
["limit"] = NumberDataValue.Create(10),
|
||||
["includeDeleted"] = BooleanDataValue.Create(false),
|
||||
});
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("{}"));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
handler.VerifySent(info =>
|
||||
info.QueryParameters?.Count == 3 &&
|
||||
info.QueryParameters["filter"] == "active" &&
|
||||
info.QueryParameters["limit"] == "10" &&
|
||||
info.QueryParameters["includeDeleted"] == "false");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestAddsResponseToConversationAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
const string ConversationId = "conv-12345";
|
||||
const string ResponseBody = "response-text";
|
||||
|
||||
this._agentProvider
|
||||
.Setup(p => p.CreateMessageAsync(It.IsAny<string>(), It.IsAny<ChatMessage>(), It.IsAny<CancellationToken>()))
|
||||
.Returns<string, ChatMessage, CancellationToken>((_, message, _) => Task.FromResult(message));
|
||||
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestAddsResponseToConversationAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
conversationId: ConversationId);
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult(ResponseBody));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
this._agentProvider.Verify(
|
||||
p => p.CreateMessageAsync(
|
||||
ConversationId,
|
||||
It.Is<ChatMessage>(m => m.Role == ChatRole.Assistant && m.Text == ResponseBody),
|
||||
It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestWithoutConversationIdSkipsConversationAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestWithoutConversationIdSkipsConversationAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get);
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("response"));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
this._agentProvider.Verify(
|
||||
p => p.CreateMessageAsync(It.IsAny<string>(), It.IsAny<ChatMessage>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestForwardsConnectionNameAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
const string ConnectionName = "my-connection";
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestForwardsConnectionNameAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
connectionName: ConnectionName);
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("{}"));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
handler.VerifySent(info => info.ConnectionName == ConnectionName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestEmptyConversationIdSkipsConversationAsync()
|
||||
{
|
||||
// Arrange - empty-string conversationId should be treated as unset.
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestEmptyConversationIdSkipsConversationAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
conversationId: "");
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("response"));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
this._agentProvider.Verify(
|
||||
p => p.CreateMessageAsync(It.IsAny<string>(), It.IsAny<ChatMessage>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestEmptyResponseBodySkipsConversationAsync()
|
||||
{
|
||||
// Arrange - conversationId set, but empty body should not produce a conversation message.
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestEmptyResponseBodySkipsConversationAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
conversationId: "conv-1");
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult(""));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
this._agentProvider.Verify(
|
||||
p => p.CreateMessageAsync(It.IsAny<string>(), It.IsAny<ChatMessage>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpGetReturnsJsonArrayAsync()
|
||||
{
|
||||
// Arrange - exercises JsonValueKind.Array branch of ParseResponseBody.
|
||||
this.State.InitializeSystem();
|
||||
const string ResponseVar = "Result";
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpGetReturnsJsonArrayAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
responseVariable: ResponseVar);
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("[1, 2, 3]"));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
FormulaValue stored = this.State.Get(ResponseVar);
|
||||
Assert.IsType<TableValue>(stored, exactMatch: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpGetWithEmptyHeaderValueDropsHeaderAsync()
|
||||
{
|
||||
// Arrange - empty header values should be filtered out (matches GetHeaders guard).
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpGetWithEmptyHeaderValueDropsHeaderAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
headers: new Dictionary<string, string>
|
||||
{
|
||||
["X-Trace"] = "trace-1",
|
||||
["X-Empty"] = "",
|
||||
});
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("{}"));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
handler.VerifySent(info =>
|
||||
info.Headers?.ContainsKey("X-Trace") == true &&
|
||||
info.Headers?.ContainsKey("X-Empty") == false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpRequestZeroTimeoutNotForwardedAsync()
|
||||
{
|
||||
// Arrange - non-positive timeouts should not be forwarded (handler default applies).
|
||||
this.State.InitializeSystem();
|
||||
HttpRequestAction model = this.CreateModel(
|
||||
displayName: nameof(HttpRequestZeroTimeoutNotForwardedAsync),
|
||||
url: TestUrl,
|
||||
method: HttpMethodType.Get,
|
||||
timeoutMilliseconds: 0);
|
||||
|
||||
MockHttpRequestHandler handler = new(HttpRequestResult("{}"));
|
||||
HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
handler.VerifySent(info => info.Timeout is null);
|
||||
}
|
||||
|
||||
private static HttpRequestResult HttpRequestResult(
|
||||
string? body,
|
||||
int statusCode = 200,
|
||||
bool isSuccess = true,
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>>? headers = null) =>
|
||||
new()
|
||||
{
|
||||
StatusCode = statusCode,
|
||||
IsSuccessStatusCode = isSuccess,
|
||||
Body = body,
|
||||
Headers = headers,
|
||||
};
|
||||
|
||||
private HttpRequestAction CreateModel(
|
||||
string displayName,
|
||||
string url,
|
||||
HttpMethodType method,
|
||||
string? responseVariable = null,
|
||||
string? responseHeadersVariable = null,
|
||||
IReadOnlyDictionary<string, string>? headers = null,
|
||||
IReadOnlyDictionary<string, DataValue>? queryParameters = null,
|
||||
string? conversationId = null,
|
||||
string? connectionName = null,
|
||||
DataValue? jsonBody = null,
|
||||
string? rawBody = null,
|
||||
string? rawContentType = null,
|
||||
long? timeoutMilliseconds = null,
|
||||
string? continueOnErrorStatusVariable = null,
|
||||
string? continueOnErrorBodyVariable = null)
|
||||
{
|
||||
HttpRequestAction.Builder builder = new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
Url = new StringExpression.Builder(StringExpression.Literal(url)),
|
||||
Method = new EnumExpression<HttpMethodTypeWrapper>.Builder(
|
||||
EnumExpression<HttpMethodTypeWrapper>.Literal(HttpMethodTypeWrapper.Get(method))),
|
||||
};
|
||||
|
||||
if (responseVariable is not null)
|
||||
{
|
||||
builder.Response = PropertyPath.Create(FormatVariablePath(responseVariable));
|
||||
}
|
||||
|
||||
if (responseHeadersVariable is not null)
|
||||
{
|
||||
builder.ResponseHeaders = PropertyPath.Create(FormatVariablePath(responseHeadersVariable));
|
||||
}
|
||||
|
||||
if (headers is not null)
|
||||
{
|
||||
foreach (KeyValuePair<string, string> header in headers)
|
||||
{
|
||||
builder.Headers.Add(header.Key, new StringExpression.Builder(StringExpression.Literal(header.Value)));
|
||||
}
|
||||
}
|
||||
|
||||
if (queryParameters is not null)
|
||||
{
|
||||
foreach (KeyValuePair<string, DataValue> parameter in queryParameters)
|
||||
{
|
||||
builder.QueryParameters.Add(parameter.Key, new ValueExpression.Builder(ValueExpression.Literal(parameter.Value)));
|
||||
}
|
||||
}
|
||||
|
||||
if (conversationId is not null)
|
||||
{
|
||||
builder.ConversationId = new StringExpression.Builder(StringExpression.Literal(conversationId));
|
||||
}
|
||||
|
||||
if (connectionName is not null)
|
||||
{
|
||||
builder.Connection = new RemoteConnection.Builder
|
||||
{
|
||||
Name = new StringExpression.Builder(StringExpression.Literal(connectionName)),
|
||||
};
|
||||
}
|
||||
|
||||
if (jsonBody is not null)
|
||||
{
|
||||
builder.Body = new JsonRequestContent.Builder()
|
||||
{
|
||||
Content = new ValueExpression.Builder(ValueExpression.Literal(jsonBody)),
|
||||
};
|
||||
}
|
||||
else if (rawBody is not null)
|
||||
{
|
||||
RawRequestContent.Builder rawBuilder = new()
|
||||
{
|
||||
Content = new StringExpression.Builder(StringExpression.Literal(rawBody)),
|
||||
};
|
||||
if (rawContentType is not null)
|
||||
{
|
||||
rawBuilder.ContentType = new StringExpression.Builder(StringExpression.Literal(rawContentType));
|
||||
}
|
||||
builder.Body = rawBuilder;
|
||||
}
|
||||
|
||||
if (timeoutMilliseconds is not null)
|
||||
{
|
||||
builder.RequestTimeoutInMilliseconds = new IntExpression.Builder(IntExpression.Literal(timeoutMilliseconds.Value));
|
||||
}
|
||||
|
||||
if (continueOnErrorStatusVariable is not null || continueOnErrorBodyVariable is not null)
|
||||
{
|
||||
ContinueOnErrorBehavior.Builder continueBuilder = new();
|
||||
if (continueOnErrorStatusVariable is not null)
|
||||
{
|
||||
continueBuilder.StatusCode = PropertyPath.Create(FormatVariablePath(continueOnErrorStatusVariable));
|
||||
}
|
||||
if (continueOnErrorBodyVariable is not null)
|
||||
{
|
||||
continueBuilder.ErrorResponseBody = PropertyPath.Create(FormatVariablePath(continueOnErrorBodyVariable));
|
||||
}
|
||||
builder.ErrorHandling = continueBuilder;
|
||||
}
|
||||
|
||||
return AssignParent<HttpRequestAction>(builder);
|
||||
}
|
||||
|
||||
private sealed class MockHttpRequestHandler : Mock<IHttpRequestHandler>
|
||||
{
|
||||
private HttpRequestInfo? _lastRequest;
|
||||
|
||||
public MockHttpRequestHandler(HttpRequestResult result, Exception? throwOnSend = null)
|
||||
{
|
||||
this.Setup(handler => handler.SendAsync(It.IsAny<HttpRequestInfo>(), It.IsAny<CancellationToken>()))
|
||||
.Returns<HttpRequestInfo, CancellationToken>((info, _) =>
|
||||
{
|
||||
this._lastRequest = info;
|
||||
if (throwOnSend is not null)
|
||||
{
|
||||
throw throwOnSend;
|
||||
}
|
||||
return Task.FromResult(result);
|
||||
});
|
||||
}
|
||||
|
||||
public void VerifySent(Func<HttpRequestInfo, bool> predicate)
|
||||
{
|
||||
Assert.NotNull(this._lastRequest);
|
||||
Assert.True(predicate(this._lastRequest!), "Sent HTTP request did not match expected predicate.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="InvokeAzureAgentExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class InvokeAzureAgentExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void InvokeAzureAgentThrowsWhenModelInvalid() =>
|
||||
// Arrange, Act & Assert
|
||||
Assert.Throws<DeclarativeModelException>(() => new InvokeAzureAgentExecutor(new InvokeAzureAgent(), new CapturingAgentProvider("text"), this.State));
|
||||
|
||||
#region Input argument binding
|
||||
|
||||
[Fact]
|
||||
public async Task MultipleNamedArgumentsAreAllBoundAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
CapturingAgentProvider provider = new("acknowledged");
|
||||
InvokeAzureAgent model =
|
||||
this.CreateModel(
|
||||
displayName: nameof(MultipleNamedArgumentsAreAllBoundAsync),
|
||||
agentName: "BrainCombine",
|
||||
arguments:
|
||||
[
|
||||
("a", ValueExpression.Literal(new StringDataValue("alpha"))),
|
||||
("b", ValueExpression.Literal(new StringDataValue("beta"))),
|
||||
]);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(provider.CapturedArguments);
|
||||
Assert.Equal("alpha", provider.CapturedArguments!["a"]);
|
||||
Assert.Equal("beta", provider.CapturedArguments!["b"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RecordValuedArgumentIsBoundAsRecordAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
this.State.Set(
|
||||
"R",
|
||||
FormulaValue.NewRecordFromFields(
|
||||
new NamedValue("a", FormulaValue.New("alpha")),
|
||||
new NamedValue("b", FormulaValue.New("beta"))));
|
||||
CapturingAgentProvider provider = new("acknowledged");
|
||||
InvokeAzureAgent model =
|
||||
this.CreateModel(
|
||||
displayName: nameof(RecordValuedArgumentIsBoundAsRecordAsync),
|
||||
agentName: "BrainTest",
|
||||
arguments:
|
||||
[
|
||||
("input", ValueExpression.Variable(PropertyPath.TopicVariable("R"))),
|
||||
]);
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(provider.CapturedArguments);
|
||||
IDictionary<string, object?> record = Assert.IsAssignableFrom<IDictionary<string, object?>>(provider.CapturedArguments!["input"]);
|
||||
Assert.Equal("alpha", record["a"]);
|
||||
Assert.Equal("beta", record["b"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InlineRecordExpressionArgumentIsBoundAsRecordAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
CapturingAgentProvider provider = new("acknowledged");
|
||||
InvokeAzureAgent model =
|
||||
this.CreateModel(
|
||||
displayName: nameof(InlineRecordExpressionArgumentIsBoundAsRecordAsync),
|
||||
agentName: "BrainInlineRecord",
|
||||
arguments:
|
||||
[
|
||||
("input", ValueExpression.Expression("""{ a: "alpha", b: "beta" }""")),
|
||||
]);
|
||||
|
||||
// Act (reporter repro (b): a single argument whose value is an inline record literal)
|
||||
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(provider.CapturedArguments);
|
||||
IDictionary<string, object?> record = Assert.IsAssignableFrom<IDictionary<string, object?>>(provider.CapturedArguments!["input"]);
|
||||
Assert.Equal("alpha", record["a"]);
|
||||
Assert.Equal("beta", record["b"]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Response object parsing
|
||||
|
||||
[Fact]
|
||||
public async Task JsonObjectOutputAssignsRecordAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
CapturingAgentProvider provider = new("""{ "a": "alpha", "b": "beta" }""");
|
||||
InvokeAzureAgent model =
|
||||
this.CreateModel(
|
||||
displayName: nameof(JsonObjectOutputAssignsRecordAsync),
|
||||
agentName: "BrainObject",
|
||||
responseObjectVariable: "Result");
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
RecordValue record = Assert.IsAssignableFrom<RecordValue>(this.State.Get("Result"));
|
||||
Assert.Equal("alpha", ((StringValue)record.GetField("a")).Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JsonArrayOutputAssignsListAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
CapturingAgentProvider provider = new("""["alpha","beta"]""");
|
||||
InvokeAzureAgent model =
|
||||
this.CreateModel(
|
||||
displayName: nameof(JsonArrayOutputAssignsListAsync),
|
||||
agentName: "BrainArray",
|
||||
responseObjectVariable: "Result");
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
TableValue table = Assert.IsAssignableFrom<TableValue>(this.State.Get("Result"));
|
||||
Assert.Equal(2, table.Rows.Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmptyJsonArrayOutputAssignsEmptyListAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
CapturingAgentProvider provider = new("[]");
|
||||
InvokeAzureAgent model =
|
||||
this.CreateModel(
|
||||
displayName: nameof(EmptyJsonArrayOutputAssignsEmptyListAsync),
|
||||
agentName: "BrainEmptyArray",
|
||||
responseObjectVariable: "Result");
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
TableValue table = Assert.IsAssignableFrom<TableValue>(this.State.Get("Result"));
|
||||
Assert.Empty(table.Rows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task JsonScalarOutputAssignsScalarAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
CapturingAgentProvider provider = new("42");
|
||||
InvokeAzureAgent model =
|
||||
this.CreateModel(
|
||||
displayName: nameof(JsonScalarOutputAssignsScalarAsync),
|
||||
agentName: "BrainScalar",
|
||||
responseObjectVariable: "Result");
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
NumberValue number = Assert.IsAssignableFrom<NumberValue>(this.State.Get("Result"));
|
||||
Assert.Equal(42d, number.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MixedJsonArrayOutputSkipsAssignmentAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
CapturingAgentProvider provider = new("""["alpha",1]""");
|
||||
InvokeAzureAgent model =
|
||||
this.CreateModel(
|
||||
displayName: nameof(MixedJsonArrayOutputSkipsAssignmentAsync),
|
||||
agentName: "BrainMixedArray",
|
||||
responseObjectVariable: "Result");
|
||||
|
||||
// Act (must not throw despite non-convertible JSON)
|
||||
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
this.VerifyUndefined("Result");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NestedJsonArrayOutputSkipsAssignmentAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
CapturingAgentProvider provider = new("[[1,2],[3,4]]");
|
||||
InvokeAzureAgent model =
|
||||
this.CreateModel(
|
||||
displayName: nameof(NestedJsonArrayOutputSkipsAssignmentAsync),
|
||||
agentName: "BrainNestedArray",
|
||||
responseObjectVariable: "Result");
|
||||
|
||||
// Act (a nested array parses but is not convertible to a workflow value — must not throw)
|
||||
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
this.VerifyUndefined("Result");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlainTextOutputSkipsAssignmentAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
CapturingAgentProvider provider = new("hello world");
|
||||
InvokeAzureAgent model =
|
||||
this.CreateModel(
|
||||
displayName: nameof(PlainTextOutputSkipsAssignmentAsync),
|
||||
agentName: "BrainText",
|
||||
responseObjectVariable: "Result");
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
this.VerifyUndefined("Result");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NonObjectJsonOutputWithoutResponseObjectDoesNotThrowAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.InitializeSystem();
|
||||
CapturingAgentProvider provider = new("""["alpha","beta"]""");
|
||||
InvokeAzureAgent model =
|
||||
this.CreateModel(
|
||||
displayName: nameof(NonObjectJsonOutputWithoutResponseObjectDoesNotThrowAsync),
|
||||
agentName: "BrainNoOutput");
|
||||
|
||||
// Act & Assert (reporter repro shape — no output block; must not throw)
|
||||
await this.ExecuteAsync(new InvokeAzureAgentExecutor(model, provider, this.State), isDiscrete: false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
private InvokeAzureAgent CreateModel(
|
||||
string displayName,
|
||||
string agentName,
|
||||
IReadOnlyList<(string Key, ValueExpression Value)>? arguments = null,
|
||||
string? responseObjectVariable = null)
|
||||
{
|
||||
InvokeAzureAgent.Builder builder =
|
||||
new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
Agent =
|
||||
new AzureAgentUsage.Builder
|
||||
{
|
||||
Name = new StringExpression.Builder(StringExpression.Literal(agentName)),
|
||||
},
|
||||
};
|
||||
|
||||
if (arguments is not null)
|
||||
{
|
||||
AzureAgentInput.Builder inputBuilder = new();
|
||||
foreach ((string key, ValueExpression value) in arguments)
|
||||
{
|
||||
inputBuilder.Arguments.Add(key, value);
|
||||
}
|
||||
builder.Input = inputBuilder;
|
||||
}
|
||||
|
||||
if (responseObjectVariable is not null)
|
||||
{
|
||||
builder.Output =
|
||||
new AzureAgentOutput.Builder
|
||||
{
|
||||
AutoSend = new BoolExpression.Builder(BoolExpression.Literal(false)),
|
||||
ResponseObject = new InitializablePropertyPath(PropertyPath.TopicVariable(responseObjectVariable), isInitializer: false),
|
||||
};
|
||||
}
|
||||
|
||||
return AssignParent<InvokeAzureAgent>(builder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal <see cref="ResponseAgentProvider"/> that returns a single configured text response and
|
||||
/// captures the input arguments supplied to <see cref="InvokeAgentAsync"/>.
|
||||
/// </summary>
|
||||
private sealed class CapturingAgentProvider(string responseText) : ResponseAgentProvider
|
||||
{
|
||||
public IDictionary<string, object?>? CapturedArguments { get; private set; }
|
||||
|
||||
public override IAsyncEnumerable<AgentResponseUpdate> InvokeAgentAsync(
|
||||
string agentId,
|
||||
string? agentVersion,
|
||||
string? conversationId,
|
||||
IEnumerable<ChatMessage>? messages,
|
||||
IDictionary<string, object?>? inputArguments,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.CapturedArguments = inputArguments;
|
||||
return YieldAsync(responseText);
|
||||
}
|
||||
|
||||
public override Task<string> CreateConversationAsync(CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(Guid.NewGuid().ToString("N"));
|
||||
|
||||
public override Task<ChatMessage> CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(conversationMessage);
|
||||
|
||||
public override Task<ChatMessage> GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public override async IAsyncEnumerable<ChatMessage> GetMessagesAsync(
|
||||
string conversationId,
|
||||
int? limit = null,
|
||||
string? after = null,
|
||||
string? before = null,
|
||||
bool newestFirst = false,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
yield break;
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<AgentResponseUpdate> YieldAsync(string text)
|
||||
{
|
||||
yield return new AgentResponseUpdate(ChatRole.Assistant, text);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
+1418
File diff suppressed because it is too large
Load Diff
+1852
File diff suppressed because it is too large
Load Diff
+120
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
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 } },
|
||||
}
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
this.FormatDisplayName(nameof(ParseRecordAsync)),
|
||||
recordBuilder,
|
||||
@"{ ""key1"": ""val1"" }",
|
||||
FormulaValue.NewRecordFromFields(new NamedValue("key1", FormulaValue.New("val1"))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseTableAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
this.FormatDisplayName(nameof(ParseTableAsync)),
|
||||
DataType.EmptyTable,
|
||||
@"[""apple"",""banana"",""cat""]",
|
||||
FormulaValue.NewSingleColumnTable(FormulaValue.New("apple"), FormulaValue.New("banana"), FormulaValue.New("cat")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseBooleanAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
this.FormatDisplayName(nameof(ParseBooleanAsync)),
|
||||
new BooleanDataType.Builder(),
|
||||
"True",
|
||||
FormulaValue.New(true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseNumberAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
this.FormatDisplayName(nameof(ParseNumberAsync)),
|
||||
new NumberDataType.Builder(),
|
||||
"42",
|
||||
FormulaValue.New(42));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseStringAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
this.FormatDisplayName(nameof(ParseStringAsync)),
|
||||
new StringDataType.Builder(),
|
||||
"Hello, World!",
|
||||
FormulaValue.New("Hello, World!"));
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync(
|
||||
string displayName,
|
||||
DataType.Builder dataBuilder,
|
||||
string sourceText,
|
||||
FormulaValue expectedValue)
|
||||
{
|
||||
ParseValue model =
|
||||
this.CreateModel(
|
||||
displayName,
|
||||
"Target",
|
||||
dataBuilder,
|
||||
sourceText);
|
||||
|
||||
// Act
|
||||
ParseValueExecutor action = new(model, this.State);
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
this.VerifyState("Target", expectedValue);
|
||||
}
|
||||
|
||||
private ParseValue CreateModel(
|
||||
string displayName,
|
||||
string variableName,
|
||||
DataType.Builder typeBuilder,
|
||||
string sourceText)
|
||||
{
|
||||
ParseValue.Builder actionBuilder =
|
||||
new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
ValueType = typeBuilder,
|
||||
Variable = PropertyPath.TopicVariable(variableName),
|
||||
Value = new ValueExpression.Builder(ValueExpression.Literal(StringDataValue.Create(sourceText))),
|
||||
};
|
||||
|
||||
return AssignParent<ParseValue>(actionBuilder);
|
||||
}
|
||||
}
|
||||
+501
@@ -0,0 +1,501 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="QuestionExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class QuestionExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void QuestionNamingConvention()
|
||||
{
|
||||
// Arrange
|
||||
string testId = this.CreateActionId().Value;
|
||||
|
||||
// Act
|
||||
string prepareStep = QuestionExecutor.Steps.Prepare(testId);
|
||||
string inputStep = QuestionExecutor.Steps.Input(testId);
|
||||
string captureStep = QuestionExecutor.Steps.Capture(testId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal($"{testId}_{nameof(QuestionExecutor.Steps.Prepare)}", prepareStep);
|
||||
Assert.Equal($"{testId}_{nameof(QuestionExecutor.Steps.Input)}", inputStep);
|
||||
Assert.Equal($"{testId}_{nameof(QuestionExecutor.Steps.Capture)}", captureStep);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, false)]
|
||||
[InlineData("anything", false)]
|
||||
[InlineData(null, true)]
|
||||
public void QuestionIsComplete(object? result, bool expectIsComplete)
|
||||
{
|
||||
// Arrange - "Complete" result corresponds to null value
|
||||
ActionExecutorResult executorResult = new(nameof(QuestionIsComplete), result);
|
||||
|
||||
// Act
|
||||
bool isComplete = QuestionExecutor.IsComplete(executorResult);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectIsComplete, isComplete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QuestionExecuteWithResultUndefinedAsync()
|
||||
{
|
||||
// Arrange
|
||||
Question model = this.CreateModel(
|
||||
displayName: nameof(QuestionExecuteWithResultUndefinedAsync),
|
||||
"TestVariable");
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(model, expectPrompt: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QuestionExecuteWithAlwaysPromptAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set("TestVariable", FormulaValue.New("existing-value"));
|
||||
Question model = this.CreateModel(
|
||||
displayName: nameof(QuestionExecuteWithAlwaysPromptAsync),
|
||||
"TestVariable",
|
||||
alwaysPrompt: true);
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(model, expectPrompt: true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SkipQuestionMode.AlwaysSkipIfVariableHasValue)]
|
||||
[InlineData(SkipQuestionMode.SkipOnFirstExecutionIfVariableHasValue)]
|
||||
[InlineData(SkipQuestionMode.AlwaysAsk)]
|
||||
public async Task QuestionExecuteWithSkipModeAsyncWithResultUndefinedAsync(SkipQuestionMode skipMode)
|
||||
{
|
||||
// Arrange
|
||||
Question model = this.CreateModel(
|
||||
displayName: nameof(QuestionExecuteWithSkipModeAsyncWithResultUndefinedAsync),
|
||||
variableName: "TestVariable",
|
||||
skipMode: skipMode);
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(model, expectPrompt: true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SkipQuestionMode.AlwaysSkipIfVariableHasValue, false)]
|
||||
[InlineData(SkipQuestionMode.SkipOnFirstExecutionIfVariableHasValue, false)]
|
||||
[InlineData(SkipQuestionMode.AlwaysAsk, true)]
|
||||
public async Task QuestionExecuteWithSkipModeAsyncWithResultDefinedAsync(SkipQuestionMode skipMode, bool expectPrompt)
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set("TestVariable", FormulaValue.New("existing-value"));
|
||||
Question model = this.CreateModel(
|
||||
displayName: nameof(QuestionExecuteWithSkipModeAsyncWithResultDefinedAsync),
|
||||
variableName: "TestVariable",
|
||||
skipMode: skipMode);
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(model, expectPrompt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QuestionPrepareResponseAsync()
|
||||
{
|
||||
// Arrange
|
||||
Question model = this.CreateModel(
|
||||
displayName: nameof(QuestionPrepareResponseAsync),
|
||||
variableName: "TestVariable",
|
||||
promptText: "Provide input:");
|
||||
|
||||
// Act & Assert
|
||||
await this.PrepareResponseTestAsync(model, expectedPrompt: "Provide input:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QuestionCaptureResponseWithValidEntityAsync()
|
||||
{
|
||||
// Arrange
|
||||
Question model = this.CreateModel(
|
||||
displayName: nameof(QuestionCaptureResponseWithValidEntityAsync),
|
||||
variableName: "TestVariable",
|
||||
alwaysPrompt: true,
|
||||
skipMode: SkipQuestionMode.AlwaysAsk,
|
||||
entity: new NumberPrebuiltEntity());
|
||||
|
||||
// Act & Assert
|
||||
await this.CaptureResponseTestAsync(
|
||||
model,
|
||||
variableName: "TestVariable",
|
||||
responseText: "42",
|
||||
expectAutoSend: true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("Invalid input, please try again.")]
|
||||
public async Task QuestionCaptureResponseWithInvalidEntityAsync(string? invalidResponse)
|
||||
{
|
||||
// Arrange
|
||||
Question model = this.CreateModel(
|
||||
displayName: nameof(QuestionCaptureResponseWithInvalidEntityAsync),
|
||||
variableName: "TestVariable",
|
||||
invalidResponseText: invalidResponse,
|
||||
entity: new NumberPrebuiltEntity());
|
||||
|
||||
// Act & Assert
|
||||
await this.CaptureResponseTestAsync(
|
||||
model,
|
||||
variableName: "TestVariable",
|
||||
responseText: "not-a-number",
|
||||
expectResponse: false);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("Invalid input, please try again.")]
|
||||
public async Task QuestionCaptureResponseWithUnrecognizedResponseAsync(string? unrecognizedResponse)
|
||||
{
|
||||
// Arrange
|
||||
Question model = this.CreateModel(
|
||||
displayName: nameof(QuestionCaptureResponseWithUnrecognizedResponseAsync),
|
||||
variableName: "TestVariable",
|
||||
unrecognizedResponseText: unrecognizedResponse);
|
||||
|
||||
// Act & Assert
|
||||
await this.CaptureResponseTestAsync(
|
||||
model,
|
||||
variableName: "TestVariable",
|
||||
responseText: null,
|
||||
expectResponse: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QuestionCaptureResponseWithUnsupportedPromptAsync()
|
||||
{
|
||||
// Arrange
|
||||
Question.Builder actionBuilder = new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(nameof(QuestionCaptureResponseWithUnsupportedPromptAsync)),
|
||||
Variable = PropertyPath.Create(FormatVariablePath("TestVariable")),
|
||||
Prompt = new UnknownActivityTemplateBase.Builder(),
|
||||
UnrecognizedPrompt = new UnknownActivityTemplateBase.Builder(),
|
||||
Entity = new StringPrebuiltEntity(),
|
||||
};
|
||||
|
||||
Question model = actionBuilder.Build();
|
||||
|
||||
// Act & Assert
|
||||
await this.CaptureResponseTestAsync(
|
||||
model,
|
||||
variableName: "TestVariable",
|
||||
responseText: null,
|
||||
expectResponse: false);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task QuestionCaptureResponseExceedingRepeatCountAsync(bool hasDefault)
|
||||
{
|
||||
// Arrange
|
||||
Question model = this.CreateModel(
|
||||
displayName: nameof(QuestionCaptureResponseExceedingRepeatCountAsync),
|
||||
variableName: "TestVariable",
|
||||
repeatCount: 0,
|
||||
defaultValue: hasDefault ? new NumberDataValue(0) : null,
|
||||
entity: new NumberPrebuiltEntity());
|
||||
|
||||
// Act & Assert
|
||||
await this.CaptureResponseTestAsync(
|
||||
model,
|
||||
variableName: "TestVariable",
|
||||
responseText: "not-a-number",
|
||||
expectResponse: false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QuestionCaptureResponseWithAutoSendFalseAsync()
|
||||
{
|
||||
// Arrange
|
||||
Question model = this.CreateModel(
|
||||
displayName: nameof(QuestionCaptureResponseWithAutoSendFalseAsync),
|
||||
variableName: "TestVariable",
|
||||
autoSend: new BooleanDataValue(false));
|
||||
|
||||
// Act & Assert
|
||||
await this.CaptureResponseTestAsync(
|
||||
model,
|
||||
variableName: "TestVariable",
|
||||
responseText: "test response");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QuestionCaptureResponseWithAutoSendTrueAsync()
|
||||
{
|
||||
// Arrange
|
||||
Question model = this.CreateModel(
|
||||
displayName: nameof(QuestionCaptureResponseWithAutoSendTrueAsync),
|
||||
variableName: "TestVariable",
|
||||
autoSend: new BooleanDataValue(true));
|
||||
|
||||
// Act & Assert
|
||||
await this.CaptureResponseTestAsync(
|
||||
model,
|
||||
variableName: "TestVariable",
|
||||
responseText: "test response",
|
||||
expectAutoSend: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QuestionCaptureResponseWithAutoSendInvalidAsync()
|
||||
{
|
||||
// Arrange
|
||||
Question model = this.CreateModel(
|
||||
displayName: nameof(QuestionCaptureResponseWithAutoSendInvalidAsync),
|
||||
variableName: "TestVariable",
|
||||
autoSend: new NumberDataValue(33));
|
||||
|
||||
// Act & Assert
|
||||
await this.CaptureResponseTestAsync(
|
||||
model,
|
||||
variableName: "TestVariable",
|
||||
responseText: "test response");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QuestionCompleteAsync()
|
||||
{
|
||||
// Arrange
|
||||
Question model =
|
||||
this.CreateModel(
|
||||
displayName: nameof(QuestionCompleteAsync),
|
||||
variableName: "TestVariable");
|
||||
|
||||
// Act & Assert
|
||||
await this.CompleteTestAsync(model);
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync(Question model, bool expectPrompt)
|
||||
{
|
||||
// Arrange
|
||||
bool? sentMessage = null;
|
||||
Mock<ResponseAgentProvider> mockProvider = new(MockBehavior.Loose);
|
||||
QuestionExecutor action = new(model, mockProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events =
|
||||
await this.ExecuteAsync(
|
||||
action,
|
||||
QuestionExecutor.Steps.Capture(action.Id),
|
||||
CaptureResultAsync);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
VerifyInvocationEvent(events);
|
||||
Assert.NotNull(sentMessage);
|
||||
Assert.Equal(expectPrompt, sentMessage);
|
||||
|
||||
ValueTask CaptureResultAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken)
|
||||
{
|
||||
Assert.Null(sentMessage); // Should only be called once
|
||||
sentMessage = message.Result is not null;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PrepareResponseTestAsync(
|
||||
Question model,
|
||||
string expectedPrompt)
|
||||
{
|
||||
// Arrange
|
||||
Mock<ResponseAgentProvider> mockProvider = new(MockBehavior.Loose);
|
||||
QuestionExecutor action = new(model, mockProvider.Object, this.State);
|
||||
string? capturedPrompt = null;
|
||||
|
||||
// Act
|
||||
await this.ExecuteAsync(
|
||||
[
|
||||
action,
|
||||
new DelegateActionExecutor(
|
||||
QuestionExecutor.Steps.Prepare(action.Id),
|
||||
this.State,
|
||||
action.PrepareResponseAsync),
|
||||
new DelegateActionExecutor<ExternalInputRequest>(
|
||||
QuestionExecutor.Steps.Capture(action.Id),
|
||||
this.State,
|
||||
CaptureExternalRequestAsync)
|
||||
],
|
||||
isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
Assert.NotNull(capturedPrompt);
|
||||
Assert.Equal(expectedPrompt, capturedPrompt);
|
||||
|
||||
ValueTask CaptureExternalRequestAsync(IWorkflowContext context, ExternalInputRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
Assert.Null(capturedPrompt);
|
||||
capturedPrompt = request.AgentResponse.Text;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CaptureResponseTestAsync(
|
||||
Question model,
|
||||
string variableName,
|
||||
string? responseText,
|
||||
bool expectResponse = true,
|
||||
bool expectAutoSend = false)
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set(SystemScope.Names.ConversationId, FormulaValue.New("ExternalConversationId"), VariableScopeNames.System);
|
||||
|
||||
Mock<ResponseAgentProvider> mockProvider = new(MockBehavior.Loose);
|
||||
mockProvider
|
||||
.Setup(p => p.CreateMessageAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<ChatMessage>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((string cid, ChatMessage msg, CancellationToken ct) => msg);
|
||||
|
||||
QuestionExecutor action = new(model, mockProvider.Object, this.State);
|
||||
ExternalInputResponse response = responseText is not null
|
||||
? new ExternalInputResponse(new ChatMessage(ChatRole.User, responseText))
|
||||
: new ExternalInputResponse([]);
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events = await this.ExecuteAsync(
|
||||
action,
|
||||
QuestionExecutor.Steps.Capture(action.Id),
|
||||
(context, message, cancellationToken) =>
|
||||
action.CaptureResponseAsync(context, response, cancellationToken));
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
|
||||
if (expectResponse)
|
||||
{
|
||||
// Variable should be set with the extracted value
|
||||
FormulaValue actualValue = this.State.Get(variableName);
|
||||
Assert.Equal(responseText, actualValue.Format());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Should have prompted again or sent unrecognized/invalid message
|
||||
Assert.Contains(events, e => e is MessageActivityEvent);
|
||||
}
|
||||
|
||||
if (expectAutoSend)
|
||||
{
|
||||
this.VerifyState(SystemScope.Names.LastMessageText, VariableScopeNames.System, FormulaValue.New(responseText ?? string.Empty));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.VerifyUndefined(SystemScope.Names.LastMessageText, VariableScopeNames.System);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CompleteTestAsync(Question model)
|
||||
{
|
||||
// Arrange
|
||||
Mock<ResponseAgentProvider> mockProvider = new(MockBehavior.Loose);
|
||||
QuestionExecutor action = new(model, mockProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events = await this.ExecuteAsync(
|
||||
QuestionExecutor.Steps.Input(action.Id),
|
||||
action.CompleteAsync);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
VerifyCompletionEvent(events);
|
||||
}
|
||||
|
||||
private Question CreateModel(
|
||||
string displayName,
|
||||
string variableName,
|
||||
string promptText = "Please provide a value",
|
||||
string? invalidResponseText = null,
|
||||
string? unrecognizedResponseText = null,
|
||||
string? defaultValueResponseText = null,
|
||||
DataValue? defaultValue = null,
|
||||
bool? alwaysPrompt = null,
|
||||
SkipQuestionMode? skipMode = null,
|
||||
int? repeatCount = null,
|
||||
EntityReference? entity = null,
|
||||
DataValue? autoSend = null)
|
||||
{
|
||||
BoolExpression.Builder? alwaysPromptExpression = null;
|
||||
if (alwaysPrompt is not null)
|
||||
{
|
||||
alwaysPromptExpression = BoolExpression.Literal(alwaysPrompt.Value).ToBuilder();
|
||||
}
|
||||
|
||||
IntExpression.Builder? repeatCountExpression = null;
|
||||
if (repeatCount is not null)
|
||||
{
|
||||
repeatCountExpression = IntExpression.Literal(repeatCount.Value).ToBuilder();
|
||||
}
|
||||
|
||||
ValueExpression.Builder? defaultValueExpression = null;
|
||||
if (defaultValue is not null)
|
||||
{
|
||||
defaultValueExpression = ValueExpression.Literal(defaultValue).ToBuilder();
|
||||
}
|
||||
|
||||
EnumExpression<SkipQuestionModeWrapper>.Builder? skipModeExpression = null;
|
||||
if (skipMode is not null)
|
||||
{
|
||||
skipModeExpression = EnumExpression<SkipQuestionModeWrapper>.Literal(skipMode).ToBuilder();
|
||||
}
|
||||
|
||||
Question.Builder actionBuilder = new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
AlwaysPrompt = alwaysPromptExpression,
|
||||
SkipQuestionMode = skipModeExpression,
|
||||
Variable = PropertyPath.Create(FormatVariablePath(variableName)),
|
||||
Prompt = CreateMessageActivity(promptText),
|
||||
InvalidPrompt = CreateOptionalMessageActivity(invalidResponseText),
|
||||
UnrecognizedPrompt = CreateOptionalMessageActivity(unrecognizedResponseText),
|
||||
DefaultValue = defaultValueExpression,
|
||||
DefaultValueResponse = CreateOptionalMessageActivity(defaultValueResponseText),
|
||||
RepeatCount = repeatCountExpression,
|
||||
Entity = entity ?? new StringPrebuiltEntity(),
|
||||
};
|
||||
|
||||
if (autoSend is not null)
|
||||
{
|
||||
RecordDataValue.Builder extensionDataBuilder = new();
|
||||
extensionDataBuilder.Properties.Add("autoSend", autoSend);
|
||||
actionBuilder.ExtensionData = extensionDataBuilder.Build();
|
||||
}
|
||||
|
||||
return AssignParent<Question>(actionBuilder);
|
||||
}
|
||||
|
||||
private static MessageActivityTemplate.Builder? CreateOptionalMessageActivity(string? text) =>
|
||||
text is null ? null : CreateMessageActivity(text);
|
||||
|
||||
private static MessageActivityTemplate.Builder CreateMessageActivity(string text) =>
|
||||
new()
|
||||
{
|
||||
Text = { TemplateLine.Parse(text) },
|
||||
};
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Events;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.ObjectModel;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="RequestExternalInputExecutor"/>.
|
||||
/// </summary>
|
||||
public sealed class RequestExternalInputExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output)
|
||||
{
|
||||
[Fact]
|
||||
public void RequestExternalInputNamingConvention()
|
||||
{
|
||||
// Arrange
|
||||
string testId = this.CreateActionId().Value;
|
||||
|
||||
// Act
|
||||
string inputStep = RequestExternalInputExecutor.Steps.Input(testId);
|
||||
string captureStep = RequestExternalInputExecutor.Steps.Capture(testId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal($"{testId}_{nameof(RequestExternalInputExecutor.Steps.Input)}", inputStep);
|
||||
Assert.Equal($"{testId}_{nameof(RequestExternalInputExecutor.Steps.Capture)}", captureStep);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteRequestsExternalInputAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(ExecuteRequestsExternalInputAsync),
|
||||
variableName: "TestVariable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CaptureResponseWithVariableAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.CaptureResponseTestAsync(
|
||||
displayName: nameof(CaptureResponseWithVariableAsync),
|
||||
variableName: "TestVariable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CaptureResponseWithoutVariableAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.CaptureResponseTestAsync(
|
||||
displayName: nameof(CaptureResponseWithoutVariableAsync),
|
||||
variableName: null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CaptureResponseWithMultipleMessagesAsync()
|
||||
{
|
||||
// Arrange, Act & Assert
|
||||
await this.CaptureResponseTestAsync(
|
||||
displayName: nameof(CaptureResponseWithMultipleMessagesAsync),
|
||||
variableName: "TestVariable",
|
||||
messageCount: 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CaptureResponseWithWorkflowConversationAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set(SystemScope.Names.ConversationId, FormulaValue.New("WorkflowConversationId"), VariableScopeNames.System);
|
||||
|
||||
// Act & Assert
|
||||
await this.CaptureResponseTestAsync(
|
||||
displayName: nameof(CaptureResponseWithWorkflowConversationAsync),
|
||||
variableName: "TestVariable",
|
||||
messageCount: 2,
|
||||
expectMessagesCreated: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CaptureResponseWithEmptyMessagesAsync()
|
||||
{
|
||||
await this.CaptureResponseTestAsync(
|
||||
displayName: nameof(CaptureResponseWithEmptyMessagesAsync),
|
||||
variableName: "TestVariable",
|
||||
messageCount: 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CaptureResponseWithEmptyMessagesAndWorkflowConversationAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set(SystemScope.Names.ConversationId, FormulaValue.New("WorkflowConversationId"), VariableScopeNames.System);
|
||||
|
||||
// Act & Assert
|
||||
await this.CaptureResponseTestAsync(
|
||||
displayName: nameof(CaptureResponseWithEmptyMessagesAndWorkflowConversationAsync),
|
||||
variableName: "TestVariable",
|
||||
messageCount: 0,
|
||||
expectMessagesCreated: false);
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync(
|
||||
string displayName,
|
||||
string variableName)
|
||||
{
|
||||
MockAgentProvider mockAgentProvider = new();
|
||||
RequestExternalInput model = this.CreateModel(displayName, variableName);
|
||||
RequestExternalInputExecutor action = new(model, mockAgentProvider.Object, this.State);
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events = await this.ExecuteAsync(action, isDiscrete: false);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
VerifyInvocationEvent(events);
|
||||
}
|
||||
|
||||
private async Task CaptureResponseTestAsync(
|
||||
string displayName,
|
||||
string? variableName = null,
|
||||
int messageCount = 1,
|
||||
bool expectMessagesCreated = false)
|
||||
{
|
||||
// Arrange
|
||||
RequestExternalInput model = this.CreateModel(displayName, variableName);
|
||||
MockAgentProvider mockAgentProvider = new();
|
||||
RequestExternalInputExecutor action = new(model, mockAgentProvider.Object, this.State);
|
||||
|
||||
// Create test messages
|
||||
List<ChatMessage> testMessages = [];
|
||||
for (int i = 0; i < messageCount; i++)
|
||||
{
|
||||
testMessages.Add(new ChatMessage(ChatRole.User, $"Test message {i + 1}"));
|
||||
}
|
||||
|
||||
ExternalInputResponse response = new(testMessages);
|
||||
|
||||
// Act
|
||||
WorkflowEvent[] events =
|
||||
await this.ExecuteAsync(
|
||||
RequestExternalInputExecutor.Steps.Capture(action.Id),
|
||||
(context, message, cancellationToken) => action.CaptureResponseAsync(context, response, cancellationToken));
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
VerifyCompletionEvent(events);
|
||||
|
||||
// Verify messages were created in the workflow conversation if expected
|
||||
mockAgentProvider.Verify(p => p.CreateMessageAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<ChatMessage>(),
|
||||
It.IsAny<CancellationToken>()), Times.Exactly(expectMessagesCreated ? messageCount : 0));
|
||||
|
||||
// Verify the variable was set correctly
|
||||
if (variableName is not null)
|
||||
{
|
||||
this.VerifyState(variableName, testMessages.ToTable());
|
||||
}
|
||||
}
|
||||
|
||||
private RequestExternalInput CreateModel(string displayName, string? variablePath)
|
||||
{
|
||||
RequestExternalInput.Builder actionBuilder =
|
||||
new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
Variable = variablePath is null ? null : (InitializablePropertyPath?)PropertyPath.Create(FormatVariablePath(variablePath)),
|
||||
};
|
||||
|
||||
return AssignParent<RequestExternalInput>(actionBuilder);
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
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 = PropertyPath.Create(variablePath),
|
||||
};
|
||||
|
||||
return AssignParent<ResetVariable>(actionBuilder);
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
// 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.Agents.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
// 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.Agents.ObjectModel;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
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);
|
||||
|
||||
// The executor must also emit an AgentResponseEvent carrying the activity text
|
||||
// so workflow consumers (hosting runtime, UIs) can surface it as an agent turn.
|
||||
AgentResponseEvent agentEvent = Assert.Single(events.OfType<AgentResponseEvent>());
|
||||
Assert.Equal(action.Id, agentEvent.ExecutorId);
|
||||
ChatMessage message = Assert.Single(agentEvent.Response.Messages);
|
||||
Assert.Equal(ChatRole.Assistant, message.Role);
|
||||
Assert.Equal("Test activity message", message.Text);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
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"));
|
||||
|
||||
// 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"));
|
||||
|
||||
// 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 SetMultipleVariablesWithNullVariableAsync()
|
||||
{
|
||||
// Arrange, Act, Assert
|
||||
await this.ExecuteTestAsync(
|
||||
displayName: nameof(SetMultipleVariablesWithNullVariableAsync),
|
||||
assignments: [
|
||||
new AssignmentCase("NullVar1", null, FormulaValue.NewBlank()),
|
||||
new AssignmentCase(null, 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.Where(a => a.VariableName != null))
|
||||
{
|
||||
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}")
|
||||
};
|
||||
|
||||
InitializablePropertyPath? variablePath = null;
|
||||
if (assignment.VariableName != null)
|
||||
{
|
||||
variablePath = PropertyPath.Create(FormatVariablePath(assignment.VariableName));
|
||||
}
|
||||
|
||||
actionBuilder.Assignments.Add(new VariableAssignment.Builder()
|
||||
{
|
||||
Variable = variablePath,
|
||||
Value = valueExpressionBuilder,
|
||||
});
|
||||
}
|
||||
|
||||
return AssignParent<SetMultipleVariables>(actionBuilder);
|
||||
}
|
||||
|
||||
private sealed record AssignmentCase(string? VariableName, object? ValueExpression, FormulaValue ExpectedValue);
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
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, Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
this.FormatDisplayName(nameof(SetLiteralValueAsync)),
|
||||
"TextVar",
|
||||
"New value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateExistingValueAsync()
|
||||
{
|
||||
// Arrange
|
||||
this.State.Set("TextVar", FormulaValue.New("Old value"));
|
||||
|
||||
// Act & Assert
|
||||
await this.ExecuteTestAsync(
|
||||
this.FormatDisplayName(nameof(UpdateExistingValueAsync)),
|
||||
"TextVar",
|
||||
"New value");
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync(
|
||||
string displayName,
|
||||
string variableName,
|
||||
string textValue)
|
||||
{
|
||||
// Arrange
|
||||
SetTextVariable model =
|
||||
this.CreateModel(
|
||||
displayName,
|
||||
variableName,
|
||||
textValue);
|
||||
|
||||
// Act
|
||||
SetTextVariableExecutor action = new(model, this.State);
|
||||
await this.ExecuteAsync(action);
|
||||
|
||||
// Assert
|
||||
VerifyModel(model, action);
|
||||
this.VerifyState(variableName, FormulaValue.New(textValue));
|
||||
}
|
||||
|
||||
private SetTextVariable CreateModel(string displayName, string variablePath, string textValue)
|
||||
{
|
||||
SetTextVariable.Builder actionBuilder =
|
||||
new()
|
||||
{
|
||||
Id = this.CreateActionId(),
|
||||
DisplayName = this.FormatDisplayName(displayName),
|
||||
Variable = PropertyPath.Create(FormatVariablePath(variablePath)),
|
||||
Value = TemplateLine.Parse(textValue),
|
||||
};
|
||||
|
||||
return AssignParent<SetTextVariable>(actionBuilder);
|
||||
}
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
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));
|
||||
|
||||
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));
|
||||
|
||||
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"));
|
||||
|
||||
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 = PropertyPath.Create(variablePath),
|
||||
Value = valueExpression,
|
||||
};
|
||||
|
||||
return AssignParent<SetVariable>(actionBuilder);
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
// 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.Kit;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
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 Task<WorkflowEvent[]> ExecuteAsync(string actionId, DelegateAction<ActionExecutorResult> executorAction) =>
|
||||
this.ExecuteAsync([new DelegateActionExecutor(actionId, this.State, executorAction)], isDiscrete: false);
|
||||
|
||||
internal Task<WorkflowEvent[]> ExecuteAsync(Executor executor, string actionId, DelegateAction<ActionExecutorResult> executorAction) =>
|
||||
this.ExecuteAsync([executor, new DelegateActionExecutor(actionId, this.State, executorAction)], isDiscrete: false);
|
||||
|
||||
internal async Task<WorkflowEvent[]> ExecuteAsync(DeclarativeActionExecutor executor, bool isDiscrete = true)
|
||||
{
|
||||
VerifyIsDiscrete(executor, isDiscrete);
|
||||
return await this.ExecuteAsync([executor], isDiscrete);
|
||||
}
|
||||
|
||||
internal async Task<WorkflowEvent[]> ExecuteAsync(Executor[] executors, bool isDiscrete)
|
||||
{
|
||||
this.State.Bind();
|
||||
|
||||
TestWorkflowExecutor workflowExecutor = new();
|
||||
WorkflowBuilder workflowBuilder = new(workflowExecutor);
|
||||
Executor prevExecutor = workflowExecutor;
|
||||
foreach (Executor executor in executors)
|
||||
{
|
||||
workflowBuilder.AddEdge(prevExecutor, executor);
|
||||
prevExecutor = executor;
|
||||
}
|
||||
|
||||
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflowBuilder.Build(), this.State);
|
||||
WorkflowEvent[] events = await run.WatchStreamAsync().ToArrayAsync();
|
||||
|
||||
if (isDiscrete)
|
||||
{
|
||||
VerifyInvocationEvent(events);
|
||||
VerifyCompletionEvent(events);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
internal static void VerifyIsDiscrete(DeclarativeActionExecutor action, bool isDiscrete = true)
|
||||
{
|
||||
Assert.Equal(
|
||||
isDiscrete,
|
||||
action.GetType().BaseType?
|
||||
.GetProperty("IsDiscreteAction", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?
|
||||
.GetValue(action));
|
||||
}
|
||||
|
||||
protected static void VerifyInvocationEvent(WorkflowEvent[] events) =>
|
||||
Assert.Contains(events, e => e is DeclarativeActionInvokedEvent);
|
||||
|
||||
protected static void VerifyCompletionEvent(WorkflowEvent[] events) =>
|
||||
Assert.Contains(events, e => e is DeclarativeActionCompletedEvent);
|
||||
|
||||
protected void VerifyState(string variableName, FormulaValue expectedValue) => this.VerifyState(variableName, WorkflowFormulaState.DefaultScopeName, expectedValue);
|
||||
|
||||
protected void VerifyState(string variableName, string scopeName, FormulaValue expectedValue)
|
||||
{
|
||||
FormulaValue actualValue = this.State.Get(variableName, scopeName);
|
||||
Assert.Equal(expectedValue.Format(), actualValue.Format());
|
||||
}
|
||||
|
||||
protected 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")
|
||||
{
|
||||
[SendsMessage(typeof(ActionExecutorResult))]
|
||||
public override async ValueTask HandleAsync(WorkflowFormulaState message, IWorkflowContext context, CancellationToken cancellationToken) =>
|
||||
await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
+68
@@ -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.MessageContent.Fields.Type));
|
||||
Assert.Equal(TypeSchema.MessageContent.ContentTypes.Text, contentType.Value);
|
||||
|
||||
StringValue contentValue = Assert.IsType<StringValue>(rows[0].GetField(TypeSchema.MessageContent.Fields.Value));
|
||||
Assert.Equal(Text, contentValue.Value);
|
||||
}
|
||||
}
|
||||
+113
@@ -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);
|
||||
}
|
||||
}
|
||||
+68
@@ -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.MessageContent.Fields.Type));
|
||||
Assert.Equal(TypeSchema.MessageContent.ContentTypes.Text, contentType.Value);
|
||||
|
||||
StringValue contentValue = Assert.IsType<StringValue>(rows[0].GetField(TypeSchema.MessageContent.Fields.Value));
|
||||
Assert.Equal(Text, contentValue.Value);
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.PowerFx;
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.PowerFx;
|
||||
|
||||
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;
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+551
@@ -0,0 +1,551 @@
|
||||
// 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.Agents.ObjectModel;
|
||||
using Microsoft.Agents.ObjectModel.Abstractions;
|
||||
using Microsoft.Agents.ObjectModel.Exceptions;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.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,72 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
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,49 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
|
||||
using Microsoft.Agents.ObjectModel;
|
||||
|
||||
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}";
|
||||
}
|
||||
+119
@@ -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, ResponseAgentProvider 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);
|
||||
}
|
||||
}
|
||||
+15
@@ -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}
|
||||
+4
@@ -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
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
kind: ToolDialog
|
||||
beginDialog:
|
||||
kind: OnActivity
|
||||
id: my_workflow
|
||||
type: Message
|
||||
actions:
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
+94
@@ -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);
|
||||
}
|
||||
}
|
||||
+13
@@ -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!
|
||||
+29
@@ -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
|
||||
+85
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: my_workflow
|
||||
actions:
|
||||
|
||||
- kind: ClearAllVariables
|
||||
id: clear_all
|
||||
variables: ConversationScopedVariables
|
||||
|
||||
+201
@@ -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);
|
||||
}
|
||||
}
|
||||
+32
@@ -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!
|
||||
+193
@@ -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);
|
||||
}
|
||||
}
|
||||
+29
@@ -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!
|
||||
+25
@@ -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!
|
||||
+95
@@ -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, ResponseAgentProvider 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);
|
||||
}
|
||||
}
|
||||
+12
@@ -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?")]
|
||||
|
||||
+88
@@ -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, ResponseAgentProvider 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);
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: workflow_test
|
||||
actions:
|
||||
|
||||
- kind: CreateConversation
|
||||
id: conversation_create
|
||||
conversationId: Local.PrivateConversationId
|
||||
+87
@@ -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);
|
||||
}
|
||||
}
|
||||
+17
@@ -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}
|
||||
+87
@@ -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);
|
||||
}
|
||||
}
|
||||
+18
@@ -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}
|
||||
+94
@@ -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);
|
||||
}
|
||||
}
|
||||
+13
@@ -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!
|
||||
+94
@@ -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);
|
||||
}
|
||||
}
|
||||
+13
@@ -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
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: my_workflow
|
||||
actions:
|
||||
|
||||
- kind: HttpRequestAction
|
||||
id: http_request
|
||||
method: GET
|
||||
url: =Concatenate("https://api.example.test/items/", System.LastMessageText)
|
||||
headers:
|
||||
Accept: application/json
|
||||
response: Local.HttpResult
|
||||
responseHeaders: Local.HttpHeaders
|
||||
+112
@@ -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, ResponseAgentProvider 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);
|
||||
}
|
||||
}
|
||||
+14
@@ -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)]
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
// <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("Count", UnassignedValue.Instance, "Local").ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync("LoopIndex", UnassignedValue.Instance, "Local").ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync("LoopValue", UnassignedValue.Instance, "Local").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loops over a list assignign the loop variable to "Local.LoopValue" variable.
|
||||
/// </summary>
|
||||
internal sealed class ForeachLoopExecutor(FormulaSession session) : ActionExecutor(id: "foreach_loop", session)
|
||||
{
|
||||
private int _index;
|
||||
private object[] _values = [];
|
||||
|
||||
public bool HasValue { get; private set; }
|
||||
|
||||
// <inheritdoc />
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
this._index = 0;
|
||||
object? evaluatedValue = await context.EvaluateValueAsync<object>("""["a", "b", "c", "d", "e", "f"]""").ConfigureAwait(false);
|
||||
|
||||
if (evaluatedValue == null)
|
||||
{
|
||||
this._values = [];
|
||||
this.HasValue = false;
|
||||
}
|
||||
else
|
||||
if (evaluatedValue is IEnumerable evaluatedList)
|
||||
{
|
||||
this._values = [.. evaluatedList];
|
||||
}
|
||||
else
|
||||
{
|
||||
this._values = [evaluatedValue];
|
||||
}
|
||||
|
||||
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public async ValueTask TakeNextAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.HasValue = this._index < this._values.Length)
|
||||
{
|
||||
object value = this._values[this._index];
|
||||
|
||||
await context.QueueStateUpdateAsync(key: "LoopValue", value: value, scopeName: "Local").ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync(key: "LoopIndex", value: this._index, scopeName: "Local").ConfigureAwait(false);
|
||||
|
||||
this._index++;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask CompleteAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
|
||||
{
|
||||
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask ResetAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await context.QueueStateUpdateAsync(key: "LoopValue", value: UnassignedValue.Instance, scopeName: "Local").ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync(key: "LoopIndex", value: UnassignedValue.Instance, scopeName: "Local").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns an evaluated expression, other variable, or literal value to the "Local.Count" variable.
|
||||
/// </summary>
|
||||
internal sealed class SetVariableInnerExecutor(FormulaSession session) : ActionExecutor(id: "set_variable_inner", session)
|
||||
{
|
||||
// <inheritdoc />
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
object? evaluatedValue = await context.EvaluateValueAsync<object>("Local.Count + 1").ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync(key: "Count", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a message template and sends an activity event.
|
||||
/// </summary>
|
||||
internal sealed class SendActivityInnerExecutor(FormulaSession session) : ActionExecutor(id: "send_activity_inner", session)
|
||||
{
|
||||
// <inheritdoc />
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
string activityText =
|
||||
await context.FormatTemplateAsync(
|
||||
"""
|
||||
x{Local.Count} - {Local.LoopIndex}:{Local.LoopValue}
|
||||
"""
|
||||
);
|
||||
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);
|
||||
ForeachLoopExecutor foreachLoop = new(myWorkflowRoot.Session);
|
||||
DelegateExecutor foreachLoopNext = new(id: "foreach_loop_Next", myWorkflowRoot.Session, foreachLoop.TakeNextAsync);
|
||||
DelegateExecutor foreachLoopPost = new(id: "foreach_loop_Post", myWorkflowRoot.Session);
|
||||
DelegateExecutor foreachLoopStart = new(id: "foreach_loop_Start", myWorkflowRoot.Session);
|
||||
DelegateExecutor breakLoopNow = new(id: "break_loop_now", myWorkflowRoot.Session);
|
||||
DelegateExecutor breakLoopNowRestart = new(id: "break_loop_now_Restart", myWorkflowRoot.Session);
|
||||
SetVariableInnerExecutor setVariableInner = new(myWorkflowRoot.Session);
|
||||
SendActivityInnerExecutor sendActivityInner = new(myWorkflowRoot.Session);
|
||||
DelegateExecutor endAll = new(id: "end_all", myWorkflowRoot.Session);
|
||||
DelegateExecutor foreachLoopEnd = new(id: "foreach_loop_End", myWorkflowRoot.Session, foreachLoop.CompleteAsync);
|
||||
|
||||
// Define the workflow builder
|
||||
WorkflowBuilder builder = new(myWorkflowRoot);
|
||||
|
||||
// Connect executors
|
||||
builder.AddEdge(myWorkflowRoot, myWorkflow);
|
||||
builder.AddEdge(myWorkflow, foreachLoop);
|
||||
builder.AddEdge(foreachLoop, foreachLoopNext);
|
||||
builder.AddEdge(foreachLoopNext, foreachLoopPost, (object? result) => !foreachLoop.HasValue);
|
||||
builder.AddEdge(foreachLoopNext, foreachLoopStart, (object? result) => foreachLoop.HasValue);
|
||||
builder.AddEdge(foreachLoopStart, breakLoopNow);
|
||||
builder.AddEdge(breakLoopNow, foreachLoopPost);
|
||||
builder.AddEdge(breakLoopNowRestart, setVariableInner);
|
||||
builder.AddEdge(setVariableInner, sendActivityInner);
|
||||
builder.AddEdge(foreachLoopPost, endAll);
|
||||
builder.AddEdge(sendActivityInner, foreachLoopEnd);
|
||||
builder.AddEdge(foreachLoopEnd, foreachLoopNext);
|
||||
|
||||
// Build the workflow
|
||||
return builder.Build(validateOrphans: false);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: my_workflow
|
||||
actions:
|
||||
|
||||
- kind: Foreach
|
||||
id: foreach_loop
|
||||
items: =["a", "b", "c", "d", "e", "f"]
|
||||
index: Local.LoopIndex
|
||||
value: Local.LoopValue
|
||||
actions:
|
||||
|
||||
- kind: BreakLoop
|
||||
id: break_loop_now
|
||||
|
||||
- kind: SetVariable
|
||||
id: set_variable_inner
|
||||
variable: Local.Count
|
||||
value: =Local.Count + 1
|
||||
|
||||
- kind: SendActivity
|
||||
id: send_activity_inner
|
||||
activity: x{Local.Count} - {Local.LoopIndex}:{Local.LoopValue}
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
// <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("Count", UnassignedValue.Instance, "Local").ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync("LoopIndex", UnassignedValue.Instance, "Local").ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync("LoopValue", UnassignedValue.Instance, "Local").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loops over a list assignign the loop variable to "Local.LoopValue" variable.
|
||||
/// </summary>
|
||||
internal sealed class ForeachLoopExecutor(FormulaSession session) : ActionExecutor(id: "foreach_loop", session)
|
||||
{
|
||||
private int _index;
|
||||
private object[] _values = [];
|
||||
|
||||
public bool HasValue { get; private set; }
|
||||
|
||||
// <inheritdoc />
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
this._index = 0;
|
||||
object? evaluatedValue = await context.EvaluateValueAsync<object>("""["a", "b", "c", "d", "e", "f"]""").ConfigureAwait(false);
|
||||
|
||||
if (evaluatedValue == null)
|
||||
{
|
||||
this._values = [];
|
||||
this.HasValue = false;
|
||||
}
|
||||
else
|
||||
if (evaluatedValue is IEnumerable evaluatedList)
|
||||
{
|
||||
this._values = [.. evaluatedList];
|
||||
}
|
||||
else
|
||||
{
|
||||
this._values = [evaluatedValue];
|
||||
}
|
||||
|
||||
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public async ValueTask TakeNextAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.HasValue = this._index < this._values.Length)
|
||||
{
|
||||
object value = this._values[this._index];
|
||||
|
||||
await context.QueueStateUpdateAsync(key: "LoopValue", value: value, scopeName: "Local").ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync(key: "LoopIndex", value: this._index, scopeName: "Local").ConfigureAwait(false);
|
||||
|
||||
this._index++;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask CompleteAsync(IWorkflowContext context, object? _, CancellationToken cancellationToken)
|
||||
{
|
||||
await this.ResetAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask ResetAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await context.QueueStateUpdateAsync(key: "LoopValue", value: UnassignedValue.Instance, scopeName: "Local").ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync(key: "LoopIndex", value: UnassignedValue.Instance, scopeName: "Local").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns an evaluated expression, other variable, or literal value to the "Local.Count" variable.
|
||||
/// </summary>
|
||||
internal sealed class SetVariableInnerExecutor(FormulaSession session) : ActionExecutor(id: "set_variable_inner", session)
|
||||
{
|
||||
// <inheritdoc />
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
object? evaluatedValue = await context.EvaluateValueAsync<object>("Local.Count + 1").ConfigureAwait(false);
|
||||
await context.QueueStateUpdateAsync(key: "Count", value: evaluatedValue, scopeName: "Local").ConfigureAwait(false);
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a message template and sends an activity event.
|
||||
/// </summary>
|
||||
internal sealed class SendActivityInnerExecutor(FormulaSession session) : ActionExecutor(id: "send_activity_inner", session)
|
||||
{
|
||||
// <inheritdoc />
|
||||
protected override async ValueTask<object?> ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
string activityText =
|
||||
await context.FormatTemplateAsync(
|
||||
"""
|
||||
x{Local.Count} - {Local.LoopIndex}:{Local.LoopValue}
|
||||
"""
|
||||
);
|
||||
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);
|
||||
ForeachLoopExecutor foreachLoop = new(myWorkflowRoot.Session);
|
||||
DelegateExecutor foreachLoopNext = new(id: "foreach_loop_Next", myWorkflowRoot.Session, foreachLoop.TakeNextAsync);
|
||||
DelegateExecutor foreachLoopPost = new(id: "foreach_loop_Post", myWorkflowRoot.Session);
|
||||
DelegateExecutor foreachLoopStart = new(id: "foreach_loop_Start", myWorkflowRoot.Session);
|
||||
DelegateExecutor continueLoopNow = new(id: "continue_loop_now", myWorkflowRoot.Session);
|
||||
DelegateExecutor continueLoopNowRestart = new(id: "continue_loop_now_Restart", myWorkflowRoot.Session);
|
||||
SetVariableInnerExecutor setVariableInner = new(myWorkflowRoot.Session);
|
||||
SendActivityInnerExecutor sendActivityInner = new(myWorkflowRoot.Session);
|
||||
DelegateExecutor endAll = new(id: "end_all", myWorkflowRoot.Session);
|
||||
DelegateExecutor foreachLoopEnd = new(id: "foreach_loop_End", myWorkflowRoot.Session, foreachLoop.CompleteAsync);
|
||||
|
||||
// Define the workflow builder
|
||||
WorkflowBuilder builder = new(myWorkflowRoot);
|
||||
|
||||
// Connect executors
|
||||
builder.AddEdge(myWorkflowRoot, myWorkflow);
|
||||
builder.AddEdge(myWorkflow, foreachLoop);
|
||||
builder.AddEdge(foreachLoop, foreachLoopNext);
|
||||
builder.AddEdge(foreachLoopNext, foreachLoopPost, (object? result) => !foreachLoop.HasValue);
|
||||
builder.AddEdge(foreachLoopNext, foreachLoopStart, (object? result) => foreachLoop.HasValue);
|
||||
builder.AddEdge(foreachLoopStart, continueLoopNow);
|
||||
builder.AddEdge(continueLoopNow, foreachLoopStart);
|
||||
builder.AddEdge(continueLoopNowRestart, setVariableInner);
|
||||
builder.AddEdge(setVariableInner, sendActivityInner);
|
||||
builder.AddEdge(foreachLoopPost, endAll);
|
||||
builder.AddEdge(sendActivityInner, foreachLoopEnd);
|
||||
builder.AddEdge(foreachLoopEnd, foreachLoopNext);
|
||||
|
||||
// Build the workflow
|
||||
return builder.Build(validateOrphans: false);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: my_workflow
|
||||
actions:
|
||||
|
||||
- kind: Foreach
|
||||
id: foreach_loop
|
||||
items: =["a", "b", "c", "d", "e", "f"]
|
||||
index: Local.LoopIndex
|
||||
value: Local.LoopValue
|
||||
actions:
|
||||
|
||||
- kind: ContinueLoop
|
||||
id: continue_loop_now
|
||||
|
||||
- kind: SetVariable
|
||||
id: set_variable_inner
|
||||
variable: Local.Count
|
||||
value: =Local.Count + 1
|
||||
|
||||
- kind: SendActivity
|
||||
id: send_activity_inner
|
||||
activity: x{Local.Count} - {Local.LoopIndex}:{Local.LoopValue}
|
||||
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user