.NET: Map additional props <-> A2A metadata (#3137)

* map additional props from agent run options to a2a request metadata

* small touches

* add unit tests for new extension methods

* Sort using

* add unit test

* add additiona unit tests

* special case json element to avoid unnecessary serialization
This commit is contained in:
SergeyMenshykh
2026-01-08 14:05:16 +00:00
committed by GitHub
parent 299a5110ed
commit 33888641ec
10 changed files with 907 additions and 7 deletions
+14 -6
View File
@@ -84,9 +84,13 @@ public sealed class A2AAgent : AIAgent
}
else
{
var a2aMessage = CreateA2AMessage(typedThread, messages);
MessageSendParams sendParams = new()
{
Message = CreateA2AMessage(typedThread, messages),
Metadata = options?.AdditionalProperties?.ToA2AMetadata()
};
a2aResponse = await this._a2aClient.SendMessageAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
a2aResponse = await this._a2aClient.SendMessageAsync(sendParams, cancellationToken).ConfigureAwait(false);
}
this._logger.LogAgentChatClientInvokedAgent(nameof(RunAsync), this.Id, this.Name);
@@ -154,9 +158,13 @@ public sealed class A2AAgent : AIAgent
// a2aSseEvents = this._a2aClient.SubscribeToTaskAsync(token.TaskId, cancellationToken).ConfigureAwait(false);
}
var a2aMessage = CreateA2AMessage(typedThread, messages);
MessageSendParams sendParams = new()
{
Message = CreateA2AMessage(typedThread, messages),
Metadata = options?.AdditionalProperties?.ToA2AMetadata()
};
a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(new MessageSendParams { Message = a2aMessage }, cancellationToken).ConfigureAwait(false);
a2aSseEvents = this._a2aClient.SendMessageStreamingAsync(sendParams, cancellationToken).ConfigureAwait(false);
this._logger.LogAgentChatClientInvokedAgent(nameof(RunStreamingAsync), this.Id, this.Name);
@@ -198,10 +206,10 @@ public sealed class A2AAgent : AIAgent
protected override string? IdCore => this._id;
/// <inheritdoc/>
public override string? Name => this._name ?? base.Name;
public override string? Name => this._name;
/// <inheritdoc/>
public override string? Description => this._description ?? base.Description;
public override string? Description => this._description;
private A2AAgentThread GetA2AThread(AgentThread? thread, AgentRunOptions? options)
{
@@ -14,6 +14,9 @@ internal static class A2AMetadataExtensions
/// <summary>
/// Converts a dictionary of metadata to an <see cref="AdditionalPropertiesDictionary"/>.
/// </summary>
/// <remarks>
/// This method can be replaced by the one from A2A SDK once it is public.
/// </remarks>
/// <param name="metadata">The metadata dictionary to convert.</param>
/// <returns>The converted <see cref="AdditionalPropertiesDictionary"/>, or null if the input is null or empty.</returns>
internal static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary<string, JsonElement>? metadata)
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Agents.AI;
namespace Microsoft.Extensions.AI;
/// <summary>
/// Extension methods for AdditionalPropertiesDictionary.
/// </summary>
internal static class AdditionalPropertiesDictionaryExtensions
{
/// <summary>
/// Converts an <see cref="AdditionalPropertiesDictionary"/> to a dictionary of <see cref="JsonElement"/> values suitable for A2A metadata.
/// </summary>
/// <remarks>
/// This method can be replaced by the one from A2A SDK once it is available.
/// </remarks>
/// <param name="additionalProperties">The additional properties dictionary to convert, or <c>null</c>.</param>
/// <returns>A dictionary of JSON elements representing the metadata, or <c>null</c> if the input is null or empty.</returns>
internal static Dictionary<string, JsonElement>? ToA2AMetadata(this AdditionalPropertiesDictionary? additionalProperties)
{
if (additionalProperties is not { Count: > 0 })
{
return null;
}
var metadata = new Dictionary<string, JsonElement>();
foreach (var kvp in additionalProperties)
{
if (kvp.Value is JsonElement)
{
metadata[kvp.Key] = (JsonElement)kvp.Value!;
continue;
}
metadata[kvp.Key] = JsonSerializer.SerializeToElement(kvp.Value, A2AJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
}
return metadata;
}
}
@@ -43,10 +43,14 @@ public static class AIAgentExtensions
{
var contextId = messageSendParams.Message.ContextId ?? Guid.NewGuid().ToString("N");
var thread = await hostAgent.GetOrCreateThreadAsync(contextId, cancellationToken).ConfigureAwait(false);
var options = messageSendParams.Metadata is not { Count: > 0 }
? null
: new AgentRunOptions { AdditionalProperties = messageSendParams.Metadata.ToAdditionalProperties() };
var response = await hostAgent.RunAsync(
messageSendParams.ToChatMessages(),
thread: thread,
options: options,
cancellationToken: cancellationToken).ConfigureAwait(false);
await hostAgent.SaveThreadAsync(contextId, thread, cancellationToken).ConfigureAwait(false);
@@ -56,7 +60,8 @@ public static class AIAgentExtensions
MessageId = response.ResponseId ?? Guid.NewGuid().ToString("N"),
ContextId = contextId,
Role = MessageRole.Agent,
Parts = parts
Parts = parts,
Metadata = response.AdditionalProperties?.ToA2AMetadata()
};
}
}
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Hosting.A2A.Converters;
/// <summary>
/// Extension methods for A2A metadata dictionary.
/// </summary>
internal static class A2AMetadataExtensions
{
/// <summary>
/// Converts a dictionary of metadata to an <see cref="AdditionalPropertiesDictionary"/>.
/// </summary>
/// <remarks>
/// This method can be replaced by the one from A2A SDK once it is public.
/// </remarks>
/// <param name="metadata">The metadata dictionary to convert.</param>
/// <returns>The converted <see cref="AdditionalPropertiesDictionary"/>, or null if the input is null or empty.</returns>
internal static AdditionalPropertiesDictionary? ToAdditionalProperties(this Dictionary<string, JsonElement>? metadata)
{
if (metadata is not { Count: > 0 })
{
return null;
}
var additionalProperties = new AdditionalPropertiesDictionary();
foreach (var kvp in metadata)
{
additionalProperties[kvp.Key] = kvp.Value;
}
return additionalProperties;
}
}
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using A2A;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Hosting.A2A.Converters;
/// <summary>
/// Extension methods for AdditionalPropertiesDictionary.
/// </summary>
internal static class AdditionalPropertiesDictionaryExtensions
{
/// <summary>
/// Converts an <see cref="AdditionalPropertiesDictionary"/> to a dictionary of <see cref="JsonElement"/> values suitable for A2A metadata.
/// </summary>
/// <remarks>
/// This method can be replaced by the one from A2A SDK once it is available.
/// </remarks>
/// <param name="additionalProperties">The additional properties dictionary to convert, or <c>null</c>.</param>
/// <returns>A dictionary of JSON elements representing the metadata, or <c>null</c> if the input is null or empty.</returns>
internal static Dictionary<string, JsonElement>? ToA2AMetadata(this AdditionalPropertiesDictionary? additionalProperties)
{
if (additionalProperties is not { Count: > 0 })
{
return null;
}
var metadata = new Dictionary<string, JsonElement>();
foreach (var kvp in additionalProperties)
{
if (kvp.Value is JsonElement)
{
metadata[kvp.Key] = (JsonElement)kvp.Value!;
continue;
}
metadata[kvp.Key] = JsonSerializer.SerializeToElement(kvp.Value, A2AJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
}
return metadata;
}
}