Skip to content

Commit 6285419

Browse files
CopilotstephentoubSergeyMenshykh
authored
.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 <[email protected]> * Fix dotnet format --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: stephentoub <[email protected]> Co-authored-by: Stephen Toub <[email protected]> Co-authored-by: SergeyMenshykh <[email protected]>
1 parent 4c5c6d0 commit 6285419

File tree

9 files changed

+142
-33
lines changed

9 files changed

+142
-33
lines changed

dotnet/src/Microsoft.Agents.AI.A2A/A2AAgent.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync
159159
RawRepresentation = message,
160160
Role = ChatRole.Assistant,
161161
MessageId = message.MessageId,
162-
Contents = [.. message.Parts.Select(part => part.ToAIContent())],
162+
Contents = [.. message.Parts.Select(part => part.ToAIContent()).OfType<AIContent>()],
163163
AdditionalProperties = message.Metadata.ToAdditionalProperties(),
164164
};
165165
}

dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AAIContentExtensions.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
using System;
43
using System.Collections.Generic;
54
using A2A;
65

@@ -22,7 +21,11 @@ internal static class A2AAIContentExtensions
2221

2322
foreach (var content in contents)
2423
{
25-
(parts ??= []).Add(content.ToA2APart());
24+
var part = content.ToA2APart();
25+
if (part is not null)
26+
{
27+
(parts ??= []).Add(part);
28+
}
2629
}
2730

2831
return parts;
@@ -32,12 +35,13 @@ internal static class A2AAIContentExtensions
3235
/// Converts a <see cref="AIContent"/> to a <see cref="Part"/> object."/>
3336
/// </summary>
3437
/// <param name="content">AI content to convert.</param>
35-
/// <returns>The corresponding A2A <see cref="Part"/> object.</returns>
36-
internal static Part ToA2APart(this AIContent content) =>
38+
/// <returns>The corresponding A2A <see cref="Part"/> object, or null if the content type is not supported.</returns>
39+
internal static Part? ToA2APart(this AIContent content) =>
3740
content switch
3841
{
3942
TextContent textContent => new TextPart { Text = textContent.Text },
4043
HostedFileContent hostedFileContent => new FilePart { File = new FileWithUri { Uri = hostedFileContent.FileId } },
41-
_ => throw new NotSupportedException($"Unsupported content type: {content.GetType().Name}."),
44+
// Ignore unknown content types (FunctionCallContent, FunctionResultContent, etc.)
45+
_ => null,
4246
};
4347
}

dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AArtifactExtensions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ internal static ChatMessage ToChatMessage(this Artifact artifact)
1616

1717
foreach (var part in artifact.Parts)
1818
{
19-
(aiContents ??= []).Add(part.ToAIContent());
19+
var content = part.ToAIContent();
20+
if (content is not null)
21+
{
22+
(aiContents ??= []).Add(content);
23+
}
2024
}
2125

2226
return new ChatMessage(ChatRole.Assistant, aiContents)

dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2AMessageExtensions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ internal static ChatMessage ToChatMessage(this AgentMessage message)
1616

1717
foreach (var part in message.Parts)
1818
{
19-
(aiContents ??= []).Add(part.ToAIContent());
19+
var content = part.ToAIContent();
20+
if (content is not null)
21+
{
22+
(aiContents ??= []).Add(content);
23+
}
2024
}
2125

2226
return new ChatMessage(ChatRole.Assistant, aiContents)

dotnet/src/Microsoft.Agents.AI.A2A/Extensions/A2APartExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
using System;
43
using Microsoft.Extensions.AI;
54

65
namespace A2A;
@@ -14,8 +13,8 @@ internal static class A2APartExtensions
1413
/// Converts an A2A <see cref="Part"/> to an <see cref="AIContent"/>.
1514
/// </summary>
1615
/// <param name="part">The A2A part to convert.</param>
17-
/// <returns>The corresponding <see cref="AIContent"/>.</returns>
18-
internal static AIContent ToAIContent(this Part part) =>
16+
/// <returns>The corresponding <see cref="AIContent"/>, or null if the part type is not supported.</returns>
17+
internal static AIContent? ToAIContent(this Part part) =>
1918
part switch
2019
{
2120
TextPart textPart => new TextContent(textPart.Text)
@@ -30,6 +29,7 @@ internal static AIContent ToAIContent(this Part part) =>
3029
AdditionalProperties = filePart.Metadata.ToAdditionalProperties()
3130
},
3231

33-
_ => throw new NotSupportedException($"Part type '{part.GetType().Name}' is not supported.")
32+
// Ignore unknown part types (DataPart, etc.)
33+
_ => null
3434
};
3535
}

dotnet/src/Microsoft.Agents.AI.Hosting.A2A/Converters/MessageConverter.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@ public static IReadOnlyCollection<ChatMessage> ToChatMessages(this ICollection<A
160160
RawRepresentation = textPart,
161161
AdditionalProperties = textPart.Metadata?.ToAdditionalPropertiesDictionary()
162162
},
163-
FilePart or DataPart or _ => throw new NotSupportedException($"Part type '{part.GetType().Name}' is not supported. Only TextPart is supported.")
163+
// Ignore unknown content types (FilePart, DataPart, etc.)
164+
_ => null
164165
};
165166

