Skip to content

Commit 45556dc

Browse files
feat: Add AdditionalPropertiesExtensions with ToAdditionalProperties and ToA2AMetadata methods (#260)
* Initial plan * Add AdditionalPropertiesExtensions with ToAdditionalProperties and ToA2AMetadata methods Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> * Move AdditionalPropertiesExtensions to Microsoft.Extensions.AI namespace Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
1 parent 4951daa commit 45556dc

File tree

3 files changed

+184
-18
lines changed

3 files changed

+184
-18
lines changed

src/A2A/Extensions/AIContentExtensions.cs

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public static ChatMessage ToChatMessage(this AgentMessage agentMessage)
2525

2626
return new()
2727
{
28-
AdditionalProperties = ToAdditionalProperties(agentMessage.Metadata),
28+
AdditionalProperties = agentMessage.Metadata.ToAdditionalProperties(),
2929
Contents = agentMessage.Parts.ConvertAll(p => p.ToAIContent()),
3030
MessageId = agentMessage.MessageId,
3131
RawRepresentation = agentMessage,
@@ -106,7 +106,7 @@ public static AIContent ToAIContent(this Part part)
106106

107107
content ??= new AIContent();
108108

109-
content.AdditionalProperties = ToAdditionalProperties(part.Metadata);
109+
content.AdditionalProperties = part.Metadata.ToAdditionalProperties();
110110
content.RawRepresentation = part;
111111

112112
return content;
@@ -171,20 +171,4 @@ public static AIContent ToAIContent(this Part part)
171171

172172
return part;
173173
}
174-
175-
private static AdditionalPropertiesDictionary? ToAdditionalProperties(Dictionary<string, JsonElement>? metadata)
176-
{
177-
if (metadata is not { Count: > 0 })
178-
{
179-
return null;
180-
}
181-
182-
AdditionalPropertiesDictionary props = [];
183-
foreach (var kvp in metadata)
184-
{
185-
props[kvp.Key] = kvp.Value;
186-
}
187-
188-
return props;
189-
}
190174
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using A2A;
2+
using System.Text.Json;
3+
4+
namespace Microsoft.Extensions.AI;
5+
6+
/// <summary>
7+
/// Provides extension methods for converting between <see cref="AdditionalPropertiesDictionary"/> and A2A metadata dictionaries.
8+
/// </summary>
9+
public static class AdditionalPropertiesExtensions
10+
{
11+
/// <summary>
12+
/// Creates an <see cref="AdditionalPropertiesDictionary"/> from A2A metadata.
13+
/// </summary>
14+
/// <param name="metadata">The A2A metadata dictionary to convert.</param>
15+
/// <returns>An <see cref="AdditionalPropertiesDictionary"/> containing the metadata, or <see langword="null"/> if the metadata is empty.</returns>
16+
public static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary<string, JsonElement>? metadata)
17+
{
18+
if (metadata is not { Count: > 0 })
19+
{
20+
return null;
21+
}
22+
23+
AdditionalPropertiesDictionary props = [];
24+
foreach (var kvp in metadata)
25+
{
26+
props[kvp.Key] = kvp.Value;
27+
}
28+
29+
return props;
30+
}
31+
32+
/// <summary>
33+
/// Creates an A2A metadata dictionary from an <see cref="AdditionalPropertiesDictionary"/>.
34+
/// </summary>
35+
/// <param name="additionalProperties">The additional properties dictionary to convert.</param>
36+
/// <returns>A dictionary of A2A metadata, or <see langword="null"/> if the additional properties dictionary is empty.</returns>
37+
public static Dictionary<string, JsonElement>? ToA2AMetadata(this AdditionalPropertiesDictionary? additionalProperties)
38+
{
39+
if (additionalProperties is not { Count: > 0 })
40+
{
41+
return null;
42+
}
43+
44+
var metadata = new Dictionary<string, JsonElement>();
45+
46+
foreach (var kvp in additionalProperties)
47+
{
48+
metadata[kvp.Key] = JsonSerializer.SerializeToElement(kvp.Value, A2AJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
49+
}
50+
51+
return metadata;
52+
}
53+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using System.Text.Json;
2+
using Microsoft.Extensions.AI;
3+
4+
namespace A2A.UnitTests.Models
5+
{
6+
public sealed class AdditionalPropertiesExtensionsTests
7+
{
8+
[Fact]
9+
public void ToAdditionalProperties_ReturnsNullForNullMetadata()
10+
{
11+
Dictionary<string, JsonElement>? metadata = null;
12+
var result = metadata.ToAdditionalProperties();
13+
Assert.Null(result);
14+
}
15+
16+
[Fact]
17+
public void ToAdditionalProperties_ReturnsNullForEmptyMetadata()
18+
{
19+
var metadata = new Dictionary<string, JsonElement>();
20+
var result = metadata.ToAdditionalProperties();
21+
Assert.Null(result);
22+
}
23+
24+
[Fact]
25+
public void ToAdditionalProperties_ConvertsMetadataToAdditionalProperties()
26+
{
27+
var metadata = new Dictionary<string, JsonElement>
28+
{
29+
["num"] = JsonSerializer.SerializeToElement(42),
30+
["str"] = JsonSerializer.SerializeToElement("value"),
31+
["bool"] = JsonSerializer.SerializeToElement(true)
32+
};
33+
34+
var result = metadata.ToAdditionalProperties();
35+
36+
Assert.NotNull(result);
37+
Assert.Equal(3, result!.Count);
38+
Assert.True(result.TryGetValue("num", out var numObj));
39+
Assert.True(result.TryGetValue("str", out var strObj));
40+
Assert.True(result.TryGetValue("bool", out var boolObj));
41+
var numJe = Assert.IsType<JsonElement>(numObj);
42+
var strJe = Assert.IsType<JsonElement>(strObj);
43+
var boolJe = Assert.IsType<JsonElement>(boolObj);
44+
Assert.Equal(42, numJe.GetInt32());
45+
Assert.Equal("value", strJe.GetString());
46+
Assert.True(boolJe.GetBoolean());
47+
}
48+
49+
[Fact]
50+
public void ToA2AMetadata_ReturnsNullForNullAdditionalProperties()
51+
{
52+
AdditionalPropertiesDictionary? additionalProperties = null;
53+
var result = additionalProperties.ToA2AMetadata();
54+
Assert.Null(result);
55+
}
56+
57+
[Fact]
58+
public void ToA2AMetadata_ReturnsNullForEmptyAdditionalProperties()
59+
{
60+
var additionalProperties = new AdditionalPropertiesDictionary();
61+
var result = additionalProperties.ToA2AMetadata();
62+
Assert.Null(result);
63+
}
64+
65+
[Fact]
66+
public void ToA2AMetadata_ConvertsAdditionalPropertiesToMetadata()
67+
{
68+
var additionalProperties = new AdditionalPropertiesDictionary
69+
{
70+
["num"] = 42,
71+
["str"] = "value",
72+
["bool"] = true
73+
};
74+
75+
var result = additionalProperties.ToA2AMetadata();
76+
77+
Assert.NotNull(result);
78+
Assert.Equal(3, result!.Count);
79+
Assert.True(result.TryGetValue("num", out var numJe));
80+
Assert.True(result.TryGetValue("str", out var strJe));
81+
Assert.True(result.TryGetValue("bool", out var boolJe));
82+
Assert.Equal(42, numJe.GetInt32());
83+
Assert.Equal("value", strJe.GetString());
84+
Assert.True(boolJe.GetBoolean());
85+
}
86+
87+
[Fact]
88+
public void ToA2AMetadata_HandlesJsonElementValues()
89+
{
90+
var additionalProperties = new AdditionalPropertiesDictionary
91+
{
92+
["nested"] = JsonSerializer.SerializeToElement(new { key = "val" })
93+
};
94+
95+
var result = additionalProperties.ToA2AMetadata();
96+
97+
Assert.NotNull(result);
98+
Assert.Single(result!);
99+
Assert.True(result.TryGetValue("nested", out var nestedJe));
100+
Assert.Equal(JsonValueKind.Object, nestedJe.ValueKind);
101+
Assert.Equal("val", nestedJe.GetProperty("key").GetString());
102+
}
103+
104+
[Fact]
105+
public void RoundTrip_ToA2AMetadata_And_ToAdditionalProperties()
106+
{
107+
var originalProps = new AdditionalPropertiesDictionary
108+
{
109+
["number"] = 123,
110+
["text"] = "hello"
111+
};
112+
113+
var metadata = originalProps.ToA2AMetadata();
114+
Assert.NotNull(metadata);
115+
116+
var restoredProps = metadata.ToAdditionalProperties();
117+
Assert.NotNull(restoredProps);
118+
Assert.Equal(2, restoredProps!.Count);
119+
120+
// Values are now JsonElements after the round trip
121+
Assert.True(restoredProps.TryGetValue("number", out var numObj));
122+
Assert.True(restoredProps.TryGetValue("text", out var textObj));
123+
var numJe = Assert.IsType<JsonElement>(numObj);
124+
var textJe = Assert.IsType<JsonElement>(textObj);
125+
Assert.Equal(123, numJe.GetInt32());
126+
Assert.Equal("hello", textJe.GetString());
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)