From 39598741e45d667c0e1f91c3097bb7baacee5555 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:36:18 -0800 Subject: [PATCH] .NET Workflows - Support "structured inputs" feature for declarative workflows (#2053) --- dotnet/agent-framework-dotnet.slnx | 1 + .../Declarative/ConfirmInput/Program.cs | 2 +- .../Declarative/DeepResearch/Program.cs | 2 +- .../Declarative/FunctionTools/Program.cs | 2 +- .../InputArguments/InputArguments.csproj | 39 +++++ .../InputArguments/InputArguments.yaml | 97 ++++++++++++ .../Declarative/InputArguments/Program.cs | 147 ++++++++++++++++++ .../Declarative/Marketing/Program.cs | 2 +- .../Declarative/StudentTeacher/Program.cs | 2 +- .../Declarative/ToolApproval/Program.cs | 2 +- .../AzureAgentProvider.cs | 18 ++- .../Extensions/AgentProviderExtensions.cs | 3 +- .../Extensions/FormulaValueExtensions.cs | 35 +++-- .../Kit/AgentExecutor.cs | 2 +- .../ObjectModel/InvokeAzureAgentExecutor.cs | 21 ++- .../ObjectModel/SetVariableExecutor.cs | 7 +- .../PowerFx/Functions/AgentMessage.cs | 15 ++ .../PowerFx/Functions/MessageFunction.cs | 36 +++++ .../PowerFx/Functions/MessageText.cs | 54 +++++++ .../PowerFx/Functions/UserMessage.cs | 29 +--- .../PowerFx/RecalcEngineFactory.cs | 4 + .../WorkflowAgentProvider.cs | 9 +- .../Agents/AgentProvider.cs | 4 +- .../Agents/PoemAgentProvider.cs | 43 +++++ .../DeclarativeWorkflowTest.cs | 7 +- .../Testcases/InputArguments.json | 23 +++ .../Testcases/MathChat.json | 2 +- .../Workflows/InputArguments.yaml | 15 ++ .../PowerFx/Functions/AgentMessageTests.cs | 68 ++++++++ .../PowerFx/Functions/MessageTextTests.cs | 113 ++++++++++++++ .../PowerFx/Functions/UserMessageTests.cs | 5 +- workflow-samples/DeepResearch.yaml | 12 +- workflow-samples/MathChat.yaml | 2 +- workflow-samples/README.md | 2 +- 34 files changed, 749 insertions(+), 76 deletions(-) create mode 100644 dotnet/samples/GettingStarted/Workflows/Declarative/InputArguments/InputArguments.csproj create mode 100644 dotnet/samples/GettingStarted/Workflows/Declarative/InputArguments/InputArguments.yaml create mode 100644 dotnet/samples/GettingStarted/Workflows/Declarative/InputArguments/Program.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/AgentMessage.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/MessageFunction.cs create mode 100644 dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/MessageText.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/PoemAgentProvider.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/InputArguments.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/InputArguments.yaml create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/Functions/AgentMessageTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/Functions/MessageTextTests.cs diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 2c1e84208c..8f0f34093b 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -116,6 +116,7 @@ + diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/ConfirmInput/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/ConfirmInput/Program.cs index 5ecda10049..2117bf8b07 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/ConfirmInput/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/ConfirmInput/Program.cs @@ -10,7 +10,7 @@ namespace Demo.Workflows.Declarative.ConfirmInput; /// and confirm it matches the original input. /// /// -/// See the README.md file in the parent folder (../Declarative/README.md) for detailed +/// See the README.md file in the parent folder (../README.md) for detailed /// information the configuration required to run this sample. /// internal sealed class Program diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/Program.cs index 0d1368d7ac..b3a7be9171 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/DeepResearch/Program.cs @@ -14,7 +14,7 @@ namespace Demo.Workflows.Declarative.DeepResearch; /// using the Magentic orchestration pattern developed by AutoGen. /// /// -/// See the README.md file in the parent folder (../Declarative/README.md) for detailed +/// See the README.md file in the parent folder (../README.md) for detailed /// information the configuration required to run this sample. /// internal sealed class Program diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/Program.cs index 170f9b08a0..2549312b95 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/FunctionTools/Program.cs @@ -15,7 +15,7 @@ namespace Demo.Workflows.Declarative.FunctionTools; /// with function tools assigned. Exits the loop when the user enters "exit". /// /// -/// See the README.md file in the parent folder (../Declarative/README.md) for detailed +/// See the README.md file in the parent folder (../README.md) for detailed /// information the configuration required to run this sample. /// internal sealed class Program diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/InputArguments/InputArguments.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/InputArguments/InputArguments.csproj new file mode 100644 index 0000000000..af04305c91 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/InputArguments/InputArguments.csproj @@ -0,0 +1,39 @@ + + + + Exe + net9.0 + net9.0 + $(ProjectsDebugTargetFrameworks) + enable + enable + + + + true + true + true + true + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/InputArguments/InputArguments.yaml b/dotnet/samples/GettingStarted/Workflows/Declarative/InputArguments/InputArguments.yaml new file mode 100644 index 0000000000..3f602d0e7b --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/InputArguments/InputArguments.yaml @@ -0,0 +1,97 @@ +# +# This workflow demonstrates providing input arguments to an agent. +# +# Example input: +# I'd like to go on vacation. +# +kind: Workflow +trigger: + + kind: OnConversationStart + id: workflow_demo + actions: + + # Capture the original user message for input to the location-aware agent + - kind: SetVariable + id: set_count_increment + variable: Local.InputMessage + value: =System.LastMessage + + # Invoke the triage agent to determine location requirements + - kind: InvokeAzureAgent + id: solicit_input + conversationId: =System.ConversationId + agent: + name: LocationTriageAgent + input: + messages: =Local.ActionMessage + output: + messages: Local.TriageResponse + + # Request input from the user based on the triage response + - kind: RequestExternalInput + id: request_requirements + variable: Local.NextInput + + # Capture the most recent interaction for evaluation + - kind: SetTextVariable + id: set_status_message + variable: Local.LocationStatusInput + value: |- + AGENT - {MessageText(Local.TriageResponse)} + + USER - {MessageText(Local.NextInput)} + + # Evaluate the status of the location triage + - kind: InvokeAzureAgent + id: evaluate_location + agent: + name: LocationCaptureAgent + input: + messages: =UserMessage(Local.LocationStatusInput) + output: + responseObject: Local.LocationResponse + + # Determine if the location information is complete + - kind: ConditionGroup + id: check_completion + conditions: + + - condition: |- + =Local.LocationResponse.is_location_defined = false Or + Local.LocationResponse.is_location_confirmed = false + id: check_done + actions: + + # Capture the action message for input to the triage agent + - kind: SetVariable + id: set_next_message + variable: Local.ActionMessage + value: =AgentMessage(Local.LocationResponse.action) + + - kind: GotoAction + id: goto_solicit_input + actionId: solicit_input + + elseActions: + + # Create a new conversation so the prior context does not interfere + - kind: CreateConversation + id: conversation_location + conversationId: Local.LocationConversationId + + # Invoke the location-aware agent with the location argument + # and loop until the user types "EXIT" + - kind: InvokeAzureAgent + id: location_response + conversationId: =Local.LocationConversationId + agent: + name: LocationAwareAgent + input: + messages: =Local.InputMessage + arguments: + location: =Local.LocationResponse.place + externalLoop: + when: =Upper(System.LastMessage.Text) <> "EXIT" + output: + autoSend: true diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/InputArguments/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/InputArguments/Program.cs new file mode 100644 index 0000000000..ff12ccd874 --- /dev/null +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/InputArguments/Program.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.Agents; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using OpenAI.Responses; +using Shared.Foundry; +using Shared.Workflows; + +namespace Demo.Workflows.Declarative.InputArguments; + +/// +/// Demonstrate a workflow that consumes input arguments to dynamically enhance the agent +/// instructions. Exits the loop when the user enters "exit". +/// +/// +/// See the README.md file in the parent folder (../README.md) for detailed +/// information the configuration required to run this sample. +/// +internal sealed class Program +{ + public static async Task Main(string[] args) + { + // Initialize configuration + IConfiguration configuration = Application.InitializeConfig(); + Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint)); + + // Ensure sample agents exist in Foundry. + await CreateAgentAsync(foundryEndpoint, configuration); + + // Get input from command line or console + string workflowInput = Application.GetInput(args); + + // Create the workflow factory. This class demonstrates how to initialize a + // declarative workflow from a YAML file. Once the workflow is created, it + // can be executed just like any regular workflow. + WorkflowFactory workflowFactory = new("InputArguments.yaml", foundryEndpoint); + + // Execute the workflow: The WorkflowRunner demonstrates how to execute + // a workflow, handle the workflow events, and providing external input. + // This also includes the ability to checkpoint workflow state and how to + // resume execution. + WorkflowRunner runner = new(); + await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput); + } + + private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration) + { + AgentClient agentsClient = new(foundryEndpoint, new AzureCliCredential()); + + await agentsClient.CreateAgentAsync( + agentName: "LocationTriageAgent", + agentDefinition: DefineLocationTriageAgent(configuration), + agentDescription: "Chats with the user to solicit a location of interest."); + + await agentsClient.CreateAgentAsync( + agentName: "LocationCaptureAgent", + agentDefinition: DefineLocationCaptureAgent(configuration), + agentDescription: "Evaluate the status of soliciting the location."); + + await agentsClient.CreateAgentAsync( + agentName: "LocationAwareAgent", + agentDefinition: DefineLocationAwareAgent(configuration), + agentDescription: "Chats with the user with location awareness."); + } + + private static PromptAgentDefinition DefineLocationTriageAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelMini)) + { + Instructions = + """ + Your only job is to solicit a location from the user. + + Always repeat back the location when addressing the user, except when it is not known. + """ + }; + + private static PromptAgentDefinition DefineLocationCaptureAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelMini)) + { + Instructions = + """ + Request a location from the user. This location could be their own location + or perhaps a location they are interested in. + + City level precision is sufficient. + + If extrapolating region and country, confirm you have it right. + """, + TextOptions = + new ResponseTextOptions + { + TextFormat = + ResponseTextFormat.CreateJsonSchemaFormat( + "TaskEvaluation", + BinaryData.FromString( + """ + { + "type": "object", + "properties": { + "place": { + "type": "string", + "description": "Captures only your understanding of the location specified by the user without explanation, or 'unknown' if not yet defined." + }, + "action": { + "type": "string", + "description": "The instruction for the next action to take regarding the need for additional detail or confirmation." + }, + "is_location_defined": { + "type": "boolean", + "description": "True if the user location is understood." + }, + "is_location_confirmed": { + "type": "boolean", + "description": "True if the user location is confirmed. An unambiguous location may be implicitly confirmed without explicit user confirmation." + } + }, + "required": ["place", "action", "is_location_defined", "is_location_confirmed"], + "additionalProperties": false + } + """), + jsonSchemaFormatDescription: null, + jsonSchemaIsStrict: true), + } + }; + + private static PromptAgentDefinition DefineLocationAwareAgent(IConfiguration configuration) => + new(configuration.GetValue(Application.Settings.FoundryModelMini)) + { + // Parameterized instructions reference the "location" input argument. + Instructions = + """ + Talk to the user about their request. + Their request is related to a specific location: {{location}}. + """, + StructuredInputs = + { + ["location"] = + new StructuredInputDefinition + { + IsRequired = false, + DefaultValue = BinaryData.FromString(@"""unknown"""), + Description = "The user's location", + } + } + }; +} diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/Marketing/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/Marketing/Program.cs index 20987c6bfe..edeb82419b 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/Marketing/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/Marketing/Program.cs @@ -13,7 +13,7 @@ namespace Demo.Workflows.Declarative.Marketing; /// sequentially engaging in a task. /// /// -/// See the README.md file in the parent folder (../Declarative/README.md) for detailed +/// See the README.md file in the parent folder (../README.md) for detailed /// information the configuration required to run this sample. /// internal sealed class Program diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs index d2299add5f..bbe1c526d5 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/StudentTeacher/Program.cs @@ -13,7 +13,7 @@ namespace Demo.Workflows.Declarative.StudentTeacher; /// in an iterative conversation. /// /// -/// See the README.md file in the parent folder (../Declarative/README.md) for detailed +/// See the README.md file in the parent folder (../README.md) for detailed /// information the configuration required to run this sample. /// internal sealed class Program diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/Program.cs index 01be1abfd5..736edadf8d 100644 --- a/dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/Program.cs +++ b/dotnet/samples/GettingStarted/Workflows/Declarative/ToolApproval/Program.cs @@ -14,7 +14,7 @@ namespace Demo.Workflows.Declarative.ToolApproval; /// has an MCP tool that requires approval. Exits the loop when the user enters "exit". /// /// -/// See the README.md file in the parent folder (../Declarative/README.md) for detailed +/// See the README.md file in the parent folder (../README.md) for detailed /// information the configuration required to run this sample. /// internal sealed class Program diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/AzureAgentProvider.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/AzureAgentProvider.cs index 79906fb3f5..de50e16922 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/AzureAgentProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/AzureAgentProvider.cs @@ -7,10 +7,12 @@ using System.Collections.ObjectModel; using System.Linq; using System.Net.Http; using System.Runtime.CompilerServices; +using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; using Azure.AI.Agents; using Azure.Core; +using Microsoft.Agents.AI.Workflows.Declarative.Extensions; using Microsoft.Extensions.AI; using OpenAI.Responses; @@ -32,6 +34,11 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj private AgentClient? _agentClient; private ConversationClient? _conversationClient; + /// + /// Optional options used when creating the . + /// + public AgentClientOptions? ClientOptions { get; init; } + /// public override async Task CreateConversationAsync(CancellationToken cancellationToken = default) { @@ -78,6 +85,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj string? agentVersion, string? conversationId, IEnumerable? messages, + IDictionary? inputArguments, [EnumeratorCancellation] CancellationToken cancellationToken = default) { AgentVersion agentVersionResult = await this.QueryAgentAsync(agentId, agentVersion, cancellationToken).ConfigureAwait(false); @@ -90,6 +98,14 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj AllowMultipleToolCalls = this.AllowMultipleToolCalls, }; + if (inputArguments is not null) + { + JsonNode jsonNode = inputArguments.ToFormula().ToJson(); + ResponseCreationOptions responseCreationOptions = new(); + responseCreationOptions.SetStructuredInputs(BinaryData.FromString(jsonNode.ToJsonString())); + chatOptions.RawRepresentationFactory = (_) => responseCreationOptions; + } + ChatClientAgentRunOptions runOptions = new(chatOptions); IAsyncEnumerable agentResponse = @@ -206,7 +222,7 @@ public sealed class AzureAgentProvider(Uri projectEndpoint, TokenCredential proj { if (this._agentClient is null) { - AgentClientOptions clientOptions = new(); + AgentClientOptions clientOptions = this.ClientOptions ?? new(); if (httpClient is not null) { diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/AgentProviderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/AgentProviderExtensions.cs index 170d5a52a9..037665e8b8 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/AgentProviderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/AgentProviderExtensions.cs @@ -17,9 +17,10 @@ internal static class AgentProviderExtensions string? conversationId, bool autoSend, IEnumerable? inputMessages = null, + IDictionary? inputArguments = null, CancellationToken cancellationToken = default) { - IAsyncEnumerable agentUpdates = agentProvider.InvokeAgentAsync(agentName, null, conversationId, inputMessages, cancellationToken); + IAsyncEnumerable agentUpdates = agentProvider.InvokeAgentAsync(agentName, null, conversationId, inputMessages, inputArguments, cancellationToken); // Enable "autoSend" behavior if this is the workflow conversation. bool isWorkflowConversation = context.IsWorkflowConversation(conversationId, out string? workflowConversationId); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/FormulaValueExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/FormulaValueExtensions.cs index 108dca7682..3e397f3e87 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/FormulaValueExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/FormulaValueExtensions.cs @@ -163,6 +163,24 @@ internal static class FormulaValueExtensions } } } + + public static JsonNode ToJson(this FormulaValue value) => + value switch + { + BooleanValue booleanValue => JsonValue.Create(booleanValue.Value), + DecimalValue decimalValue => JsonValue.Create(decimalValue.Value), + NumberValue numberValue => JsonValue.Create(numberValue.Value), + DateValue dateValue => JsonValue.Create(dateValue.GetConvertedValue(TimeZoneInfo.Utc)), + DateTimeValue datetimeValue => JsonValue.Create(datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)), + TimeValue timeValue => JsonValue.Create($"{timeValue.Value}"), + StringValue stringValue => JsonValue.Create(stringValue.Value), + GuidValue guidValue => JsonValue.Create(guidValue.Value), + RecordValue recordValue => recordValue.ToJson(), + TableValue tableValue => tableValue.ToJson(), + BlankValue => JsonValue.Create(string.Empty), + _ => $"[{value.GetType().Name}]", + }; + public static RecordValue ToRecord(this Dictionary value) => FormulaValue.NewRecordFromFields( value.Select( @@ -256,23 +274,6 @@ internal static class FormulaValueExtensions private static KeyValuePair GetKeyValuePair(this NamedValue value) => new(value.Name, value.Value.ToDataValue()); - private static JsonNode ToJson(this FormulaValue value) => - value switch - { - BooleanValue booleanValue => JsonValue.Create(booleanValue.Value), - DecimalValue decimalValue => JsonValue.Create(decimalValue.Value), - NumberValue numberValue => JsonValue.Create(numberValue.Value), - DateValue dateValue => JsonValue.Create(dateValue.GetConvertedValue(TimeZoneInfo.Utc)), - DateTimeValue datetimeValue => JsonValue.Create(datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)), - TimeValue timeValue => JsonValue.Create($"{timeValue.Value}"), - StringValue stringValue => JsonValue.Create(stringValue.Value), - GuidValue guidValue => JsonValue.Create(guidValue.Value), - RecordValue recordValue => recordValue.ToJson(), - TableValue tableValue => tableValue.ToJson(), - BlankValue => JsonValue.Create(string.Empty), - _ => $"[{value.GetType().Name}]", - }; - private static JsonArray ToJson(this TableValue value) { return new([.. GetJsonElements()]); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/AgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/AgentExecutor.cs index 9e6a6884b6..45a5b47bd8 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/AgentExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Kit/AgentExecutor.cs @@ -33,5 +33,5 @@ public abstract class AgentExecutor(string id, FormulaSession session, WorkflowA bool autoSend, IEnumerable? inputMessages = null, CancellationToken cancellationToken = default) - => agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, inputMessages, cancellationToken); + => agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, inputMessages, inputArguments: null, cancellationToken); } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs index cafece9a57..ed069a9b78 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeAzureAgentExecutor.cs @@ -60,8 +60,8 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA string? conversationId = this.GetConversationId(); string agentName = this.GetAgentName(); bool autoSend = this.GetAutoSendValue(); - - AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, messages, cancellationToken).ConfigureAwait(false); + Dictionary? inputParameters = this.GetStructuredInputs(); + AgentRunResponse agentResponse = await agentProvider.InvokeAgentAsync(this.Id, context, agentName, conversationId, autoSend, messages, inputParameters, cancellationToken).ConfigureAwait(false); ChatMessage[] actionableMessages = FilterActionableContent(agentResponse).ToArray(); if (actionableMessages.Length > 0) @@ -107,6 +107,23 @@ internal sealed class InvokeAzureAgentExecutor(InvokeAzureAgent model, WorkflowA await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false); } + private Dictionary? GetStructuredInputs() + { + Dictionary? inputs = null; + + if (this.AgentInput?.Arguments is not null) + { + inputs = []; + + foreach (KeyValuePair argument in this.AgentInput.Arguments) + { + inputs[argument.Key] = this.Evaluator.GetValue(argument.Value).Value.ToObject(); + } + } + + return inputs; + } + private IEnumerable? GetInputMessages() { DataValue? userInput = null; diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs index 449e982982..81ed6e30d3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/SetVariableExecutor.cs @@ -8,7 +8,6 @@ using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; using Microsoft.Bot.ObjectModel.Abstractions; using Microsoft.PowerFx.Types; -using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; @@ -17,17 +16,15 @@ internal sealed class SetVariableExecutor(SetVariable model, WorkflowFormulaStat { protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default) { - PropertyPath variablePath = Throw.IfNull(this.Model.Variable?.Path, $"{nameof(this.Model)}.{nameof(model.Variable)}"); - if (this.Model.Value is null) { - await this.AssignAsync(variablePath, FormulaValue.NewBlank(), context).ConfigureAwait(false); + await this.AssignAsync(this.Model.Variable?.Path, FormulaValue.NewBlank(), context).ConfigureAwait(false); } else { EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Value); - await this.AssignAsync(variablePath, expressionResult.Value.ToFormula(), context).ConfigureAwait(false); + await this.AssignAsync(this.Model.Variable?.Path, expressionResult.Value.ToFormula(), context).ConfigureAwait(false); } return default; diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/AgentMessage.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/AgentMessage.cs new file mode 100644 index 0000000000..927a842478 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/AgentMessage.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.AI; +using Microsoft.PowerFx.Types; + +namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions; + +internal sealed class AgentMessage : MessageFunction +{ + public const string FunctionName = nameof(AgentMessage); + + public AgentMessage() : base(FunctionName) { } + + public static FormulaValue Execute(StringValue input) => Create(ChatRole.Assistant, input); +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/MessageFunction.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/MessageFunction.cs new file mode 100644 index 0000000000..e9d52c2e15 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/MessageFunction.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI.Workflows.Declarative.Extensions; +using Microsoft.Extensions.AI; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; + +namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions; + +internal abstract class MessageFunction : ReflectionFunction +{ + protected MessageFunction(string functionName) + : base(functionName, FormulaType.String, FormulaType.String) + { } + + protected static FormulaValue Create(ChatRole role, StringValue input) => + string.IsNullOrEmpty(input.Value) ? + FormulaValue.NewBlank(RecordType.Empty()) : + FormulaValue.NewRecordFromFields( + new NamedValue(TypeSchema.Discriminator, nameof(ChatMessage).ToFormula()), + new NamedValue(TypeSchema.Message.Fields.Role, FormulaValue.New(role.Value)), + new NamedValue( + TypeSchema.Message.Fields.Content, + FormulaValue.NewTable( + RecordType.Empty() + .Add(TypeSchema.Message.Fields.ContentType, FormulaType.String) + .Add(TypeSchema.Message.Fields.ContentValue, FormulaType.String), + [ + FormulaValue.NewRecordFromFields( + new NamedValue(TypeSchema.Message.Fields.ContentType, FormulaValue.New(TypeSchema.Message.ContentTypes.Text)), + new NamedValue(TypeSchema.Message.Fields.ContentValue, input)) + ] + ) + ) + ); +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/MessageText.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/MessageText.cs new file mode 100644 index 0000000000..ff9f7d499e --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/MessageText.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; + +namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions; + +internal static class MessageText +{ + public const string FunctionName = nameof(MessageText); + + public sealed class StringInput() + : ReflectionFunction(FunctionName, FormulaType.String, FormulaType.String) + { + public static FormulaValue Execute(StringValue input) => input; + } + + public sealed class RecordInput() : ReflectionFunction(FunctionName, FormulaType.String, RecordType.Empty()) + { + public static FormulaValue Execute(RecordValue input) => FormulaValue.New(GetTextFromRecord(input)); + } + + public sealed class TableInput() : ReflectionFunction(FunctionName, FormulaType.String, TableType.Empty()) + { + public static FormulaValue Execute(TableValue tableValue) + { + return FormulaValue.New(string.Join("\n", GetText())); + + IEnumerable GetText() + { + foreach (DValue row in tableValue.Rows) + { + string text = GetTextFromRecord(row.Value); + if (!string.IsNullOrWhiteSpace(text)) + { + yield return text; + } + } + } + } + } + + private static string GetTextFromRecord(RecordValue recordValue) + { + FormulaValue textValue = recordValue.GetField(TypeSchema.Message.Fields.Text); + + return textValue switch + { + StringValue stringValue => stringValue.Value.Trim(), + _ => string.Empty, + }; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/UserMessage.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/UserMessage.cs index 1d4510b11d..968431623a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/UserMessage.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/Functions/UserMessage.cs @@ -1,38 +1,15 @@ // Copyright (c) Microsoft. All rights reserved. -using Microsoft.Agents.AI.Workflows.Declarative.Extensions; using Microsoft.Extensions.AI; -using Microsoft.PowerFx; using Microsoft.PowerFx.Types; namespace Microsoft.Agents.AI.Workflows.Declarative.PowerFx.Functions; -internal sealed class UserMessage : ReflectionFunction +internal sealed class UserMessage : MessageFunction { public const string FunctionName = nameof(UserMessage); - public UserMessage() - : base(FunctionName, FormulaType.String, FormulaType.String) - { } + public UserMessage() : base(FunctionName) { } - public static FormulaValue Execute(StringValue input) => - string.IsNullOrEmpty(input.Value) ? - FormulaValue.NewBlank(RecordType.Empty()) : - FormulaValue.NewRecordFromFields( - new NamedValue(TypeSchema.Discriminator, nameof(ChatMessage).ToFormula()), - new NamedValue(TypeSchema.Message.Fields.Role, FormulaValue.New(ChatRole.User.Value)), - new NamedValue( - TypeSchema.Message.Fields.Content, - FormulaValue.NewTable( - RecordType.Empty() - .Add(TypeSchema.Message.Fields.ContentType, FormulaType.String) - .Add(TypeSchema.Message.Fields.ContentValue, FormulaType.String), - [ - FormulaValue.NewRecordFromFields( - new NamedValue(TypeSchema.Message.Fields.ContentType, FormulaValue.New(TypeSchema.Message.ContentTypes.Text)), - new NamedValue(TypeSchema.Message.Fields.ContentValue, input)) - ] - ) - ) - ); + public static FormulaValue Execute(StringValue input) => Create(ChatRole.User, input); } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs index 6d0364603c..2087307504 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/RecalcEngineFactory.cs @@ -38,7 +38,11 @@ internal static class RecalcEngineFactory } config.EnableSetFunction(); + config.AddFunction(new AgentMessage()); config.AddFunction(new UserMessage()); + config.AddFunction(new MessageText.StringInput()); + config.AddFunction(new MessageText.RecordInput()); + config.AddFunction(new MessageText.TableInput()); return config; } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/WorkflowAgentProvider.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/WorkflowAgentProvider.cs index 7afaf4fa04..34c327281a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/WorkflowAgentProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/WorkflowAgentProvider.cs @@ -87,9 +87,16 @@ public abstract class WorkflowAgentProvider /// An optional agent version. /// Optional identifier of the target conversation. /// The messages to include in the invocation. + /// Optional input arguments for agents that provide support. /// A token that propagates notification when operation should be canceled. /// Asynchronous set of . - public abstract IAsyncEnumerable InvokeAgentAsync(string agentId, string? agentVersion, string? conversationId, IEnumerable? messages, CancellationToken cancellationToken = default); + public abstract IAsyncEnumerable InvokeAgentAsync( + string agentId, + string? agentVersion, + string? conversationId, + IEnumerable? messages, + IDictionary? inputArguments, + CancellationToken cancellationToken = default); /// /// Retrieves a set of messages from a conversation. diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/AgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/AgentProvider.cs index f90e6fbbd5..1ea1690458 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/AgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/AgentProvider.cs @@ -15,6 +15,7 @@ internal abstract class AgentProvider(IConfiguration configuration) public const string FunctionTool = "FUNCTIONTOOL"; public const string Marketing = "MARKETING"; public const string MathChat = "MATHCHAT"; + public const string InputArguments = "INPUTARGUMENTS"; } public static class Settings @@ -31,6 +32,7 @@ internal abstract class AgentProvider(IConfiguration configuration) Names.FunctionTool => new FunctionToolAgentProvider(configuration), Names.Marketing => new MarketingAgentProvider(configuration), Names.MathChat => new MathChatAgentProvider(configuration), + Names.InputArguments => new PoemAgentProvider(configuration), _ => new TestAgentProvider(configuration), }; @@ -40,7 +42,7 @@ internal abstract class AgentProvider(IConfiguration configuration) await foreach (AgentVersion agent in this.CreateAgentsAsync(foundryEndpoint)) { - Console.WriteLine($"Created agent: {agent.Name}:{agent.Version})"); + Console.WriteLine($"Created agent: {agent.Name}:{agent.Version}"); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/PoemAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/PoemAgentProvider.cs new file mode 100644 index 0000000000..9ee7797edf --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/PoemAgentProvider.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Azure.AI.Agents; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using Shared.Foundry; + +namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; + +internal sealed class PoemAgentProvider(IConfiguration configuration) : AgentProvider(configuration) +{ + protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) + { + AgentClient agentClient = new(foundryEndpoint, new AzureCliCredential()); + + yield return + await agentClient.CreateAgentAsync( + agentName: "PoemAgent", + agentDefinition: this.DefinePoemAgent(), + agentDescription: "Authors original poems"); + } + + private PromptAgentDefinition DefinePoemAgent() => + new(this.GetSetting(Settings.FoundryModelMini)) + { + Instructions = + """ + Write a one verse poem on the requested topic in the style of: {{style}}. + """, + StructuredInputs = + { + ["style"] = + new StructuredInputDefinition + { + IsRequired = false, + DefaultValue = BinaryData.FromString(@"""haiku"""), + Description = "The style of poem to write", + } + } + }; +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs index 567683971b..b2de034da6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/DeclarativeWorkflowTest.cs @@ -17,11 +17,12 @@ public sealed class DeclarativeWorkflowTest(ITestOutputHelper output) : Workflow { [Theory] [InlineData("CheckSystem.yaml", "CheckSystem.json")] - [InlineData("SendActivity.yaml", "SendActivity.json")] - [InlineData("InvokeAgent.yaml", "InvokeAgent.json")] - [InlineData("InvokeAgent.yaml", "InvokeAgent.json", true)] [InlineData("ConversationMessages.yaml", "ConversationMessages.json")] [InlineData("ConversationMessages.yaml", "ConversationMessages.json", true)] + [InlineData("InputArguments.yaml", "InputArguments.json")] + [InlineData("InvokeAgent.yaml", "InvokeAgent.json")] + [InlineData("InvokeAgent.yaml", "InvokeAgent.json", true)] + [InlineData("SendActivity.yaml", "SendActivity.json")] public Task ValidateCaseAsync(string workflowFileName, string testcaseFileName, bool externalConveration = false) => this.RunWorkflowAsync(GetWorkflowPath(workflowFileName, isSample: false), testcaseFileName, externalConveration); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/InputArguments.json b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/InputArguments.json new file mode 100644 index 0000000000..8eb876eb60 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/InputArguments.json @@ -0,0 +1,23 @@ +{ + "description": "Authors a poem in the style specified by the input argument.", + "setup": { + "input": { + "type": "String", + "value": "Why is the sky blue?" + } + }, + "validation": { + "conversation_count": 1, + "min_action_count": 1, + "min_response_count": 1, + "min_message_count": 3, + "actions": { + "start": [ + "invoke_poem" + ], + "final": [ + "invoke_poem" + ] + } + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/MathChat.json b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/MathChat.json index e3a9b85836..ea5337263a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/MathChat.json +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Testcases/MathChat.json @@ -13,7 +13,7 @@ "min_response_count": 2, "max_response_count": 8, "min_message_count": 4, - "max_message_count": 17, + "max_message_count": -1, "actions": { "start": [ ], diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/InputArguments.yaml b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/InputArguments.yaml new file mode 100644 index 0000000000..c963de31e3 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Workflows/InputArguments.yaml @@ -0,0 +1,15 @@ +kind: Workflow +trigger: + + kind: OnConversationStart + id: workflow_test + actions: + + - kind: InvokeAzureAgent + id: invoke_poem + conversationId: =System.ConversationId + agent: + name: PoemAgent + input: + arguments: + style: "ee cummings" diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/Functions/AgentMessageTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/Functions/AgentMessageTests.cs new file mode 100644 index 0000000000..7ca7041549 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/Functions/AgentMessageTests.cs @@ -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(result); + } + + [Fact] + public void Execute_ReturnsExpectedRecord_ForNonEmptyInput() + { + const string Text = "Hello"; + FormulaValue sourceValue = FormulaValue.New(Text); + StringValue stringValue = Assert.IsType(sourceValue); + + FormulaValue result = AgentMessage.Execute(stringValue); + + RecordValue recordResult = Assert.IsType(result, exactMatch: false); + + // Discriminator + FormulaValue discriminator = recordResult.GetField(TypeSchema.Discriminator); + StringValue discriminatorValue = Assert.IsType(discriminator); + Assert.Equal(nameof(ChatMessage), discriminatorValue.Value); + + // Role + FormulaValue role = recordResult.GetField(TypeSchema.Message.Fields.Role); + StringValue roleValue = Assert.IsType(role); + Assert.Equal(ChatRole.Assistant.Value, roleValue.Value); + + // Content table + FormulaValue content = recordResult.GetField(TypeSchema.Message.Fields.Content); + TableValue table = Assert.IsType(content, exactMatch: false); + + List rows = table.Rows.Select(value => value.Value).ToList(); + Assert.Single(rows); + + StringValue contentType = Assert.IsType(rows[0].GetField(TypeSchema.Message.Fields.ContentType)); + Assert.Equal(TypeSchema.Message.ContentTypes.Text, contentType.Value); + + StringValue contentValue = Assert.IsType(rows[0].GetField(TypeSchema.Message.Fields.ContentValue)); + Assert.Equal(Text, contentValue.Value); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/Functions/MessageTextTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/Functions/MessageTextTests.cs new file mode 100644 index 0000000000..5cbc374990 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/Functions/MessageTextTests.cs @@ -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(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(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(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(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(result); + Assert.Equal("test message 1\ntest message 2", stringResult.Value); + } + + [Fact] + public void Execute_ReturnsEmpty_ForEmptyList() + { + // Arrange + TableValue sourceValue = Array.Empty().ToTable(); + + // Act + FormulaValue result = MessageText.TableInput.Execute(sourceValue); + + // Assert + StringValue stringResult = Assert.IsType(result); + Assert.Empty(stringResult.Value); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/Functions/UserMessageTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/Functions/UserMessageTests.cs index 712fb139bf..705831473c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/Functions/UserMessageTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/Functions/UserMessageTests.cs @@ -22,11 +22,10 @@ public class UserMessageTests public void Execute_ReturnsBlank_ForEmptyInput() { // Arrange - FormulaValue sourceValue = FormulaValue.New(string.Empty); - StringValue stringValue = Assert.IsType(sourceValue); + StringValue sourceValue = FormulaValue.New(string.Empty); // Act - FormulaValue result = UserMessage.Execute(stringValue); + FormulaValue result = UserMessage.Execute(sourceValue); // Assert Assert.IsType(result); diff --git a/workflow-samples/DeepResearch.yaml b/workflow-samples/DeepResearch.yaml index 4e13b5b9f3..4949680c56 100644 --- a/workflow-samples/DeepResearch.yaml +++ b/workflow-samples/DeepResearch.yaml @@ -119,13 +119,13 @@ trigger: # FACTS Consider this initial fact sheet: - {Trim(Last(Local.TaskFacts).Text)} + {MessageText(Local.TaskFacts)} # PLAN Here is the plan to follow as best as possible: - {Last(Local.Plan).Text} + {MessageText(Local.Plan)} - kind: SendActivity id: sendActivity_bwNZiM @@ -247,7 +247,7 @@ trigger: Here is the old fact sheet: - {Local.TaskFacts}" + {MessageText(Local.TaskFacts)}" - kind: SendActivity id: sendActivity_dsBaJU @@ -291,13 +291,13 @@ trigger: # FACTS Consider this initial fact sheet: - {Local.TaskFacts.Text} + {MessageText(Local.TaskFacts)} # PLAN Here is the plan to follow as best as possible: - {Local.Plan.Text} + {MessageText(Local.Plan)} - kind: SetVariable id: setVariable_6J2snP @@ -356,7 +356,7 @@ trigger: - kind: SetVariable id: setVariable_XzNrdM variable: Local.AgentResponseText - value: =Last(Local.AgentResponse).Text + value: =MessageText(Local.AgentResponse) - kind: ResetVariable id: setVariable_8eIx2A diff --git a/workflow-samples/MathChat.yaml b/workflow-samples/MathChat.yaml index 855d968546..363256efc3 100644 --- a/workflow-samples/MathChat.yaml +++ b/workflow-samples/MathChat.yaml @@ -35,7 +35,7 @@ trigger: id: check_completion conditions: - - condition: =!IsBlank(Find("CONGRATULATIONS", Upper(Last(Local.TeacherResponse).Text))) + - condition: =!IsBlank(Find("CONGRATULATIONS", Upper(MessageText(Local.TeacherResponse)))) id: check_turn_done actions: diff --git a/workflow-samples/README.md b/workflow-samples/README.md index 6932f52874..a7bed697e5 100644 --- a/workflow-samples/README.md +++ b/workflow-samples/README.md @@ -6,7 +6,7 @@ may be executed locally no different from any regular `Workflow` that is defined The difference is that the workflow definition is loaded from a YAML file instead of being defined in code: ```c# -Workflow workflow = DeclarativeWorkflowBuilder.Build("Marketing.yaml", options); +Workflow workflow = DeclarativeWorkflowBuilder.Build("Marketing.yaml", options); ``` These example workflows may be executed by the workflow