mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
dfc3079d68
* .NET: Add A2A input-request content for human-in-the-loop scenarios Adds first-class support for handling user input requests from A2A agents when they return an `input-required` task state. - Add `A2AInputRequestContent` (wraps the requested `AIContent`) and `A2AInputResponseContent` (wraps the user's `AIContent` reply), with `CreateResponse` helper overloads on the request type. - Surface input requests on `AgentResponse` / `AgentResponseUpdate` via `AgentTask` and `TaskStatusUpdateEvent` mappings. - Link follow-up messages containing `A2AInputResponseContent` to the existing task via `TaskId` instead of `ReferenceTaskIds`. - Add `A2AAgent_HumanInTheLoop` sample and register it in the solution and parent README. - Add unit tests for the new types, extensions, and `A2AAgent` paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove unnecessary using directive flagged by CI format check Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * address feedback * Guard against null TaskId when sending A2AInputResponseContent Throw InvalidOperationException if TaskId is missing when the message contains A2AInputResponseContent, preventing silent no-op responses. Also adds tests for both RunAsync and RunStreamingAsync paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Leave Contents null for non-InputRequired status updates Remove unnecessary '?? []' fallback so Contents stays null when there are no input requests, matching the other update mapping patterns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use consistent GUID format for request IDs Use ToString("N") to match message ID format used elsewhere in the A2A component. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove Debug build exclusion for the HumanInTheLoop sample so it participates in normal solution validation. * Add missing using Microsoft.Extensions.AI to A2AAgent_HumanInTheLoop The sample uses ChatMessage, TextContent, and ChatRole types from Microsoft.Extensions.AI but was missing the using directive, causing CS0246 build errors on all CI jobs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * change the way user input requests are handled based on pr review comments --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
122 lines
3.5 KiB
C#
122 lines
3.5 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System.Collections.Generic;
|
|
using A2A;
|
|
using Microsoft.Extensions.AI;
|
|
|
|
namespace Microsoft.Agents.AI.A2A.UnitTests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for the <see cref="AgentTaskStatusExtensions"/> class.
|
|
/// </summary>
|
|
public sealed class AgentTaskStatusExtensionsTests
|
|
{
|
|
[Fact]
|
|
public void GetUserInputRequests_WithNullMessage_ReturnsNull()
|
|
{
|
|
// Arrange
|
|
var status = new TaskStatus
|
|
{
|
|
State = TaskState.InputRequired,
|
|
Message = null,
|
|
};
|
|
|
|
// Act
|
|
IList<AIContent>? result = status.GetUserInputRequests();
|
|
|
|
// Assert
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetUserInputRequests_WithNotInputRequiredState_ReturnsNull()
|
|
{
|
|
// Arrange
|
|
var status = new TaskStatus
|
|
{
|
|
State = TaskState.Completed,
|
|
Message = new Message { Parts = [Part.FromText("Some text")] },
|
|
};
|
|
|
|
// Act
|
|
IList<AIContent>? result = status.GetUserInputRequests();
|
|
|
|
// Assert
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetUserInputRequests_WithInputRequiredStateAndMultipleRequests_ReturnsAIContentList()
|
|
{
|
|
// Arrange
|
|
var status = new TaskStatus
|
|
{
|
|
State = TaskState.InputRequired,
|
|
Message = new Message
|
|
{
|
|
Parts =
|
|
[
|
|
Part.FromText("First request"),
|
|
Part.FromText("Second request"),
|
|
Part.FromText("Third request")
|
|
],
|
|
},
|
|
};
|
|
|
|
// Act
|
|
IList<AIContent>? result = status.GetUserInputRequests();
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal(3, result.Count);
|
|
Assert.Equal("First request", Assert.IsType<TextContent>(result[0]).Text);
|
|
Assert.Equal("Second request", Assert.IsType<TextContent>(result[1]).Text);
|
|
Assert.Equal("Third request", Assert.IsType<TextContent>(result[2]).Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetUserInputRequests_WithTextParts_SetsRawRepresentationAndAdditionalPropertiesCorrectly()
|
|
{
|
|
// Arrange
|
|
var textPart = Part.FromText("Input request");
|
|
textPart.Metadata = new Dictionary<string, System.Text.Json.JsonElement>
|
|
{
|
|
{ "key1", System.Text.Json.JsonSerializer.SerializeToElement("value1") },
|
|
{ "key2", System.Text.Json.JsonSerializer.SerializeToElement("value2") }
|
|
};
|
|
var status = new TaskStatus
|
|
{
|
|
State = TaskState.InputRequired,
|
|
Message = new Message { Parts = [textPart] },
|
|
};
|
|
|
|
// Act
|
|
IList<AIContent>? result = status.GetUserInputRequests();
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
var content = Assert.IsType<TextContent>(result[0]);
|
|
Assert.Equal(textPart, content.RawRepresentation);
|
|
Assert.NotNull(content.AdditionalProperties);
|
|
Assert.True(content.AdditionalProperties.ContainsKey("key1"));
|
|
Assert.True(content.AdditionalProperties.ContainsKey("key2"));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetUserInputRequests_WithEmptyMessageParts_ReturnsNull()
|
|
{
|
|
// Arrange
|
|
var status = new TaskStatus
|
|
{
|
|
State = TaskState.InputRequired,
|
|
Message = new Message { Parts = [] },
|
|
};
|
|
|
|
// Act
|
|
IList<AIContent>? result = status.GetUserInputRequests();
|
|
|
|
// Assert
|
|
Assert.Null(result);
|
|
}
|
|
}
|