// 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; /// /// Tests for . /// public sealed class InvokeAzureAgentExecutorTest(ITestOutputHelper output) : WorkflowActionExecutorTest(output) { [Fact] public void InvokeAzureAgentThrowsWhenModelInvalid() => // Arrange, Act & Assert Assert.Throws(() => 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 record = Assert.IsAssignableFrom>(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 record = Assert.IsAssignableFrom>(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(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(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(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(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(builder); } /// /// Minimal that returns a single configured text response and /// captures the input arguments supplied to . /// private sealed class CapturingAgentProvider(string responseText) : ResponseAgentProvider { public IDictionary? CapturedArguments { get; private set; } public override IAsyncEnumerable InvokeAgentAsync( string agentId, string? agentVersion, string? conversationId, IEnumerable? messages, IDictionary? inputArguments, CancellationToken cancellationToken = default) { this.CapturedArguments = inputArguments; return YieldAsync(responseText); } public override Task CreateConversationAsync(CancellationToken cancellationToken = default) => Task.FromResult(Guid.NewGuid().ToString("N")); public override Task CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default) => Task.FromResult(conversationMessage); public override Task GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public override async IAsyncEnumerable 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 YieldAsync(string text) { yield return new AgentResponseUpdate(ChatRole.Assistant, text); await Task.CompletedTask.ConfigureAwait(false); } } #endregion }