diff --git a/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/Program.cs b/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/Program.cs index f7428fa4f3e..7ab48835813 100644 --- a/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/Program.cs +++ b/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/Program.cs @@ -90,7 +90,7 @@ static string GetCurrentTime() { var agents = new List() { assistantBuilder, reviewerBuilder }.Select(ab => sp.GetRequiredKeyedService(ab.Name)); return AgentWorkflowBuilder.BuildSequential(workflowName: key, agents: agents); - }).AddAsAIAgent(); + }).AddAsAIAgent(includeWorkflowOutputsInResponse: true); builder.Services.AddOpenAIResponses(); builder.Services.AddOpenAIConversations(); 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 48deaad94e7..d46b1a3905d 100644 --- a/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/README.md +++ b/dotnet/samples/02-agents/DevUI/DevUI_Step01_BasicUsage/README.md @@ -60,9 +60,16 @@ To add DevUI to your ASP.NET Core application: var agent1Builder = builder.AddAIAgent("workflow-agent1", "You are agent 1."); var agent2Builder = builder.AddAIAgent("workflow-agent2", "You are agent 2."); builder.AddSequentialWorkflow("my-workflow", [agent1Builder, agent2Builder]) - .AddAsAIAgent(); + .AddAsAIAgent(includeWorkflowOutputsInResponse: true); ``` + Set `includeWorkflowOutputsInResponse` to `true` to include 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 abee1cb566f..c29705b0f9f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/HostedWorkflowBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/HostedWorkflowBuilderExtensions.cs @@ -15,9 +15,13 @@ public static class HostedWorkflowBuilderExtensions /// /// The instance to extend. /// The DI service lifetime for the agent registration. Defaults to . + /// If , workflow outputs are included in the 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); + public static IHostedAgentBuilder AddAsAIAgent( + this IHostedWorkflowBuilder builder, + ServiceLifetime lifetime = ServiceLifetime.Singleton, + bool includeWorkflowOutputsInResponse = false) + => builder.AddAsAIAgent(name: null, lifetime: lifetime, includeWorkflowOutputsInResponse: includeWorkflowOutputsInResponse); /// /// Registers the workflow as an AI agent in the dependency injection container. @@ -25,13 +29,20 @@ 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 . + /// If , workflow outputs are included in the 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) + public static IHostedAgentBuilder AddAsAIAgent( + this IHostedWorkflowBuilder builder, + string? name, + ServiceLifetime lifetime = ServiceLifetime.Singleton, + bool includeWorkflowOutputsInResponse = false) { var workflowName = builder.Name; 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: includeWorkflowOutputsInResponse), 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 00000000000..64d16beefed --- /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 c17655bd29f..44cd39286f7 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,48 @@ 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(includeWorkflowOutputsInResponse: true); + 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 a workflow registered as an AI agent excludes its output from the response by default. + /// + [Fact] + public async Task AddAsAIAgent_DefaultExcludesWorkflowOutputFromResponseAsync() + { + // 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.Empty(response.Messages); + } + /// /// Verifies that AddAsAIAgent with a name parameter uses that name instead of the workflow name. ///