166167
/// <summary>
@@ -234,7 +235,8 @@ private static MessageRole ConvertChatRoleToMessageRole(ChatRole chatRole)
234235
{
235236
Text = textContent.Text
236237
},
237-
_ => throw new NotSupportedException($"Content type '{content.GetType().Name}' is not supported.")
238+
// Ignore unknown content types (FunctionCallContent, FunctionResultContent, etc.)
239+
_ => null
238240
};
239241

240242
private static AdditionalPropertiesDictionary? ToAdditionalPropertiesDictionary(this Dictionary<string, JsonElement> metadata)

dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2AAIContentExtensionsTests.cs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
using System;
43
using System.Collections.Generic;
54
using A2A;
65
using Microsoft.Extensions.AI;
@@ -49,14 +48,16 @@ public void ToA2APart_WithHostedFileContent_ReturnsFilePart()
4948
}
5049

5150
[Fact]
52-
public void ToA2APart_WithUnsupportedContentType_ThrowsNotSupportedException()
51+
public void ToA2APart_WithUnsupportedContentType_ReturnsNull()
5352
{
5453
// Arrange
5554
var unsupportedContent = new MockAIContent();
5655

57-
// Act & Assert
58-
var exception = Assert.Throws<NotSupportedException>(unsupportedContent.ToA2APart);
59-
Assert.Equal("Unsupported content type: MockAIContent.", exception.Message);
56+
// Act
57+
var result = unsupportedContent.ToA2APart();
58+
59+
// Assert
60+
Assert.Null(result);
6061
}
6162

6263
[Fact]
@@ -106,6 +107,37 @@ public void ToA2AParts_WithMultipleContents_ReturnsListWithAllParts()
106107
Assert.Equal("https://example.com/file2.txt", secondFileWithUri.Uri);
107108
}
108109

110+
[Fact]
111+
public void ToA2AParts_WithMixedSupportedAndUnsupportedContent_IgnoresUnsupportedContent()
112+
{
113+
// Arrange
114+
var contents = new List<AIContent>
115+
{
116+
new TextContent("First text"),
117+
new MockAIContent(), // Unsupported - should be ignored
118+
new HostedFileContent("https://example.com/file.txt"),
119+
new MockAIContent(), // Unsupported - should be ignored
120+
new TextContent("Second text")
121+
};
122+
123+
// Act
124+
var result = contents.ToA2AParts();
125+
126+
// Assert
127+
Assert.NotNull(result);
128+
Assert.Equal(3, result.Count);
129+
130+
var firstTextPart = Assert.IsType<TextPart>(result[0]);
131+
Assert.Equal("First text", firstTextPart.Text);
132+
133+
var filePart = Assert.IsType<FilePart>(result[1]);
134+
var fileWithUri = Assert.IsType<FileWithUri>(filePart.File);
135+
Assert.Equal("https://example.com/file.txt", fileWithUri.Uri);
136+
137+
var secondTextPart = Assert.IsType<TextPart>(result[2]);
138+
Assert.Equal("Second text", secondTextPart.Text);
139+
}
140+
109141
// Mock class for testing unsupported scenarios
110142
private sealed class MockAIContent : AIContent;
111143
}

dotnet/tests/Microsoft.Agents.AI.A2A.UnitTests/Extensions/A2APartExtensionsTests.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright (c) Microsoft. All rights reserved.
22

3-
using System;
43
using System.Collections.Generic;
54
using System.Text.Json;
65
using A2A;
@@ -80,14 +79,16 @@ public void ToAIContent_WithFilePartWithFileWithUri_ReturnsHostedFileContent()
8079
}
8180

8281
[Fact]
83-
public void ToAIContent_WithCustomPartType_ThrowsNotSupportedException()
82+
public void ToAIContent_WithCustomPartType_ReturnsNull()
8483
{
8584
// Arrange
8685
var customPart = new MockPart();
8786

88-
// Act & Assert
89-
var exception = Assert.Throws<NotSupportedException>(customPart.ToAIContent);
90-
Assert.Equal("Part type 'MockPart' is not supported.", exception.Message);
87+
// Act
88+
var result = customPart.ToAIContent();
89+
90+
// Assert
91+
Assert.Null(result);
9192
}
9293

9394
// Mock class for testing unsupported scenarios

dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.Tests/Converters/MessageConverterTests.cs

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -228,13 +228,16 @@ public void ToA2AMessage_ChatMessageWithTextContent_ReturnsCorrectTextPart()
228228
}
229229

