From 62854197aa30c744562a9ca2d8585bc8583e9118 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:29:43 +0200 Subject: [PATCH] .NET: Fix A2A conversion routines to ignore unknown content types instead of throwing exceptions (#1154) * Initial plan * Update A2A conversion routines to ignore unknown content types Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> * Fix dotnet format --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: Stephen Toub Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> --- .../src/Microsoft.Agents.AI.A2A/A2AAgent.cs | 2 +- .../Extensions/A2AAIContentExtensions.cs | 14 ++-- .../Extensions/A2AArtifactExtensions.cs | 6 +- .../Extensions/A2AMessageExtensions.cs | 6 +- .../Extensions/A2APartExtensions.cs | 8 +- .../Converters/MessageConverter.cs | 6 +- .../Extensions/A2AAIContentExtensionsTests.cs | 42 ++++++++-- .../Extensions/A2APartExtensionsTests.cs | 11 +-- .../Converters/MessageConverterTests.cs | 80 ++++++++++++++++--- 9 files changed, 142 insertions(+), 33 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs index 87abcbc372..966871b534 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs @@ -159,7 +159,7 @@ internal sealed class A2AAgent : AIAgent RawRepresentation = message, Role = ChatRole.Assistant, MessageId = message.MessageId, - Contents = [.. message.Parts.Select(part => part.ToAIContent())], + Contents = [.. message.Parts.Select(part => part.ToAIContent()).OfType()], AdditionalProperties = message.Metadata.ToAdditionalProperties(), }; } diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAIContentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAIContentExtensions.cs index 10d572db10..8c585c44f3 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAIContentExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAIContentExtensions.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using A2A; @@ -22,7 +21,11 @@ internal static class A2AAIContentExtensions foreach (var content in contents) { - (parts ??= []).Add(content.ToA2APart()); + var part = content.ToA2APart(); + if (part is not null) + { + (parts ??= []).Add(part); + } } return parts; @@ -32,12 +35,13 @@ internal static class A2AAIContentExtensions /// Converts a to a object."/> /// /// AI content to convert. - /// The corresponding A2A object. - internal static Part ToA2APart(this AIContent content) => + /// The corresponding A2A object, or null if the content type is not supported. + internal static Part? ToA2APart(this AIContent content) => content switch { TextContent textContent => new TextPart { Text = textContent.Text }, HostedFileContent hostedFileContent => new FilePart { File = new FileWithUri { Uri = hostedFileContent.FileId } }, - _ => throw new NotSupportedException($"Unsupported content type: {content.GetType().Name}."), + // Ignore unknown content types (FunctionCallContent, FunctionResultContent, etc.) + _ => null, }; } diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AArtifactExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AArtifactExtensions.cs index 39c498b247..36683d549b 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AArtifactExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AArtifactExtensions.cs @@ -16,7 +16,11 @@ internal static class A2AArtifactExtensions foreach (var part in artifact.Parts) { - (aiContents ??= []).Add(part.ToAIContent()); + var content = part.ToAIContent(); + if (content is not null) + { + (aiContents ??= []).Add(content); + } } return new ChatMessage(ChatRole.Assistant, aiContents) diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AMessageExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AMessageExtensions.cs index 51e7545cfc..90b15f5d8c 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AMessageExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AMessageExtensions.cs @@ -16,7 +16,11 @@ internal static class A2AMessageExtensions foreach (var part in message.Parts) { - (aiContents ??= []).Add(part.ToAIContent()); + var content = part.ToAIContent(); + if (content is not null) + { + (aiContents ??= []).Add(content); + } } return new ChatMessage(ChatRole.Assistant, aiContents) diff --git a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2APartExtensions.cs b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2APartExtensions.cs index 8fa40a159d..ac2da0ddaf 100644 --- a/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2APartExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2APartExtensions.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using Microsoft.Extensions.AI; namespace A2A; @@ -14,8 +13,8 @@ internal static class A2APartExtensions /// Converts an A2A to an . /// /// The A2A part to convert. - /// The corresponding . - internal static AIContent ToAIContent(this Part part) => + /// The corresponding , or null if the part type is not supported. + internal static AIContent? ToAIContent(this Part part) => part switch { TextPart textPart => new TextContent(textPart.Text) @@ -30,6 +29,7 @@ internal static class A2APartExtensions AdditionalProperties = filePart.Metadata.ToAdditionalProperties() }, - _ => throw new NotSupportedException($"Part type '{part.GetType().Name}' is not supported.") + // Ignore unknown part types (DataPart, etc.) + _ => null }; } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/MessageConverter.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/MessageConverter.cs index 705db9fbb1..38f4edff39 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/MessageConverter.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/MessageConverter.cs @@ -160,7 +160,8 @@ internal static class MessageConverter RawRepresentation = textPart, AdditionalProperties = textPart.Metadata?.ToAdditionalPropertiesDictionary() }, - FilePart or DataPart or _ => throw new NotSupportedException($"Part type '{part.GetType().Name}' is not supported. Only TextPart is supported.") + // Ignore unknown content types (FilePart, DataPart, etc.) + _ => null }; /// @@ -234,7 +235,8 @@ internal static class MessageConverter { Text = textContent.Text }, - _ => throw new NotSupportedException($"Content type '{content.GetType().Name}' is not supported.") + // Ignore unknown content types (FunctionCallContent, FunctionResultContent, etc.) + _ => null }; private static AdditionalPropertiesDictionary? ToAdditionalPropertiesDictionary(this Dictionary metadata) diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAIContentExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAIContentExtensionsTests.cs index b6ed70f4f0..857ab0f1d6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAIContentExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAIContentExtensionsTests.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using A2A; using Microsoft.Extensions.AI; @@ -49,14 +48,16 @@ public sealed class A2AAIContentExtensionsTests } [Fact] - public void ToA2APart_WithUnsupportedContentType_ThrowsNotSupportedException() + public void ToA2APart_WithUnsupportedContentType_ReturnsNull() { // Arrange var unsupportedContent = new MockAIContent(); - // Act & Assert - var exception = Assert.Throws(unsupportedContent.ToA2APart); - Assert.Equal("Unsupported content type: MockAIContent.", exception.Message); + // Act + var result = unsupportedContent.ToA2APart(); + + // Assert + Assert.Null(result); } [Fact] @@ -106,6 +107,37 @@ public sealed class A2AAIContentExtensionsTests Assert.Equal("https://example.com/file2.txt", secondFileWithUri.Uri); } + [Fact] + public void ToA2AParts_WithMixedSupportedAndUnsupportedContent_IgnoresUnsupportedContent() + { + // Arrange + var contents = new List + { + new TextContent("First text"), + new MockAIContent(), // Unsupported - should be ignored + new HostedFileContent("https://example.com/file.txt"), + new MockAIContent(), // Unsupported - should be ignored + new TextContent("Second text") + }; + + // Act + var result = contents.ToA2AParts(); + + // Assert + Assert.NotNull(result); + Assert.Equal(3, result.Count); + + var firstTextPart = Assert.IsType(result[0]); + Assert.Equal("First text", firstTextPart.Text); + + var filePart = Assert.IsType(result[1]); + var fileWithUri = Assert.IsType(filePart.File); + Assert.Equal("https://example.com/file.txt", fileWithUri.Uri); + + var secondTextPart = Assert.IsType(result[2]); + Assert.Equal("Second text", secondTextPart.Text); + } + // Mock class for testing unsupported scenarios private sealed class MockAIContent : AIContent; } diff --git a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2APartExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2APartExtensionsTests.cs index e44e0f1177..21be340d56 100644 --- a/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2APartExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2APartExtensionsTests.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Text.Json; using A2A; @@ -80,14 +79,16 @@ public sealed class A2APartExtensionsTests } [Fact] - public void ToAIContent_WithCustomPartType_ThrowsNotSupportedException() + public void ToAIContent_WithCustomPartType_ReturnsNull() { // Arrange var customPart = new MockPart(); - // Act & Assert - var exception = Assert.Throws(customPart.ToAIContent); - Assert.Equal("Part type 'MockPart' is not supported.", exception.Message); + // Act + var result = customPart.ToAIContent(); + + // Assert + Assert.Null(result); } // Mock class for testing unsupported scenarios diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.Tests/Converters/MessageConverterTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.Tests/Converters/MessageConverterTests.cs index 53a7445d0b..5791a0c238 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.Tests/Converters/MessageConverterTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.Tests/Converters/MessageConverterTests.cs @@ -228,13 +228,16 @@ public class MessageConverterTests } [Fact] - public void ToA2AMessage_ChatMessageWithUnsupportedContent_ThrowsNotSupportedException() + public void ToA2AMessage_ChatMessageWithUnsupportedContent_IgnoresUnsupportedContent() { var unsupportedContent = new DataContent(new byte[] { 1, 2, 3 }, "image/png"); var chatMessage = new ChatMessage(ChatRole.User, [unsupportedContent]); - var exception = Assert.Throws(chatMessage.ToA2AMessage); - Assert.Contains("Content type 'DataContent' is not supported", exception.Message); + var result = chatMessage.ToA2AMessage(); + + // Should create a message but ignore the unsupported content + Assert.NotNull(result); + Assert.Empty(result.Parts); } [Fact] @@ -389,7 +392,7 @@ public class MessageConverterTests } [Fact] - public void ConvertPartToAIContent_FilePart_ThrowsNotSupportedException() + public void ConvertPartToAIContent_FilePart_IgnoresUnsupportedPart() { var filePart = new FilePart(); var message = new AgentMessage @@ -399,12 +402,15 @@ public class MessageConverterTests Parts = [filePart] }; - var exception = Assert.Throws(() => new List { message }.ToChatMessages()); - Assert.Contains("Part type 'FilePart' is not supported", exception.Message); + var result = new List { message }.ToChatMessages(); + + // Should return empty collection since FilePart is ignored + Assert.NotNull(result); + Assert.Empty(result); } [Fact] - public void ConvertPartToAIContent_DataPart_ThrowsNotSupportedException() + public void ConvertPartToAIContent_DataPart_IgnoresUnsupportedPart() { var dataPart = new DataPart(); var message = new AgentMessage @@ -414,8 +420,11 @@ public class MessageConverterTests Parts = [dataPart] }; - var exception = Assert.Throws(() => new List { message }.ToChatMessages()); - Assert.Contains("Part type 'DataPart' is not supported", exception.Message); + var result = new List { message }.ToChatMessages(); + + // Should return empty collection since DataPart is ignored + Assert.NotNull(result); + Assert.Empty(result); } [Fact] @@ -529,4 +538,57 @@ public class MessageConverterTests var chatMessage = result.First(); Assert.Null(chatMessage.AdditionalProperties); } + + [Fact] + public void ConvertPartToAIContent_MixedPartsWithUnsupported_IgnoresUnsupportedParts() + { + var message = new AgentMessage + { + MessageId = "test", + Role = MessageRole.User, + Parts = [ + new TextPart { Text = "First part" }, + new DataPart(), // Unsupported - should be ignored + new TextPart { Text = "Second part" }, + new FilePart() // Unsupported - should be ignored + ] + }; + + var result = new List { message }.ToChatMessages(); + + Assert.NotNull(result); + Assert.Single(result); + + var chatMessage = result.First(); + Assert.Equal(2, chatMessage.Contents.Count); + + var firstContent = Assert.IsType(chatMessage.Contents[0]); + Assert.Equal("First part", firstContent.Text); + + var secondContent = Assert.IsType(chatMessage.Contents[1]); + Assert.Equal("Second part", secondContent.Text); + } + + [Fact] + public void ToA2AMessage_MixedContentWithUnsupported_IgnoresUnsupportedContent() + { + var contents = new List + { + new TextContent("First text"), + new DataContent(new byte[] { 1, 2, 3 }, "image/png"), // Unsupported - should be ignored + new TextContent("Second text") + }; + var chatMessage = new ChatMessage(ChatRole.User, contents); + + var result = chatMessage.ToA2AMessage(); + + Assert.NotNull(result); + Assert.Equal(2, result.Parts.Count); + + var firstPart = Assert.IsType(result.Parts[0]); + Assert.Equal("First text", firstPart.Text); + + var secondPart = Assert.IsType(result.Parts[1]); + Assert.Equal("Second text", secondPart.Text); + } }