diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/InputResponse.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/InputResponse.cs index a34d41610e..65454d2994 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/InputResponse.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Events/InputResponse.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; namespace Microsoft.Agents.AI.Workflows.Declarative.Events; @@ -12,15 +13,24 @@ public sealed class InputResponse /// /// The response value. /// - public string Value { get; } + public ChatMessage Value { get; } /// /// Initializes a new instance of the class. /// /// The response value. [JsonConstructor] - public InputResponse(string value) + public InputResponse(ChatMessage value) { this.Value = value; } + + /// + /// Initializes a new instance of the class. + /// + /// The response value. + public InputResponse(string value) + { + this.Value = new ChatMessage(ChatRole.User, value); + } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs index 720636178e..067810d007 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/IWorkflowContextExtensions.cs @@ -69,25 +69,20 @@ internal static class IWorkflowContextExtensions await context.AddEventAsync(new ConversationUpdateEvent(conversationId) { IsWorkflow = isExternal }, cancellationToken).ConfigureAwait(false); } + public static string? GetWorkflowConversation(this IWorkflowContext context) => + context.ReadState(SystemScope.Names.ConversationId, VariableScopeNames.System) switch + { + StringValue stringValue when stringValue.Value.Length > 0 => stringValue.Value, + _ => null, + }; + public static bool IsWorkflowConversation( this IWorkflowContext context, string? conversationId, out string? workflowConversationId) { - FormulaValue idValue = context.ReadState(SystemScope.Names.ConversationId, VariableScopeNames.System); - switch (idValue) - { - case BlankValue: - case ErrorValue: - workflowConversationId = null; - return false; - case StringValue stringValue when stringValue.Value.Length > 0: - workflowConversationId = stringValue.Value; - return workflowConversationId.Equals(conversationId, StringComparison.Ordinal); - default: - // Something has gone terribly wrong. - throw new DeclarativeActionException($"Invalid '{SystemScope.Names.ConversationId}' value type: {idValue.GetType().Name}."); - } + workflowConversationId = context.GetWorkflowConversation(); + return workflowConversationId?.Equals(conversationId, StringComparison.Ordinal) ?? false; } private static DeclarativeWorkflowContext DeclarativeContext(IWorkflowContext context) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs index 377d65c99b..ce511040cd 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs @@ -237,7 +237,7 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor this.Trace(item); // Entry point for question - QuestionExecutor action = new(item, this._workflowState); + QuestionExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState); this.ContinueWith(action); // Transition to post action if complete string postId = Steps.Post(action.Id); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs index 9dbaa2efa1..1c6e1596df 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/QuestionExecutor.cs @@ -9,12 +9,13 @@ using Microsoft.Agents.AI.Workflows.Declarative.Interpreter; using Microsoft.Agents.AI.Workflows.Declarative.Kit; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Bot.ObjectModel; +using Microsoft.Extensions.AI; using Microsoft.PowerFx.Types; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel; -internal sealed class QuestionExecutor(Question model, WorkflowFormulaState state) : +internal sealed class QuestionExecutor(Question model, WorkflowAgentProvider agentProvider, WorkflowFormulaState state) : DeclarativeActionExecutor(model, state) { public static class Steps @@ -82,21 +83,21 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat public async ValueTask CaptureResponseAsync(IWorkflowContext context, InputResponse message, CancellationToken cancellationToken) { FormulaValue? extractedValue = null; - if (string.IsNullOrWhiteSpace(message.Value)) + if (message.Value is null) { string unrecognizedResponse = this.FormatPrompt(this.Model.UnrecognizedPrompt); await context.AddEventAsync(new MessageActivityEvent(unrecognizedResponse.Trim()), cancellationToken).ConfigureAwait(false); } else { - EntityExtractionResult entityResult = EntityExtractor.Parse(this.Model.Entity, message.Value); + EntityExtractionResult entityResult = EntityExtractor.Parse(this.Model.Entity, message.Value.Text); if (entityResult.IsValid) { extractedValue = entityResult.Value; } else { - string invalidResponse = this.FormatPrompt(this.Model.InvalidPrompt); + string invalidResponse = this.Model.InvalidPrompt is not null ? this.FormatPrompt(this.Model.InvalidPrompt) : "Invalid response"; await context.AddEventAsync(new MessageActivityEvent(invalidResponse.Trim()), cancellationToken).ConfigureAwait(false); } } @@ -107,6 +108,25 @@ internal sealed class QuestionExecutor(Question model, WorkflowFormulaState stat } else { + bool autoSend = true; + + if (this.Model.ExtensionData?.Properties.TryGetValue("autoSend", out DataValue? autoSendValue) ?? false) + { + autoSend = autoSendValue.ToObject() is bool value && value; + } + + if (autoSend) + { + string? workflowConversationId = context.GetWorkflowConversation(); + if (workflowConversationId is not null) + { + // Input message always defined if values has been extracted. + ChatMessage input = message.Value!; + await agentProvider.CreateMessageAsync(workflowConversationId, input, cancellationToken).ConfigureAwait(false); + await context.SetLastMessageAsync(input).ConfigureAwait(false); + } + } + await this.AssignAsync(this.Model.Variable?.Path, extractedValue, context).ConfigureAwait(false); await this._hasExecuted.WriteAsync(context, true).ConfigureAwait(false); await context.SendResultMessageAsync(this.Id, cancellationToken).ConfigureAwait(false); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputRequest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputRequestTest.cs similarity index 100% rename from dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputRequest.cs rename to dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputRequestTest.cs diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputResponse.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputResponseTest.cs similarity index 55% rename from dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputResponse.cs rename to dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputResponseTest.cs index 6304aef69b..015d80e246 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputResponse.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Events/InputResponseTest.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.Agents.AI.Workflows.Declarative.Events; +using Microsoft.Extensions.AI; using Xunit.Abstractions; namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; @@ -11,9 +12,16 @@ namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests.Events; public sealed class InputResponseTest(ITestOutputHelper output) : EventTest(output) { [Fact] - public void VerifySerialization() + public void VerifySerializationText() { InputResponse copy = VerifyEventSerialization(new InputResponse("test response")); - Assert.Equal("test response", copy.Value); + Assert.Equal("test response", copy.Value.Text); + } + + [Fact] + public void VerifySerializationMessage() + { + InputResponse copy = VerifyEventSerialization(new InputResponse(new ChatMessage(ChatRole.User, "test response"))); + Assert.Equal("test response", copy.Value.Text); } } diff --git a/workflow-samples/HumanInLoop.yaml b/workflow-samples/HumanInLoop.yaml index 18959b067d..11c63adb5e 100644 --- a/workflow-samples/HumanInLoop.yaml +++ b/workflow-samples/HumanInLoop.yaml @@ -21,6 +21,7 @@ trigger: - kind: Question id: question_confirm alwaysPrompt: false + autoSend: false property: Local.ConfirmedInput prompt: kind: Message