diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowErrorEvent.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowErrorEvent.cs index 7af7efd0b9..aec9e8130c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowErrorEvent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowErrorEvent.cs @@ -10,4 +10,10 @@ namespace Microsoft.Agents.AI.Workflows; /// /// Optionally, the representing the error. /// -public class WorkflowErrorEvent(Exception? e) : WorkflowEvent(e); +public class WorkflowErrorEvent(Exception? e) : WorkflowEvent(e) +{ + /// + /// Gets the exception that caused the current operation to fail, if one occurred. + /// + public Exception? Exception => this.Data as Exception; +} diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs index d27de6bd5c..10d82e484f 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowThread.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; @@ -80,7 +81,7 @@ public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptio return marshaller.Marshal(info); } - public AgentRunResponseUpdate CreateUpdate(string responseId, params AIContent[] parts) + public AgentRunResponseUpdate CreateUpdate(string responseId, object raw, params AIContent[] parts) { Throw.IfNullOrEmpty(parts); @@ -89,7 +90,8 @@ public AgentRunResponseUpdate CreateUpdate(string responseId, params AIContent[] CreatedAt = DateTimeOffset.UtcNow, MessageId = Guid.NewGuid().ToString("N"), Role = ChatRole.Assistant, - ResponseId = responseId + ResponseId = responseId, + RawRepresentation = raw }; this.MessageStore.AddMessages(update.ToChatMessage()); @@ -153,10 +155,25 @@ IAsyncEnumerable InvokeStageAsync( case RequestInfoEvent requestInfo: FunctionCallContent fcContent = requestInfo.Request.ToFunctionCall(); - AgentRunResponseUpdate update = this.CreateUpdate(this.LastResponseId, fcContent); + AgentRunResponseUpdate update = this.CreateUpdate(this.LastResponseId, evt, fcContent); yield return update; break; + case WorkflowErrorEvent workflowError: + Exception? exception = workflowError.Exception; + if (exception is TargetInvocationException tie && tie.InnerException != null) + { + exception = tie.InnerException; + } + + if (exception != null) + { + ErrorContent errorContent = new(exception.Message); + yield return this.CreateUpdate(this.LastResponseId, evt, errorContent); + } + + break; + case SuperStepCompletedEvent stepCompleted: this.LastCheckpoint = stepCompleted.CompletionInfo?.Checkpoint; goto default; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs new file mode 100644 index 0000000000..133fc78e47 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostSmokeTests.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Workflows.UnitTests; + +public sealed class ExpectedException : Exception +{ + public ExpectedException(string message) + : base(message) + { + } + + public ExpectedException() : base() + { + } + + public ExpectedException(string? message, Exception? innerException) : base(message, innerException) + { + } +} + +public class WorkflowHostSmokeTests +{ + private sealed class AlwaysFailsAIAgent(bool failByThrowing) : AIAgent + { + private sealed class Thread : InMemoryAgentThread + { + public Thread() { } + + public Thread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) + : base(serializedThread, jsonSerializerOptions) + { } + } + + public override AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) + { + return new Thread(serializedThread, jsonSerializerOptions); + } + + public override AgentThread GetNewThread() + { + return new Thread(); + } + + public override async Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + return await this.RunStreamingAsync(messages, thread, options, cancellationToken) + .ToAgentRunResponseAsync(cancellationToken); + } + + public override async IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + const string ErrorMessage = "Simulated agent failure."; + if (failByThrowing) + { + throw new ExpectedException(ErrorMessage); + } + + yield return new AgentRunResponseUpdate(ChatRole.Assistant, [new ErrorContent(ErrorMessage)]); + } + } + + private static Workflow CreateWorkflow(bool failByThrowing) + { + ExecutorBinding agent = new AlwaysFailsAIAgent(failByThrowing).BindAsExecutor(emitEvents: true); + + return new WorkflowBuilder(agent).Build(); + } + + private async static Task InvokeAsAgentAndProcessResponseAsync(Workflow workflow) + { + // Arrange is done by the caller. + + // Act + List updates = await workflow.AsAgent("WorkflowAgent") + .RunStreamingAsync(new ChatMessage(ChatRole.User, "Hello")) + .ToListAsync(); + + // Assert + bool hadErrorContent = false; + foreach (AgentRunResponseUpdate update in updates) + { + if (update.Contents.Any()) + { + // We should expect a single update which contains the error content. + update.Contents.Should().ContainSingle() + .Which.Should().BeOfType() + .Which.Message.Should().Be("Simulated agent failure."); + hadErrorContent = true; + } + } + + hadErrorContent.Should().BeTrue(); + } + + [Fact] + public Task Test_AsAgent_ErrorContentStreamedOutAsync() + => InvokeAsAgentAndProcessResponseAsync(CreateWorkflow(failByThrowing: false)); + + [Fact] + public Task Test_AsAgent_ExceptionToErrorContentAsync() + => InvokeAsAgentAndProcessResponseAsync(CreateWorkflow(failByThrowing: true)); +}