test
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,539 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="AGUIEndpointRouteBuilderExtensions"/> class.
|
||||
/// </summary>
|
||||
public sealed class AGUIEndpointRouteBuilderExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void MapAGUIAgent_MapsEndpoint_AtSpecifiedPattern()
|
||||
{
|
||||
// Arrange
|
||||
Mock<IEndpointRouteBuilder> endpointsMock = new();
|
||||
Mock<IServiceProvider> serviceProviderMock = new();
|
||||
|
||||
endpointsMock.Setup(e => e.ServiceProvider).Returns(serviceProviderMock.Object);
|
||||
endpointsMock.Setup(e => e.DataSources).Returns([]);
|
||||
|
||||
const string Pattern = "/api/agent";
|
||||
AIAgent agent = new TestAgent();
|
||||
|
||||
// Act
|
||||
IEndpointConventionBuilder? result = endpointsMock.Object.MapAGUI(Pattern, agent);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAGUIAgent_WithNullOrInvalidInput_Returns400BadRequestAsync()
|
||||
{
|
||||
// Arrange
|
||||
DefaultHttpContext context = new();
|
||||
context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("invalid json"));
|
||||
context.RequestAborted = CancellationToken.None;
|
||||
|
||||
RequestDelegate handler = this.CreateRequestDelegate((messages, tools, ctx, props) => new TestAgent());
|
||||
|
||||
// Act
|
||||
await handler(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAGUIAgent_InvokesAgentFactory_WithCorrectMessagesAndContextAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage>? capturedMessages = null;
|
||||
IEnumerable<KeyValuePair<string, string>>? capturedContext = null;
|
||||
|
||||
AIAgent factory(IEnumerable<ChatMessage> messages, IEnumerable<AITool> tools, IEnumerable<KeyValuePair<string, string>> context, JsonElement props)
|
||||
{
|
||||
capturedMessages = messages.ToList();
|
||||
capturedContext = context;
|
||||
return new TestAgent();
|
||||
}
|
||||
|
||||
DefaultHttpContext httpContext = new();
|
||||
RunAgentInput input = new()
|
||||
{
|
||||
ThreadId = "thread1",
|
||||
RunId = "run1",
|
||||
Messages = [new AGUIUserMessage { Id = "m1", Content = "Test" }],
|
||||
Context = [new AGUIContextItem { Description = "key1", Value = "value1" }]
|
||||
};
|
||||
string json = JsonSerializer.Serialize(input, AGUIJsonSerializerContext.Default.RunAgentInput);
|
||||
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
|
||||
RequestDelegate handler = this.CreateRequestDelegate(factory);
|
||||
|
||||
// Act
|
||||
await handler(httpContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedMessages);
|
||||
Assert.Single(capturedMessages);
|
||||
Assert.Equal("Test", capturedMessages[0].Text);
|
||||
Assert.NotNull(capturedContext);
|
||||
Assert.Contains(capturedContext, kvp => kvp.Key == "key1" && kvp.Value == "value1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAGUIAgent_ReturnsSSEResponseStream_WithCorrectContentTypeAsync()
|
||||
{
|
||||
// Arrange
|
||||
DefaultHttpContext httpContext = new();
|
||||
RunAgentInput input = new()
|
||||
{
|
||||
ThreadId = "thread1",
|
||||
RunId = "run1",
|
||||
Messages = [new AGUIUserMessage { Id = "m1", Content = "Test" }]
|
||||
};
|
||||
string json = JsonSerializer.Serialize(input, AGUIJsonSerializerContext.Default.RunAgentInput);
|
||||
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
|
||||
RequestDelegate handler = this.CreateRequestDelegate((messages, tools, context, props) => new TestAgent());
|
||||
|
||||
// Act
|
||||
await handler(httpContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("text/event-stream", httpContext.Response.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAGUIAgent_PassesCancellationToken_ToAgentExecutionAsync()
|
||||
{
|
||||
// Arrange
|
||||
using CancellationTokenSource cts = new();
|
||||
cts.Cancel();
|
||||
|
||||
DefaultHttpContext httpContext = new();
|
||||
RunAgentInput input = new()
|
||||
{
|
||||
ThreadId = "thread1",
|
||||
RunId = "run1",
|
||||
Messages = [new AGUIUserMessage { Id = "m1", Content = "Test" }]
|
||||
};
|
||||
string json = JsonSerializer.Serialize(input, AGUIJsonSerializerContext.Default.RunAgentInput);
|
||||
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
httpContext.RequestAborted = cts.Token;
|
||||
|
||||
RequestDelegate handler = this.CreateRequestDelegate((messages, tools, context, props) => new TestAgent());
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => handler(httpContext));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAGUIAgent_ConvertsInputMessages_ToChatMessagesBeforeFactoryAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<ChatMessage>? capturedMessages = null;
|
||||
|
||||
AIAgent factory(IEnumerable<ChatMessage> messages, IEnumerable<AITool> tools, IEnumerable<KeyValuePair<string, string>> context, JsonElement props)
|
||||
{
|
||||
capturedMessages = messages.ToList();
|
||||
return new TestAgent();
|
||||
}
|
||||
|
||||
DefaultHttpContext httpContext = new();
|
||||
RunAgentInput input = new()
|
||||
{
|
||||
ThreadId = "thread1",
|
||||
RunId = "run1",
|
||||
Messages =
|
||||
[
|
||||
new AGUIUserMessage { Id = "m1", Content = "First" },
|
||||
new AGUIAssistantMessage { Id = "m2", Content = "Second" }
|
||||
]
|
||||
};
|
||||
string json = JsonSerializer.Serialize(input, AGUIJsonSerializerContext.Default.RunAgentInput);
|
||||
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
|
||||
RequestDelegate handler = this.CreateRequestDelegate(factory);
|
||||
|
||||
// Act
|
||||
await handler(httpContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedMessages);
|
||||
Assert.Equal(2, capturedMessages.Count);
|
||||
Assert.Equal(ChatRole.User, capturedMessages[0].Role);
|
||||
Assert.Equal("First", capturedMessages[0].Text);
|
||||
Assert.Equal(ChatRole.Assistant, capturedMessages[1].Role);
|
||||
Assert.Equal("Second", capturedMessages[1].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAGUIAgent_ProducesValidAGUIEventStream_WithRunStartAndFinishAsync()
|
||||
{
|
||||
// Arrange
|
||||
DefaultHttpContext httpContext = new();
|
||||
RunAgentInput input = new()
|
||||
{
|
||||
ThreadId = "thread1",
|
||||
RunId = "run1",
|
||||
Messages = [new AGUIUserMessage { Id = "m1", Content = "Test" }]
|
||||
};
|
||||
string json = JsonSerializer.Serialize(input, AGUIJsonSerializerContext.Default.RunAgentInput);
|
||||
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
|
||||
MemoryStream responseStream = new();
|
||||
httpContext.Response.Body = responseStream;
|
||||
|
||||
RequestDelegate handler = this.CreateRequestDelegate((messages, tools, context, props) => new TestAgent());
|
||||
|
||||
// Act
|
||||
await handler(httpContext);
|
||||
|
||||
// Assert
|
||||
responseStream.Position = 0;
|
||||
string responseContent = Encoding.UTF8.GetString(responseStream.ToArray());
|
||||
|
||||
List<JsonElement> events = ParseSseEvents(responseContent);
|
||||
|
||||
JsonElement runStarted = Assert.Single(events, static e => e.GetProperty("type").GetString() == AGUIEventTypes.RunStarted);
|
||||
JsonElement runFinished = Assert.Single(events, static e => e.GetProperty("type").GetString() == AGUIEventTypes.RunFinished);
|
||||
|
||||
Assert.Equal("thread1", runStarted.GetProperty("threadId").GetString());
|
||||
Assert.Equal("run1", runStarted.GetProperty("runId").GetString());
|
||||
Assert.Equal("thread1", runFinished.GetProperty("threadId").GetString());
|
||||
Assert.Equal("run1", runFinished.GetProperty("runId").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAGUIAgent_ProducesTextMessageEvents_InCorrectOrderAsync()
|
||||
{
|
||||
// Arrange
|
||||
DefaultHttpContext httpContext = new();
|
||||
RunAgentInput input = new()
|
||||
{
|
||||
ThreadId = "thread1",
|
||||
RunId = "run1",
|
||||
Messages = [new AGUIUserMessage { Id = "m1", Content = "Hello" }]
|
||||
};
|
||||
string json = JsonSerializer.Serialize(input, AGUIJsonSerializerContext.Default.RunAgentInput);
|
||||
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
|
||||
MemoryStream responseStream = new();
|
||||
httpContext.Response.Body = responseStream;
|
||||
|
||||
RequestDelegate handler = this.CreateRequestDelegate((messages, tools, context, props) => new TestAgent());
|
||||
|
||||
// Act
|
||||
await handler(httpContext);
|
||||
|
||||
// Assert
|
||||
responseStream.Position = 0;
|
||||
string responseContent = Encoding.UTF8.GetString(responseStream.ToArray());
|
||||
|
||||
List<JsonElement> events = ParseSseEvents(responseContent);
|
||||
List<string?> eventTypes = new(events.Count);
|
||||
foreach (JsonElement evt in events)
|
||||
{
|
||||
eventTypes.Add(evt.GetProperty("type").GetString());
|
||||
}
|
||||
|
||||
Assert.Contains(AGUIEventTypes.RunStarted, eventTypes);
|
||||
Assert.Contains(AGUIEventTypes.TextMessageContent, eventTypes);
|
||||
Assert.Contains(AGUIEventTypes.RunFinished, eventTypes);
|
||||
|
||||
int runStartIndex = eventTypes.IndexOf(AGUIEventTypes.RunStarted);
|
||||
int firstContentIndex = eventTypes.IndexOf(AGUIEventTypes.TextMessageContent);
|
||||
int runFinishIndex = eventTypes.LastIndexOf(AGUIEventTypes.RunFinished);
|
||||
|
||||
Assert.True(runStartIndex < firstContentIndex, "Run start should precede text content.");
|
||||
Assert.True(firstContentIndex < runFinishIndex, "Text content should precede run finish.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAGUIAgent_EmitsTextMessageContent_WithCorrectDeltaAsync()
|
||||
{
|
||||
// Arrange
|
||||
DefaultHttpContext httpContext = new();
|
||||
RunAgentInput input = new()
|
||||
{
|
||||
ThreadId = "thread1",
|
||||
RunId = "run1",
|
||||
Messages = [new AGUIUserMessage { Id = "m1", Content = "Test" }]
|
||||
};
|
||||
string json = JsonSerializer.Serialize(input, AGUIJsonSerializerContext.Default.RunAgentInput);
|
||||
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
|
||||
MemoryStream responseStream = new();
|
||||
httpContext.Response.Body = responseStream;
|
||||
|
||||
RequestDelegate handler = this.CreateRequestDelegate((messages, tools, context, props) => new TestAgent());
|
||||
|
||||
// Act
|
||||
await handler(httpContext);
|
||||
|
||||
// Assert
|
||||
responseStream.Position = 0;
|
||||
string responseContent = Encoding.UTF8.GetString(responseStream.ToArray());
|
||||
|
||||
List<JsonElement> events = ParseSseEvents(responseContent);
|
||||
JsonElement textContentEvent = Assert.Single(events, static e => e.GetProperty("type").GetString() == AGUIEventTypes.TextMessageContent);
|
||||
|
||||
Assert.Equal("Test response", textContentEvent.GetProperty("delta").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAGUIAgent_WithCustomAgent_ProducesExpectedStreamStructureAsync()
|
||||
{
|
||||
// Arrange
|
||||
static AIAgent CustomAgentFactory(IEnumerable<ChatMessage> messages, IEnumerable<AITool> tools, IEnumerable<KeyValuePair<string, string>> context, JsonElement props)
|
||||
{
|
||||
return new MultiResponseAgent();
|
||||
}
|
||||
|
||||
DefaultHttpContext httpContext = new();
|
||||
RunAgentInput input = new()
|
||||
{
|
||||
ThreadId = "custom_thread",
|
||||
RunId = "custom_run",
|
||||
Messages = [new AGUIUserMessage { Id = "m1", Content = "Multi" }]
|
||||
};
|
||||
string json = JsonSerializer.Serialize(input, AGUIJsonSerializerContext.Default.RunAgentInput);
|
||||
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
|
||||
MemoryStream responseStream = new();
|
||||
httpContext.Response.Body = responseStream;
|
||||
|
||||
RequestDelegate handler = this.CreateRequestDelegate(CustomAgentFactory);
|
||||
|
||||
// Act
|
||||
await handler(httpContext);
|
||||
|
||||
// Assert
|
||||
responseStream.Position = 0;
|
||||
string responseContent = Encoding.UTF8.GetString(responseStream.ToArray());
|
||||
|
||||
List<JsonElement> events = ParseSseEvents(responseContent);
|
||||
List<JsonElement> contentEvents = [];
|
||||
foreach (JsonElement evt in events)
|
||||
{
|
||||
if (evt.GetProperty("type").GetString() == AGUIEventTypes.TextMessageContent)
|
||||
{
|
||||
contentEvents.Add(evt);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(contentEvents.Count >= 3, $"Expected at least 3 text_message.content events, got {contentEvents.Count}");
|
||||
|
||||
List<string?> deltas = new(contentEvents.Count);
|
||||
foreach (JsonElement contentEvent in contentEvents)
|
||||
{
|
||||
deltas.Add(contentEvent.GetProperty("delta").GetString());
|
||||
}
|
||||
|
||||
Assert.Contains("First", deltas);
|
||||
Assert.Contains(" part", deltas);
|
||||
Assert.Contains(" of response", deltas);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAGUIAgent_ProducesCorrectThreadAndRunIds_InAllEventsAsync()
|
||||
{
|
||||
// Arrange
|
||||
DefaultHttpContext httpContext = new();
|
||||
RunAgentInput input = new()
|
||||
{
|
||||
ThreadId = "test_thread_123",
|
||||
RunId = "test_run_456",
|
||||
Messages = [new AGUIUserMessage { Id = "m1", Content = "Test" }]
|
||||
};
|
||||
string json = JsonSerializer.Serialize(input, AGUIJsonSerializerContext.Default.RunAgentInput);
|
||||
httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(json));
|
||||
MemoryStream responseStream = new();
|
||||
httpContext.Response.Body = responseStream;
|
||||
|
||||
RequestDelegate handler = this.CreateRequestDelegate((messages, tools, context, props) => new TestAgent());
|
||||
|
||||
// Act
|
||||
await handler(httpContext);
|
||||
|
||||
// Assert
|
||||
responseStream.Position = 0;
|
||||
string responseContent = Encoding.UTF8.GetString(responseStream.ToArray());
|
||||
|
||||
List<JsonElement> events = ParseSseEvents(responseContent);
|
||||
JsonElement runStarted = Assert.Single(events, static e => e.GetProperty("type").GetString() == AGUIEventTypes.RunStarted);
|
||||
|
||||
Assert.Equal("test_thread_123", runStarted.GetProperty("threadId").GetString());
|
||||
Assert.Equal("test_run_456", runStarted.GetProperty("runId").GetString());
|
||||
}
|
||||
|
||||
private static List<JsonElement> ParseSseEvents(string responseContent)
|
||||
{
|
||||
List<JsonElement> events = [];
|
||||
using StringReader reader = new(responseContent);
|
||||
StringBuilder dataBuilder = new();
|
||||
string? line;
|
||||
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
if (line.StartsWith("data:", StringComparison.Ordinal))
|
||||
{
|
||||
string payload = line.Length > 5 && line[5] == ' '
|
||||
? line.Substring(6)
|
||||
: line.Substring(5);
|
||||
dataBuilder.Append(payload);
|
||||
}
|
||||
else if (line.Length == 0 && dataBuilder.Length > 0)
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(dataBuilder.ToString());
|
||||
events.Add(document.RootElement.Clone());
|
||||
dataBuilder.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (dataBuilder.Length > 0)
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(dataBuilder.ToString());
|
||||
events.Add(document.RootElement.Clone());
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
private sealed class MultiResponseAgent : AIAgent
|
||||
{
|
||||
protected override string? IdCore => "multi-response-agent";
|
||||
|
||||
public override string? Description => "Agent that produces multiple text chunks";
|
||||
|
||||
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default) =>
|
||||
new(new TestInMemoryAgentThread());
|
||||
|
||||
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
|
||||
new(new TestInMemoryAgentThread(serializedThread, jsonSerializerOptions));
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
yield return new AgentResponseUpdate(new ChatResponseUpdate(ChatRole.Assistant, "First"));
|
||||
yield return new AgentResponseUpdate(new ChatResponseUpdate(ChatRole.Assistant, " part"));
|
||||
yield return new AgentResponseUpdate(new ChatResponseUpdate(ChatRole.Assistant, " of response"));
|
||||
}
|
||||
}
|
||||
|
||||
private RequestDelegate CreateRequestDelegate(
|
||||
Func<IEnumerable<ChatMessage>, IEnumerable<AITool>, IEnumerable<KeyValuePair<string, string>>, JsonElement, AIAgent> factory)
|
||||
{
|
||||
return async context =>
|
||||
{
|
||||
CancellationToken cancellationToken = context.RequestAborted;
|
||||
|
||||
RunAgentInput? input;
|
||||
try
|
||||
{
|
||||
input = await JsonSerializer.DeserializeAsync(
|
||||
context.Request.Body,
|
||||
AGUIJsonSerializerContext.Default.RunAgentInput,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
return;
|
||||
}
|
||||
|
||||
if (input is null)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<ChatMessage> messages = input.Messages.AsChatMessages(AGUIJsonSerializerContext.Default.Options);
|
||||
IEnumerable<KeyValuePair<string, string>> contextValues = input.Context.Select(c => new KeyValuePair<string, string>(c.Description, c.Value));
|
||||
JsonElement forwardedProps = input.ForwardedProperties;
|
||||
AIAgent agent = factory(messages, [], contextValues, forwardedProps);
|
||||
|
||||
IAsyncEnumerable<BaseEvent> events = agent.RunStreamingAsync(
|
||||
messages,
|
||||
cancellationToken: cancellationToken)
|
||||
.AsChatResponseUpdatesAsync()
|
||||
.AsAGUIEventStreamAsync(
|
||||
input.ThreadId,
|
||||
input.RunId,
|
||||
AGUIJsonSerializerContext.Default.Options,
|
||||
cancellationToken);
|
||||
|
||||
ILogger<AGUIServerSentEventsResult> logger = NullLogger<AGUIServerSentEventsResult>.Instance;
|
||||
await new AGUIServerSentEventsResult(events, logger).ExecuteAsync(context).ConfigureAwait(false);
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class TestInMemoryAgentThread : InMemoryAgentThread
|
||||
{
|
||||
public TestInMemoryAgentThread()
|
||||
: base()
|
||||
{
|
||||
}
|
||||
|
||||
public TestInMemoryAgentThread(JsonElement serializedThreadState, JsonSerializerOptions? jsonSerializerOptions = null)
|
||||
: base(serializedThreadState, jsonSerializerOptions, null)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestAgent : AIAgent
|
||||
{
|
||||
protected override string? IdCore => "test-agent";
|
||||
|
||||
public override string? Description => "Test agent";
|
||||
|
||||
public override ValueTask<AgentThread> GetNewThreadAsync(CancellationToken cancellationToken = default) =>
|
||||
new(new TestInMemoryAgentThread());
|
||||
|
||||
public override ValueTask<AgentThread> DeserializeThreadAsync(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) =>
|
||||
new(new TestInMemoryAgentThread(serializedThread, jsonSerializerOptions));
|
||||
|
||||
protected override Task<AgentResponse> RunCoreAsync(IEnumerable<ChatMessage> messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
|
||||
IEnumerable<ChatMessage> messages,
|
||||
AgentThread? thread = null,
|
||||
AgentRunOptions? options = null,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
yield return new AgentResponseUpdate(new ChatResponseUpdate(ChatRole.Assistant, "Test response"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="AGUIServerSentEventsResult"/> class.
|
||||
/// </summary>
|
||||
public sealed class AGUIServerSentEventsResultTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_SetsCorrectResponseHeaders_ContentTypeAndCacheControlAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events = [];
|
||||
ILogger<AGUIServerSentEventsResult> logger = NullLogger<AGUIServerSentEventsResult>.Instance;
|
||||
AGUIServerSentEventsResult result = new(events.ToAsyncEnumerableAsync(), logger);
|
||||
DefaultHttpContext httpContext = new();
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
|
||||
// Act
|
||||
await result.ExecuteAsync(httpContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("text/event-stream", httpContext.Response.ContentType);
|
||||
Assert.Equal("no-cache,no-store", httpContext.Response.Headers.CacheControl.ToString());
|
||||
Assert.Equal("no-cache", httpContext.Response.Headers.Pragma.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_SerializesEventsInSSEFormat_WithDataPrefixAndNewlinesAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
];
|
||||
ILogger<AGUIServerSentEventsResult> logger = NullLogger<AGUIServerSentEventsResult>.Instance;
|
||||
AGUIServerSentEventsResult result = new(events.ToAsyncEnumerableAsync(), logger);
|
||||
DefaultHttpContext httpContext = new();
|
||||
MemoryStream responseStream = new();
|
||||
httpContext.Response.Body = responseStream;
|
||||
|
||||
// Act
|
||||
await result.ExecuteAsync(httpContext);
|
||||
|
||||
// Assert
|
||||
string responseContent = Encoding.UTF8.GetString(responseStream.ToArray());
|
||||
Assert.Contains("data: ", responseContent);
|
||||
Assert.Contains("\n\n", responseContent);
|
||||
string[] eventStrings = responseContent.Split("\n\n", StringSplitOptions.RemoveEmptyEntries);
|
||||
Assert.Equal(2, eventStrings.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_FlushesResponse_AfterEachEventAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new TextMessageContentEvent { MessageId = "msg1", Delta = "Hello" },
|
||||
new RunFinishedEvent { ThreadId = "thread1", RunId = "run1" }
|
||||
];
|
||||
ILogger<AGUIServerSentEventsResult> logger = NullLogger<AGUIServerSentEventsResult>.Instance;
|
||||
AGUIServerSentEventsResult result = new(events.ToAsyncEnumerableAsync(), logger);
|
||||
DefaultHttpContext httpContext = new();
|
||||
MemoryStream responseStream = new();
|
||||
httpContext.Response.Body = responseStream;
|
||||
|
||||
// Act
|
||||
await result.ExecuteAsync(httpContext);
|
||||
|
||||
// Assert
|
||||
string responseContent = Encoding.UTF8.GetString(responseStream.ToArray());
|
||||
string[] eventStrings = responseContent.Split("\n\n", StringSplitOptions.RemoveEmptyEntries);
|
||||
Assert.Equal(3, eventStrings.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WithEmptyEventStream_CompletesSuccessfullyAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events = [];
|
||||
ILogger<AGUIServerSentEventsResult> logger = NullLogger<AGUIServerSentEventsResult>.Instance;
|
||||
AGUIServerSentEventsResult result = new(events.ToAsyncEnumerableAsync(), logger);
|
||||
DefaultHttpContext httpContext = new();
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
|
||||
// Act
|
||||
await result.ExecuteAsync(httpContext);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_RespectsCancellationToken_WhenCancelledAsync()
|
||||
{
|
||||
// Arrange
|
||||
using CancellationTokenSource cts = new();
|
||||
List<BaseEvent> events =
|
||||
[
|
||||
new RunStartedEvent { ThreadId = "thread1", RunId = "run1" },
|
||||
new TextMessageContentEvent { MessageId = "msg1", Delta = "Hello" }
|
||||
];
|
||||
|
||||
async IAsyncEnumerable<BaseEvent> GetEventsWithCancellationAsync()
|
||||
{
|
||||
foreach (BaseEvent evt in events)
|
||||
{
|
||||
yield return evt;
|
||||
await Task.Delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
ILogger<AGUIServerSentEventsResult> logger = NullLogger<AGUIServerSentEventsResult>.Instance;
|
||||
AGUIServerSentEventsResult result = new(GetEventsWithCancellationAsync(), logger);
|
||||
DefaultHttpContext httpContext = new();
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
httpContext.RequestAborted = cts.Token;
|
||||
|
||||
// Act
|
||||
cts.Cancel();
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => result.ExecuteAsync(httpContext));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WithNullHttpContext_ThrowsArgumentNullExceptionAsync()
|
||||
{
|
||||
// Arrange
|
||||
List<BaseEvent> events = [];
|
||||
ILogger<AGUIServerSentEventsResult> logger = NullLogger<AGUIServerSentEventsResult>.Instance;
|
||||
AGUIServerSentEventsResult result = new(events.ToAsyncEnumerableAsync(), logger);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => result.ExecuteAsync(null!));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.Shared;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests;
|
||||
|
||||
public sealed class ChatResponseUpdateAGUIExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_YieldsRunStartedEvent_AtBeginningWithCorrectIdsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string ThreadId = "thread1";
|
||||
const string RunId = "run1";
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> events = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync(ThreadId, RunId, AGUIJsonSerializerContext.Default.Options, CancellationToken.None))
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(events);
|
||||
RunStartedEvent startEvent = Assert.IsType<RunStartedEvent>(events.First());
|
||||
Assert.Equal(ThreadId, startEvent.ThreadId);
|
||||
Assert.Equal(RunId, startEvent.RunId);
|
||||
Assert.Equal(AGUIEventTypes.RunStarted, startEvent.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_YieldsRunFinishedEvent_AtEndWithCorrectIdsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string ThreadId = "thread1";
|
||||
const string RunId = "run1";
|
||||
List<ChatResponseUpdate> updates = [];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> events = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync(ThreadId, RunId, AGUIJsonSerializerContext.Default.Options, CancellationToken.None))
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotEmpty(events);
|
||||
RunFinishedEvent finishEvent = Assert.IsType<RunFinishedEvent>(events.Last());
|
||||
Assert.Equal(ThreadId, finishEvent.ThreadId);
|
||||
Assert.Equal(RunId, finishEvent.RunId);
|
||||
Assert.Equal(AGUIEventTypes.RunFinished, finishEvent.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_ConvertsTextContentUpdates_ToTextMessageEventsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string ThreadId = "thread1";
|
||||
const string RunId = "run1";
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "Hello") { MessageId = "msg1" },
|
||||
new ChatResponseUpdate(ChatRole.Assistant, " World") { MessageId = "msg1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> events = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync(ThreadId, RunId, AGUIJsonSerializerContext.Default.Options, CancellationToken.None))
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Contains(events, e => e is TextMessageStartEvent);
|
||||
Assert.Contains(events, e => e is TextMessageContentEvent);
|
||||
Assert.Contains(events, e => e is TextMessageEndEvent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_GroupsConsecutiveUpdates_WithSameMessageIdAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string ThreadId = "thread1";
|
||||
const string RunId = "run1";
|
||||
const string MessageId = "msg1";
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "Hello") { MessageId = MessageId },
|
||||
new ChatResponseUpdate(ChatRole.Assistant, " ") { MessageId = MessageId },
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "World") { MessageId = MessageId }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> events = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync(ThreadId, RunId, AGUIJsonSerializerContext.Default.Options, CancellationToken.None))
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
List<TextMessageStartEvent> startEvents = events.OfType<TextMessageStartEvent>().ToList();
|
||||
List<TextMessageEndEvent> endEvents = events.OfType<TextMessageEndEvent>().ToList();
|
||||
Assert.Single(startEvents);
|
||||
Assert.Single(endEvents);
|
||||
Assert.Equal(MessageId, startEvents[0].MessageId);
|
||||
Assert.Equal(MessageId, endEvents[0].MessageId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithRoleChanges_EmitsProperTextMessageStartEventsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string ThreadId = "thread1";
|
||||
const string RunId = "run1";
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "Hello") { MessageId = "msg1" },
|
||||
new ChatResponseUpdate(ChatRole.User, "Hi") { MessageId = "msg2" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> events = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync(ThreadId, RunId, AGUIJsonSerializerContext.Default.Options, CancellationToken.None))
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
List<TextMessageStartEvent> startEvents = events.OfType<TextMessageStartEvent>().ToList();
|
||||
Assert.Equal(2, startEvents.Count);
|
||||
Assert.Equal("msg1", startEvents[0].MessageId);
|
||||
Assert.Equal("msg2", startEvents[1].MessageId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_EmitsTextMessageEndEvent_WhenMessageIdChangesAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string ThreadId = "thread1";
|
||||
const string RunId = "run1";
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "First") { MessageId = "msg1" },
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "Second") { MessageId = "msg2" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> events = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync(ThreadId, RunId, AGUIJsonSerializerContext.Default.Options, CancellationToken.None))
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
List<TextMessageEndEvent> endEvents = events.OfType<TextMessageEndEvent>().ToList();
|
||||
Assert.NotEmpty(endEvents);
|
||||
Assert.Contains(endEvents, e => e.MessageId == "msg1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithFunctionCallContent_EmitsToolCallEventsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string ThreadId = "thread1";
|
||||
const string RunId = "run1";
|
||||
Dictionary<string, object?> arguments = new() { ["location"] = "Seattle", ["units"] = "fahrenheit" };
|
||||
FunctionCallContent functionCall = new("call_123", "GetWeather", arguments);
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, [functionCall]) { MessageId = "msg1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> events = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync(ThreadId, RunId, AGUIJsonSerializerContext.Default.Options, CancellationToken.None))
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
ToolCallStartEvent? startEvent = events.OfType<ToolCallStartEvent>().FirstOrDefault();
|
||||
Assert.NotNull(startEvent);
|
||||
Assert.Equal("call_123", startEvent.ToolCallId);
|
||||
Assert.Equal("GetWeather", startEvent.ToolCallName);
|
||||
Assert.Equal("msg1", startEvent.ParentMessageId);
|
||||
|
||||
ToolCallArgsEvent? argsEvent = events.OfType<ToolCallArgsEvent>().FirstOrDefault();
|
||||
Assert.NotNull(argsEvent);
|
||||
Assert.Equal("call_123", argsEvent.ToolCallId);
|
||||
Assert.Contains("location", argsEvent.Delta);
|
||||
Assert.Contains("Seattle", argsEvent.Delta);
|
||||
|
||||
ToolCallEndEvent? endEvent = events.OfType<ToolCallEndEvent>().FirstOrDefault();
|
||||
Assert.NotNull(endEvent);
|
||||
Assert.Equal("call_123", endEvent.ToolCallId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithMultipleFunctionCalls_EmitsAllToolCallEventsAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string ThreadId = "thread1";
|
||||
const string RunId = "run1";
|
||||
FunctionCallContent call1 = new("call_1", "Tool1", new Dictionary<string, object?>());
|
||||
FunctionCallContent call2 = new("call_2", "Tool2", new Dictionary<string, object?>());
|
||||
ChatResponseUpdate response = new(ChatRole.Assistant, [call1, call2]) { MessageId = "msg1" };
|
||||
List<ChatResponseUpdate> updates = [response];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> events = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync(ThreadId, RunId, AGUIJsonSerializerContext.Default.Options, CancellationToken.None))
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
List<ToolCallStartEvent> startEvents = events.OfType<ToolCallStartEvent>().ToList();
|
||||
Assert.Equal(2, startEvents.Count);
|
||||
Assert.Contains(startEvents, e => e.ToolCallId == "call_1" && e.ToolCallName == "Tool1");
|
||||
Assert.Contains(startEvents, e => e.ToolCallId == "call_2" && e.ToolCallName == "Tool2");
|
||||
|
||||
List<ToolCallEndEvent> endEvents = events.OfType<ToolCallEndEvent>().ToList();
|
||||
Assert.Equal(2, endEvents.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithFunctionCallWithNullArguments_EmitsEventsCorrectlyAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string ThreadId = "thread1";
|
||||
const string RunId = "run1";
|
||||
FunctionCallContent functionCall = new("call_456", "NoArgsTool", null);
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, [functionCall]) { MessageId = "msg1" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> events = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync(ThreadId, RunId, AGUIJsonSerializerContext.Default.Options, CancellationToken.None))
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Contains(events, e => e is ToolCallStartEvent);
|
||||
Assert.Contains(events, e => e is ToolCallArgsEvent);
|
||||
Assert.Contains(events, e => e is ToolCallEndEvent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsAGUIEventStreamAsync_WithMixedContentTypes_EmitsAllEventTypesAsync()
|
||||
{
|
||||
// Arrange
|
||||
const string ThreadId = "thread1";
|
||||
const string RunId = "run1";
|
||||
List<ChatResponseUpdate> updates =
|
||||
[
|
||||
new ChatResponseUpdate(ChatRole.Assistant, "Text message") { MessageId = "msg1" },
|
||||
new ChatResponseUpdate(ChatRole.Assistant, [new FunctionCallContent("call_1", "Tool1", null)]) { MessageId = "msg2" }
|
||||
];
|
||||
|
||||
// Act
|
||||
List<BaseEvent> events = [];
|
||||
await foreach (BaseEvent evt in updates.ToAsyncEnumerableAsync().AsAGUIEventStreamAsync(ThreadId, RunId, AGUIJsonSerializerContext.Default.Options, CancellationToken.None))
|
||||
{
|
||||
events.Add(evt);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Contains(events, e => e is RunStartedEvent);
|
||||
Assert.Contains(events, e => e is TextMessageStartEvent);
|
||||
Assert.Contains(events, e => e is TextMessageContentEvent);
|
||||
Assert.Contains(events, e => e is TextMessageEndEvent);
|
||||
Assert.Contains(events, e => e is ToolCallStartEvent);
|
||||
Assert.Contains(events, e => e is ToolCallArgsEvent);
|
||||
Assert.Contains(events, e => e is ToolCallEndEvent);
|
||||
Assert.Contains(events, e => e is RunFinishedEvent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(TargetFrameworksCore)</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net10.0'))">
|
||||
<PackageReference Include="System.Linq.AsyncEnumerable" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore\Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests;
|
||||
|
||||
internal static class TestHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension method to convert a synchronous enumerable to an async enumerable for testing purposes.
|
||||
/// </summary>
|
||||
public static async IAsyncEnumerable<T> ToAsyncEnumerableAsync<T>(this IEnumerable<T> source)
|
||||
{
|
||||
foreach (T item in source)
|
||||
{
|
||||
yield return item;
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user