230230
[Fact]
231-
public void ToA2AMessage_ChatMessageWithUnsupportedContent_ThrowsNotSupportedException()
231+
public void ToA2AMessage_ChatMessageWithUnsupportedContent_IgnoresUnsupportedContent()
232232
{
233233
var unsupportedContent = new DataContent(new byte[] { 1, 2, 3 }, "image/png");
234234
var chatMessage = new ChatMessage(ChatRole.User, [unsupportedContent]);
235235

236-
var exception = Assert.Throws<NotSupportedException>(chatMessage.ToA2AMessage);
237-
Assert.Contains("Content type 'DataContent' is not supported", exception.Message);
236+
var result = chatMessage.ToA2AMessage();
237+
238+
// Should create a message but ignore the unsupported content
239+
Assert.NotNull(result);
240+
Assert.Empty(result.Parts);
238241
}
239242

240243
[Fact]
@@ -389,7 +392,7 @@ public void ConvertPartToAIContent_TextPartWithMetadata_PreservesMetadata()
389392
}
390393

391394
[Fact]
392-
public void ConvertPartToAIContent_FilePart_ThrowsNotSupportedException()
395+
public void ConvertPartToAIContent_FilePart_IgnoresUnsupportedPart()
393396
{
394397
var filePart = new FilePart();
395398
var message = new AgentMessage
@@ -399,12 +402,15 @@ public void ConvertPartToAIContent_FilePart_ThrowsNotSupportedException()
399402
Parts = [filePart]
400403
};
401404

402-
var exception = Assert.Throws<NotSupportedException>(() => new List<AgentMessage> { message }.ToChatMessages());
403-
Assert.Contains("Part type 'FilePart' is not supported", exception.Message);
405+
var result = new List<AgentMessage> { message }.ToChatMessages();
406+
407+
// Should return empty collection since FilePart is ignored
408+
Assert.NotNull(result);
409+
Assert.Empty(result);
404410
}
405411

406412
[Fact]
407-
public void ConvertPartToAIContent_DataPart_ThrowsNotSupportedException()
413+
public void ConvertPartToAIContent_DataPart_IgnoresUnsupportedPart()
408414
{
409415
var dataPart = new DataPart();
410416
var message = new AgentMessage
@@ -414,8 +420,11 @@ public void ConvertPartToAIContent_DataPart_ThrowsNotSupportedException()
414420
Parts = [dataPart]
415421
};
416422

417-
var exception = Assert.Throws<NotSupportedException>(() => new List<AgentMessage> { message }.ToChatMessages());
418-
Assert.Contains("Part type 'DataPart' is not supported", exception.Message);
423+
var result = new List<AgentMessage> { message }.ToChatMessages();
424+
425+
// Should return empty collection since DataPart is ignored
426+
Assert.NotNull(result);
427+
Assert.Empty(result);
419428
}
420429

421430
[Fact]
@@ -529,4 +538,57 @@ public void ToAdditionalPropertiesDictionary_EmptyMetadata_ReturnsNull()
529538
var chatMessage = result.First();
530539
Assert.Null(chatMessage.AdditionalProperties);
531540
}
541+
542+
[Fact]
543+
public void ConvertPartToAIContent_MixedPartsWithUnsupported_IgnoresUnsupportedParts()
544+
{
545+
var message = new AgentMessage
546+
{
547+
MessageId = "test",
548+
Role = MessageRole.User,
549+
Parts = [
550+
new TextPart { Text = "First part" },
551+
new DataPart(), // Unsupported - should be ignored
552+
new TextPart { Text = "Second part" },
553+
new FilePart() // Unsupported - should be ignored
554+
]
555+
};
556+
557+
var result = new List<AgentMessage> { message }.ToChatMessages();
558+
559+
Assert.NotNull(result);
560+
Assert.Single(result);
561+
562+
var chatMessage = result.First();
563+
Assert.Equal(2, chatMessage.Contents.Count);
564+
565+
var firstContent = Assert.IsType<TextContent>(chatMessage.Contents[0]);
566+
Assert.Equal("First part", firstContent.Text);
567+
568+
var secondContent = Assert.IsType<TextContent>(chatMessage.Contents[1]);
569+
Assert.Equal("Second part", secondContent.Text);
570+
}
571+
572+
[Fact]
573+
public void ToA2AMessage_MixedContentWithUnsupported_IgnoresUnsupportedContent()
574+
{
575+
var contents = new List<AIContent>
576+
{
577+
new TextContent("First text"),
578+
new DataContent(new byte[] { 1, 2, 3 }, "image/png"), // Unsupported - should be ignored
579+
new TextContent("Second text")
580+
};
581+
var chatMessage = new ChatMessage(ChatRole.User, contents);
582+
583+
var result = chatMessage.ToA2AMessage();
584+
585+
Assert.NotNull(result);
586+
Assert.Equal(2, result.Parts.Count);
587+
588+
var firstPart = Assert.IsType<TextPart>(result.Parts[0]);
589+
Assert.Equal("First text", firstPart.Text);
590+
591+
var secondPart = Assert.IsType<TextPart>(result.Parts[1]);
592+
Assert.Equal("Second text", secondPart.Text);
593+
}
532594
}

0 commit comments

Comments
 (0)