mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.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 <stoub@microsoft.com> Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
4c5c6d0f98
commit
62854197aa
@@ -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<AIContent>()],
|
||||
AdditionalProperties = message.Metadata.ToAdditionalProperties(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 <see cref="AIContent"/> to a <see cref="Part"/> object."/>
|
||||
/// </summary>
|
||||
/// <param name="content">AI content to convert.</param>
|
||||
/// <returns>The corresponding A2A <see cref="Part"/> object.</returns>
|
||||
internal static Part ToA2APart(this AIContent content) =>
|
||||
/// <returns>The corresponding A2A <see cref="Part"/> object, or null if the content type is not supported.</returns>
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <see cref="Part"/> to an <see cref="AIContent"/>.
|
||||
/// </summary>
|
||||
/// <param name="part">The A2A part to convert.</param>
|
||||
/// <returns>The corresponding <see cref="AIContent"/>.</returns>
|
||||
internal static AIContent ToAIContent(this Part part) =>
|
||||
/// <returns>The corresponding <see cref="AIContent"/>, or null if the part type is not supported.</returns>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -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<string, JsonElement> metadata)
|
||||
|
||||
+37
-5
@@ -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<NotSupportedException>(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<AIContent>
|
||||
{
|
||||
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<TextPart>(result[0]);
|
||||
Assert.Equal("First text", firstTextPart.Text);
|
||||
|
||||
var filePart = Assert.IsType<FilePart>(result[1]);
|
||||
var fileWithUri = Assert.IsType<FileWithUri>(filePart.File);
|
||||
Assert.Equal("https://example.com/file.txt", fileWithUri.Uri);
|
||||
|
||||
var secondTextPart = Assert.IsType<TextPart>(result[2]);
|
||||
Assert.Equal("Second text", secondTextPart.Text);
|
||||
}
|
||||
|
||||
// Mock class for testing unsupported scenarios
|
||||
private sealed class MockAIContent : AIContent;
|
||||
}
|
||||
|
||||
@@ -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<NotSupportedException>(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
|
||||
|
||||
+71
-9
@@ -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<NotSupportedException>(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<NotSupportedException>(() => new List<AgentMessage> { message }.ToChatMessages());
|
||||
Assert.Contains("Part type 'FilePart' is not supported", exception.Message);
|
||||
var result = new List<AgentMessage> { 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<NotSupportedException>(() => new List<AgentMessage> { message }.ToChatMessages());
|
||||
Assert.Contains("Part type 'DataPart' is not supported", exception.Message);
|
||||
var result = new List<AgentMessage> { 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<AgentMessage> { message }.ToChatMessages();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
|
||||
var chatMessage = result.First();
|
||||
Assert.Equal(2, chatMessage.Contents.Count);
|
||||
|
||||
var firstContent = Assert.IsType<TextContent>(chatMessage.Contents[0]);
|
||||
Assert.Equal("First part", firstContent.Text);
|
||||
|
||||
var secondContent = Assert.IsType<TextContent>(chatMessage.Contents[1]);
|
||||
Assert.Equal("Second part", secondContent.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToA2AMessage_MixedContentWithUnsupported_IgnoresUnsupportedContent()
|
||||
{
|
||||
var contents = new List<AIContent>
|
||||
{
|
||||
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<TextPart>(result.Parts[0]);
|
||||
Assert.Equal("First text", firstPart.Text);
|
||||
|
||||
var secondPart = Assert.IsType<TextPart>(result.Parts[1]);
|
||||
Assert.Equal("Second text", secondPart.Text);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user