diff --git a/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/README.md b/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/README.md index 48deaad94e..d2ce2137c5 100644 --- a/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/README.md +++ b/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/README.md @@ -63,6 +63,12 @@ To add DevUI to your ASP.NET Core application: .AddAsAIAgent(); ``` + `AddAsAIAgent()` includes the workflow's final output in the hosted agent response. This is + required when the workflow is exposed through `MapOpenAIResponses()` or + `MapOpenAIConversations()`; otherwise the output can be visible in streaming workflow events + while the completed response has no output items. If you create an agent directly from a + `Workflow`, opt in with `workflow.AsAIAgent(includeWorkflowOutputsInResponse: true)`. + 3. Add OpenAI services and map the endpoints for OpenAI and DevUI: ```csharp // Register services for OpenAI responses and conversations (also required for DevUI) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedWorkflowBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedWorkflowBuilderExtensions.cs index abee1cb566..8dc684721b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedWorkflowBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedWorkflowBuilderExtensions.cs @@ -15,6 +15,7 @@ public static class HostedWorkflowBuilderExtensions /// /// The instance to extend. /// The DI service lifetime for the agent registration. Defaults to . + /// Workflow outputs are included in the hosted agent response. /// An that can be used to further configure the agent. public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, ServiceLifetime lifetime = ServiceLifetime.Singleton) => builder.AddAsAIAgent(name: null, lifetime: lifetime); @@ -25,6 +26,7 @@ public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder build /// The instance to extend. /// The optional name for the AI agent. If not specified, the workflow name is used. /// The DI service lifetime for the agent registration. Defaults to . + /// Workflow outputs are included in the hosted agent response. /// An that can be used to further configure the agent. public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder builder, string? name, ServiceLifetime lifetime = ServiceLifetime.Singleton) { @@ -32,6 +34,8 @@ public static IHostedAgentBuilder AddAsAIAgent(this IHostedWorkflowBuilder build var agentName = name ?? workflowName; return builder.HostApplicationBuilder.AddAIAgent(agentName, (sp, key) => - sp.GetRequiredKeyedService(workflowName).AsAIAgent(name: key), lifetime); + sp.GetRequiredKeyedService(workflowName).AsAIAgent( + name: key, + includeWorkflowOutputsInResponse: true), lifetime); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/ChatMessageOutputWorkflow.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/ChatMessageOutputWorkflow.cs new file mode 100644 index 0000000000..64d16beefe --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/ChatMessageOutputWorkflow.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Hosting.UnitTests; + +internal static class ChatMessageOutputWorkflow +{ + internal static Workflow Build(string name) + { + var output = new OutputExecutor("output"); + return new WorkflowBuilder(output) + .WithName(name) + .WithOutputFrom(output) + .Build(); + } + + private sealed class OutputExecutor(string id) : ChatProtocolExecutor(id) + { + protected override ValueTask TakeTurnAsync( + List messages, + IWorkflowContext context, + bool? emitEvents, + CancellationToken cancellationToken = default) + => context.AddEventAsync( + new WorkflowOutputEvent(new ChatMessage(ChatRole.Assistant, "workflow output"), this.Id), + cancellationToken); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderWorkflowExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderWorkflowExtensionsTests.cs index c17655bd29..03045b292d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderWorkflowExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostApplicationBuilderWorkflowExtensionsTests.cs @@ -2,7 +2,9 @@ using System; using System.Linq; +using System.Threading.Tasks; using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; @@ -186,6 +188,27 @@ public void AddAsAIAgent_WithoutName_UsesWorkflowName() Assert.NotNull(agentDescriptor); } + /// + /// Verifies that a workflow registered as an AI agent includes its chat-message output in the response. + /// + [Fact] + public async Task AddAsAIAgent_IncludesWorkflowOutputInResponseAsync() + { + // Arrange + var builder = new HostApplicationBuilder(); + const string WorkflowName = "outputWorkflow"; + builder.AddWorkflow(WorkflowName, (sp, key) => ChatMessageOutputWorkflow.Build(key)) + .AddAsAIAgent(); + using var host = builder.Build(); + AIAgent agent = host.Services.GetRequiredKeyedService(WorkflowName); + + // Act + AgentResponse response = await agent.RunAsync(new ChatMessage(ChatRole.User, "hello")); + + // Assert + Assert.Equal("workflow output", response.Text); + } + /// /// Verifies that AddAsAIAgent with a name parameter uses that name instead of the workflow name. ///