diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index a3b283b557eb..d2669d83f5e7 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -49,6 +49,7 @@ + @@ -101,7 +102,7 @@ - + diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj index 1519070d304a..90f2f6225a91 100644 --- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj +++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj @@ -18,16 +18,16 @@ - - + + - - - - + + + + - - + + diff --git a/dotnet/src/Agents/A2A/A2AAgentExtensions.cs b/dotnet/src/Agents/A2A/A2AAgentExtensions.cs new file mode 100644 index 000000000000..ad07a120e18a --- /dev/null +++ b/dotnet/src/Agents/A2A/A2AAgentExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using MAAI = Microsoft.Agents.AI; + +namespace Microsoft.SemanticKernel.Agents.A2A; + +/// +/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . +/// +public static class A2AAgentExtensions +{ + /// + /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . + /// + /// The Semantic Kernel to expose as a Microsoft Agent Framework . + /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework + [Experimental("SKEXP0110")] + public static MAAI.AIAgent AsAIAgent(this A2AAgent a2aAgent) + => a2aAgent.AsAIAgent( + () => new A2AAgentThread(a2aAgent.Client), + (json, options) => + { + var agentId = JsonSerializer.Deserialize(json); + return agentId is null ? new A2AAgentThread(a2aAgent.Client) : new A2AAgentThread(a2aAgent.Client, agentId); + }, + (thread, options) => JsonSerializer.SerializeToElement((thread as A2AAgentThread)?.Id)); +} diff --git a/dotnet/src/Agents/Abstractions/AIAgent/AIAgentAdapter.cs b/dotnet/src/Agents/Abstractions/AIAgent/AIAgentAdapter.cs new file mode 100644 index 000000000000..46129e2d3955 --- /dev/null +++ b/dotnet/src/Agents/Abstractions/AIAgent/AIAgentAdapter.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; +using MAAI = Microsoft.Agents.AI; + +namespace Microsoft.SemanticKernel.Agents; + +/// +/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . +/// +[Experimental("SKEXP0110")] +internal sealed class AIAgentAdapter : MAAI.AIAgent +{ + private readonly Func _threadFactory; + private readonly Func _threadDeserializationFactory; + private readonly Func _threadSerializer; + + /// + /// Initializes a new instance of the class. + /// + /// The Semantic Kernel to expose as a Microsoft Agent Framework . + /// A factory method to create the required type to use with the agent. + /// A factory method to deserialize the required type. + /// A method to serialize the type. + public AIAgentAdapter( + Agent semanticKernelAgent, + Func threadFactory, + Func threadDeserializationFactory, + Func threadSerializer) + { + Throw.IfNull(semanticKernelAgent); + Throw.IfNull(threadFactory); + Throw.IfNull(threadDeserializationFactory); + Throw.IfNull(threadSerializer); + + this.InnerAgent = semanticKernelAgent; + this._threadFactory = threadFactory; + this._threadDeserializationFactory = threadDeserializationFactory; + this._threadSerializer = threadSerializer; + } + + /// + /// Gets the underlying Semantic Kernel Agent Framework . + /// + public Agent InnerAgent { get; } + + /// + public override MAAI.AgentThread DeserializeThread(JsonElement serializedThread, JsonSerializerOptions? jsonSerializerOptions = null) + => new AIAgentThreadAdapter(this._threadDeserializationFactory(serializedThread, jsonSerializerOptions), this._threadSerializer); + + /// + public override MAAI.AgentThread GetNewThread() => new AIAgentThreadAdapter(this._threadFactory(), this._threadSerializer); + + /// + public override async Task RunAsync(IEnumerable messages, MAAI.AgentThread? thread = null, MAAI.AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + thread ??= this.GetNewThread(); + if (thread is not AIAgentThreadAdapter typedThread) + { + throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used."); + } + + List responseMessages = []; + var invokeOptions = new AgentInvokeOptions() + { + OnIntermediateMessage = (msg) => + { + responseMessages.Add(msg.ToChatMessage()); + return Task.CompletedTask; + } + }; + + AgentResponseItem? lastResponseItem = null; + ChatMessage? lastResponseMessage = null; + await foreach (var responseItem in this.InnerAgent.InvokeAsync(messages.Select(x => x.ToChatMessageContent()).ToList(), typedThread.InnerThread, invokeOptions, cancellationToken).ConfigureAwait(false)) + { + lastResponseItem = responseItem; + lastResponseMessage = responseItem.Message.ToChatMessage(); + responseMessages.Add(lastResponseMessage); + } + + return new MAAI.AgentRunResponse(responseMessages) + { + AgentId = this.InnerAgent.Id, + RawRepresentation = lastResponseItem, + AdditionalProperties = lastResponseMessage?.AdditionalProperties, + CreatedAt = lastResponseMessage?.CreatedAt, + }; + } + + /// + public override async IAsyncEnumerable RunStreamingAsync( + IEnumerable messages, + MAAI.AgentThread? thread = null, + MAAI.AgentRunOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + thread ??= this.GetNewThread(); + if (thread is not AIAgentThreadAdapter typedThread) + { + throw new InvalidOperationException("The provided thread is not compatible with the agent. Only threads created by the agent can be used."); + } + + await foreach (var responseItem in this.InnerAgent.InvokeAsync(messages.Select(x => x.ToChatMessageContent()).ToList(), typedThread.InnerThread, cancellationToken: cancellationToken).ConfigureAwait(false)) + { + var chatMessage = responseItem.Message.ToChatMessage(); + + yield return new MAAI.AgentRunResponseUpdate + { + AgentId = this.InnerAgent.Id, + RawRepresentation = responseItem, + AdditionalProperties = chatMessage.AdditionalProperties, + MessageId = chatMessage.MessageId, + Role = chatMessage.Role, + CreatedAt = chatMessage.CreatedAt, + Contents = chatMessage.Contents + }; + } + } +} diff --git a/dotnet/src/Agents/Abstractions/AIAgent/AIAgentThreadAdapter.cs b/dotnet/src/Agents/Abstractions/AIAgent/AIAgentThreadAdapter.cs new file mode 100644 index 000000000000..31d7174a6b62 --- /dev/null +++ b/dotnet/src/Agents/Abstractions/AIAgent/AIAgentThreadAdapter.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using MAAI = Microsoft.Agents.AI; + +namespace Microsoft.SemanticKernel.Agents; + +[Experimental("SKEXP0110")] +internal sealed class AIAgentThreadAdapter : MAAI.AgentThread +{ + private readonly Func _threadSerializer; + + internal AIAgentThreadAdapter(AgentThread thread, Func threadSerializer) + { + Throw.IfNull(thread); + Throw.IfNull(threadSerializer); + + this.InnerThread = thread; + this._threadSerializer = threadSerializer; + } + + /// + /// Gets the underlying Semantic Kernel Agent Framework . + /// + public AgentThread InnerThread { get; } + + /// + public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null) + => this._threadSerializer(this.InnerThread, jsonSerializerOptions); + + public override object? GetService(Type serviceType, object? serviceKey = null) + => base.GetService(serviceType, serviceKey) + ?? (serviceType == typeof(AgentThread) && serviceKey is null + ? this.InnerThread + : null); +} diff --git a/dotnet/src/Agents/Abstractions/AgentExtensions.cs b/dotnet/src/Agents/Abstractions/AgentExtensions.cs new file mode 100644 index 000000000000..cfa9f6579fee --- /dev/null +++ b/dotnet/src/Agents/Abstractions/AgentExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using MAAI = Microsoft.Agents.AI; + +namespace Microsoft.SemanticKernel.Agents; + +/// +/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . +/// +public static class AgentExtensions +{ + /// + /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . + /// + /// The Semantic Kernel to expose as a Microsoft Agent Framework . + /// A factory method to create the required type to use with the agent. + /// A factory method to deserialize the required type. + /// A method to serialize the type. + /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework + [Experimental("SKEXP0110")] + public static MAAI.AIAgent AsAIAgent( + this Agent semanticKernelAgent, + Func threadFactory, + Func threadDeserializationFactory, + Func threadSerializer) + => new AIAgentAdapter( + semanticKernelAgent, + threadFactory, + threadDeserializationFactory, + threadSerializer); +} diff --git a/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj b/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj index a0fedaa605ec..1d24d51cbf02 100644 --- a/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj +++ b/dotnet/src/Agents/Abstractions/Agents.Abstractions.csproj @@ -5,11 +5,12 @@ Microsoft.SemanticKernel.Agents.Abstractions Microsoft.SemanticKernel.Agents net8.0;netstandard2.0 - $(NoWarn) + $(NoWarn);NU5104 false + @@ -27,6 +28,7 @@ + diff --git a/dotnet/src/Agents/AzureAI/AzureAIAgentExtensions.cs b/dotnet/src/Agents/AzureAI/AzureAIAgentExtensions.cs new file mode 100644 index 000000000000..255128159d26 --- /dev/null +++ b/dotnet/src/Agents/AzureAI/AzureAIAgentExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using MAAI = Microsoft.Agents.AI; + +namespace Microsoft.SemanticKernel.Agents.AzureAI; + +/// +/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . +/// +public static class AzureAIAgentExtensions +{ + /// + /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . + /// + /// The Semantic Kernel to expose as a Microsoft Agent Framework . + /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework + [Experimental("SKEXP0110")] + public static MAAI.AIAgent AsAIAgent(this AzureAIAgent azureAIAgent) + => azureAIAgent.AsAIAgent( + () => new AzureAIAgentThread(azureAIAgent.Client), + (json, options) => + { + var agentId = JsonSerializer.Deserialize(json); + return agentId is null ? new AzureAIAgentThread(azureAIAgent.Client) : new AzureAIAgentThread(azureAIAgent.Client, agentId); + }, + (thread, options) => JsonSerializer.SerializeToElement((thread as AzureAIAgentThread)?.Id)); +} diff --git a/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentExtensions.cs b/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentExtensions.cs index eee1efe21bcb..9e3cae27edfa 100644 --- a/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentExtensions.cs +++ b/dotnet/src/Agents/Bedrock/Extensions/BedrockAgentExtensions.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Amazon.BedrockAgent; using Amazon.BedrockAgent.Model; +using Microsoft.Agents.AI; namespace Microsoft.SemanticKernel.Agents.Bedrock; @@ -13,6 +16,22 @@ namespace Microsoft.SemanticKernel.Agents.Bedrock; /// public static class BedrockAgentExtensions { + /// + /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . + /// + /// The Semantic Kernel to expose as a Microsoft Agent Framework . + /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework + [Experimental("SKEXP0110")] + public static AIAgent AsAIAgent(this BedrockAgent bedrockAgent) + => bedrockAgent.AsAIAgent( + () => new BedrockAgentThread(bedrockAgent.RuntimeClient), + (json, options) => + { + var agentId = JsonSerializer.Deserialize(json); + return agentId is null ? new BedrockAgentThread(bedrockAgent.RuntimeClient) : new BedrockAgentThread(bedrockAgent.RuntimeClient, agentId); + }, + (thread, options) => JsonSerializer.SerializeToElement((thread as BedrockAgentThread)?.Id)); + /// /// Creates an agent. /// diff --git a/dotnet/src/Agents/Copilot/CopilotStudioAgentExtensions.cs b/dotnet/src/Agents/Copilot/CopilotStudioAgentExtensions.cs new file mode 100644 index 000000000000..fedb67b5fe40 --- /dev/null +++ b/dotnet/src/Agents/Copilot/CopilotStudioAgentExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using MAAI = Microsoft.Agents.AI; + +namespace Microsoft.SemanticKernel.Agents.Copilot; + +/// +/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . +/// +public static class CopilotStudioAgentExtensions +{ + /// + /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . + /// + /// The Semantic Kernel to expose as a Microsoft Agent Framework . + /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework + [Experimental("SKEXP0110")] + public static MAAI.AIAgent AsAIAgent(this CopilotStudioAgent copilotStudioAgent) + => copilotStudioAgent.AsAIAgent( + () => new CopilotStudioAgentThread(copilotStudioAgent.Client), + (json, options) => + { + var agentId = JsonSerializer.Deserialize(json); + return agentId is null ? new CopilotStudioAgentThread(copilotStudioAgent.Client) : new CopilotStudioAgentThread(copilotStudioAgent.Client, agentId); + }, + (thread, options) => JsonSerializer.SerializeToElement((thread as CopilotStudioAgentThread)?.Id)); +} diff --git a/dotnet/src/Agents/Core/ChatCompletionAgentExtensions.cs b/dotnet/src/Agents/Core/ChatCompletionAgentExtensions.cs new file mode 100644 index 000000000000..d95f4a89b445 --- /dev/null +++ b/dotnet/src/Agents/Core/ChatCompletionAgentExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Microsoft.SemanticKernel.ChatCompletion; +using MAAI = Microsoft.Agents.AI; + +namespace Microsoft.SemanticKernel.Agents; + +/// +/// Exposes a Semantic Kernel as a Microsoft Agent Framework . +/// +public static class ChatCompletionAgentExtensions +{ + /// + /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . + /// + /// The Semantic Kernel to expose as a Microsoft Agent Framework . + /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework + [Experimental("SKEXP0110")] + public static MAAI.AIAgent AsAIAgent(this ChatCompletionAgent chatCompletionAgent) + => chatCompletionAgent.AsAIAgent( + () => new ChatHistoryAgentThread(), + (json, options) => + { + var chatHistory = JsonSerializer.Deserialize(json); + return chatHistory is null ? new ChatHistoryAgentThread() : new ChatHistoryAgentThread(chatHistory); + }, + (thread, options) => JsonSerializer.SerializeToElement((thread as ChatHistoryAgentThread)?.ChatHistory)); +} diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentExtensions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentExtensions.cs new file mode 100644 index 000000000000..6f8881ca2b50 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgentExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using MAAI = Microsoft.Agents.AI; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . +/// +public static class OpenAIAssistantAgentExtensions +{ + /// + /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . + /// + /// The Semantic Kernel to expose as a Microsoft Agent Framework . + /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework + [Experimental("SKEXP0110")] + public static MAAI.AIAgent AsAIAgent(this OpenAIAssistantAgent assistantAgent) + => assistantAgent.AsAIAgent( + () => new OpenAIAssistantAgentThread(assistantAgent.Client), + (json, options) => + { + var agentId = JsonSerializer.Deserialize(json); + return agentId is null ? new OpenAIAssistantAgentThread(assistantAgent.Client) : new OpenAIAssistantAgentThread(assistantAgent.Client, agentId); + }, + (thread, options) => JsonSerializer.SerializeToElement((thread as OpenAIAssistantAgentThread)?.Id)); +} diff --git a/dotnet/src/Agents/OpenAI/OpenAIResponseAgentExtensions.cs b/dotnet/src/Agents/OpenAI/OpenAIResponseAgentExtensions.cs new file mode 100644 index 000000000000..90184975bbed --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIResponseAgentExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using MAAI = Microsoft.Agents.AI; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . +/// +public static class OpenAIResponseAgentExtensions +{ + /// + /// Exposes a Semantic Kernel Agent Framework as a Microsoft Agent Framework . + /// + /// The Semantic Kernel to expose as a Microsoft Agent Framework . + /// The Semantic Kernel Agent Framework exposed as a Microsoft Agent Framework + [Experimental("SKEXP0110")] + public static MAAI.AIAgent AsAIAgent(this OpenAIResponseAgent responseAgent) + => responseAgent.AsAIAgent( + () => new OpenAIResponseAgentThread(responseAgent.Client), + (json, options) => + { + var agentId = JsonSerializer.Deserialize(json); + return agentId is null ? new OpenAIResponseAgentThread(responseAgent.Client) : new OpenAIResponseAgentThread(responseAgent.Client, agentId); + }, + (thread, options) => JsonSerializer.SerializeToElement((thread as OpenAIResponseAgentThread)?.Id)); +} diff --git a/dotnet/src/Agents/UnitTests/A2A/A2AAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/A2A/A2AAgentExtensionsTests.cs new file mode 100644 index 000000000000..3be6f5cfb8e6 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/A2A/A2AAgentExtensionsTests.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using System.Text.Json; +using A2A; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.A2A; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.A2A; + +public sealed class A2AAgentExtensionsTests +{ + [Fact] + public void AsAIAgent_WithValidA2AAgent_ReturnsAIAgentAdapter() + { + // Arrange + using var httpClient = new HttpClient(); + var a2aClient = new A2AClient(new Uri("http://testservice", UriKind.Absolute), httpClient); + var agentCard = new AgentCard(); + var a2aAgent = new A2AAgent(a2aClient, agentCard); + + // Act + var result = a2aAgent.AsAIAgent(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void AsAIAgent_WithNullA2AAgent_ThrowsArgumentNullException() + { + // Arrange + A2AAgent nullAgent = null!; + + // Act & Assert + Assert.Throws(() => nullAgent.AsAIAgent()); + } + + [Fact] + public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent() + { + // Arrange + using var httpClient = new HttpClient(); + var a2aClient = new A2AClient(new Uri("http://testservice", UriKind.Absolute), httpClient); + var agentCard = new AgentCard(); + var a2aAgent = new A2AAgent(a2aClient, agentCard); + + // Act + var result = a2aAgent.AsAIAgent(); + + // Assert + var adapter = Assert.IsType(result); + Assert.Same(a2aAgent, adapter.InnerAgent); + } + + [Fact] + public void AsAIAgent_CreatesWorkingThreadFactory() + { + // Arrange + using var httpClient = new HttpClient(); + var a2aClient = new A2AClient(new Uri("http://testservice", UriKind.Absolute), httpClient); + var agentCard = new AgentCard(); + var a2aAgent = new A2AAgent(a2aClient, agentCard); + + // Act + var result = a2aAgent.AsAIAgent(); + var thread = result.GetNewThread(); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithNullAgentId_CreatesNewThread() + { + // Arrange + using var httpClient = new HttpClient(); + var a2aClient = new A2AClient(new Uri("http://testservice", UriKind.Absolute), httpClient); + var agentCard = new AgentCard(); + var a2aAgent = new A2AAgent(a2aClient, agentCard); + var jsonElement = JsonSerializer.SerializeToElement((string?)null); + + // Act + var result = a2aAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithValidAgentId_CreatesThreadWithId() + { + // Arrange + using var httpClient = new HttpClient(); + var a2aClient = new A2AClient(new Uri("http://testservice", UriKind.Absolute), httpClient); + var agentCard = new AgentCard(); + var a2aAgent = new A2AAgent(a2aClient, agentCard); + var threadId = "test-agent-id"; + var jsonElement = JsonSerializer.SerializeToElement(threadId); + + // Act + var result = a2aAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + Assert.Equal(threadId, threadAdapter.InnerThread.Id); + } + + [Fact] + public void AsAIAgent_ThreadSerializer_SerializesThreadId() + { + // Arrange + using var httpClient = new HttpClient(); + var a2aClient = new A2AClient(new Uri("http://testservice", UriKind.Absolute), httpClient); + var agentCard = new AgentCard(); + var a2aAgent = new A2AAgent(a2aClient, agentCard); + var expectedThreadId = "test-thread-id"; + var a2aThread = new A2AAgentThread(a2aClient, expectedThreadId); + var jsonElement = JsonSerializer.SerializeToElement(expectedThreadId); + + var result = a2aAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Act + var serializedElement = thread.Serialize(); + + // Assert + Assert.Equal(JsonValueKind.String, serializedElement.ValueKind); + Assert.Equal(expectedThreadId, serializedElement.GetString()); + } +} diff --git a/dotnet/src/Agents/UnitTests/AIAgent/AIAgentAdapterTests.cs b/dotnet/src/Agents/UnitTests/AIAgent/AIAgentAdapterTests.cs new file mode 100644 index 000000000000..7ce8daad4e1e --- /dev/null +++ b/dotnet/src/Agents/UnitTests/AIAgent/AIAgentAdapterTests.cs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using Microsoft.SemanticKernel.Agents; +using Moq; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.AIAgent; + +public sealed class AIAgentAdapterTests +{ + [Fact] + public void Constructor_InitializesProperties() + { + // Arrange + var agentMock = new Mock(); + AgentThread ThreadFactory() => Mock.Of(); + AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of(); + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + + // Act + var adapter = new AIAgentAdapter(agentMock.Object, ThreadFactory, ThreadDeserializationFactory, ThreadSerializer); + + // Assert + Assert.Equal(agentMock.Object, adapter.InnerAgent); + } + + [Fact] + public void Constructor_WithNullSemanticKernelAgent_ThrowsArgumentNullException() + { + // Arrange + AgentThread ThreadFactory() => Mock.Of(); + AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of(); + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + + // Act & Assert + Assert.Throws(() => new AIAgentAdapter(null!, ThreadFactory, ThreadDeserializationFactory, ThreadSerializer)); + } + + [Fact] + public void Constructor_WithNullThreadFactory_ThrowsArgumentNullException() + { + // Arrange + var agentMock = new Mock(); + AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of(); + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + + // Act & Assert + Assert.Throws(() => new AIAgentAdapter(agentMock.Object, null!, ThreadDeserializationFactory, ThreadSerializer)); + } + + [Fact] + public void Constructor_WithNullThreadDeserializationFactory_ThrowsArgumentNullException() + { + // Arrange + var agentMock = new Mock(); + AgentThread ThreadFactory() => Mock.Of(); + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + + // Act & Assert + Assert.Throws(() => new AIAgentAdapter(agentMock.Object, ThreadFactory, null!, ThreadSerializer)); + } + + [Fact] + public void Constructor_WithNullThreadSerializer_ThrowsArgumentNullException() + { + // Arrange + var agentMock = new Mock(); + AgentThread ThreadFactory() => Mock.Of(); + AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of(); + + // Act & Assert + Assert.Throws(() => new AIAgentAdapter(agentMock.Object, ThreadFactory, ThreadDeserializationFactory, null!)); + } + + [Fact] + public void DeserializeThread_ReturnsAIAgentThreadAdapter() + { + // Arrange + var agentMock = new Mock(); + var expectedThread = Mock.Of(); + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => expectedThread; + var adapter = new AIAgentAdapter(agentMock.Object, () => expectedThread, ThreadDeserializationFactory, ThreadSerializer); + var json = JsonDocument.Parse("{}").RootElement; + + // Act + var result = adapter.DeserializeThread(json); + + // Assert + Assert.IsType(result); + } + + [Fact] + public void GetNewThread_ReturnsAIAgentThreadAdapter() + { + // Arrange + var agentMock = new Mock(); + var expectedThread = Mock.Of(); + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + var adapter = new AIAgentAdapter(agentMock.Object, () => expectedThread, (e, o) => expectedThread, ThreadSerializer); + + // Act + var result = adapter.GetNewThread(); + + // Assert + Assert.IsType(result); + Assert.Equal(expectedThread, ((AIAgentThreadAdapter)result).InnerThread); + } + + [Fact] + public void DeserializeThread_CallsDeserializationFactory() + { + // Arrange + var agentMock = new Mock(); + var expectedThread = Mock.Of(); + var factoryCallCount = 0; + + AgentThread DeserializationFactory(JsonElement e, JsonSerializerOptions? o) + { + factoryCallCount++; + return expectedThread; + } + + var adapter = new AIAgentAdapter(agentMock.Object, () => expectedThread, DeserializationFactory, (t, o) => default); + var json = JsonDocument.Parse("{}").RootElement; + + // Act + var result = adapter.DeserializeThread(json); + + // Assert + Assert.Equal(1, factoryCallCount); + Assert.IsType(result); + } + + [Fact] + public void GetNewThread_CallsThreadFactory() + { + // Arrange + var agentMock = new Mock(); + var expectedThread = Mock.Of(); + var factoryCallCount = 0; + + AgentThread ThreadFactory() + { + factoryCallCount++; + return expectedThread; + } + + var adapter = new AIAgentAdapter(agentMock.Object, ThreadFactory, (e, o) => expectedThread, (t, o) => default); + + // Act + var result = adapter.GetNewThread(); + + // Assert + Assert.Equal(1, factoryCallCount); + Assert.IsType(result); + } + + [Fact] + public void InnerAgent_Property_ReturnsCorrectValue() + { + // Arrange + var agentMock = new Mock(); + var adapter = new AIAgentAdapter(agentMock.Object, () => Mock.Of(), (e, o) => Mock.Of(), (t, o) => default); + + // Act + var result = adapter.InnerAgent; + + // Assert + Assert.Same(agentMock.Object, result); + } +} diff --git a/dotnet/src/Agents/UnitTests/AIAgent/AIAgentThreadAdapterTests.cs b/dotnet/src/Agents/UnitTests/AIAgent/AIAgentThreadAdapterTests.cs new file mode 100644 index 000000000000..5305a48290ee --- /dev/null +++ b/dotnet/src/Agents/UnitTests/AIAgent/AIAgentThreadAdapterTests.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using Microsoft.SemanticKernel.Agents; +using Moq; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.AIAgent; + +public sealed class AIAgentThreadAdapterTests +{ + [Fact] + public void Constructor_InitializesProperties() + { + // Arrange + var threadMock = new Mock(); + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + + // Act + var adapter = new AIAgentThreadAdapter(threadMock.Object, ThreadSerializer); + + // Assert + Assert.Equal(threadMock.Object, adapter.InnerThread); + } + + [Fact] + public void Serialize_CallsThreadSerializer() + { + // Arrange + var threadMock = new Mock(); + var serializerCallCount = 0; + var expectedJsonElement = JsonDocument.Parse("{\"test\": \"value\"}").RootElement; + + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) + { + serializerCallCount++; + Assert.Same(threadMock.Object, t); + return expectedJsonElement; + } + + var adapter = new AIAgentThreadAdapter(threadMock.Object, ThreadSerializer); + + // Act + var result = adapter.Serialize(); + + // Assert + Assert.Equal(1, serializerCallCount); + Assert.Equal(expectedJsonElement.ToString(), result.ToString()); + } + + [Fact] + public void Serialize_WithJsonSerializerOptions_PassesOptionsToSerializer() + { + // Arrange + var threadMock = new Mock(); + var expectedOptions = new JsonSerializerOptions(); + JsonSerializerOptions? capturedOptions = null; + + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) + { + capturedOptions = o; + return default; + } + + var adapter = new AIAgentThreadAdapter(threadMock.Object, ThreadSerializer); + + // Act + adapter.Serialize(expectedOptions); + + // Assert + Assert.Same(expectedOptions, capturedOptions); + } + + [Fact] + public void GetService_WithAgentThreadType_ReturnsInnerThread() + { + // Arrange + var threadMock = new Mock(); + var adapter = new AIAgentThreadAdapter(threadMock.Object, (t, o) => default); + + // Act + var result = adapter.GetService(typeof(AgentThread)); + + // Assert + Assert.Same(threadMock.Object, result); + } + + [Fact] + public void GetService_WithAgentThreadTypeAndServiceKey_ReturnsNull() + { + // Arrange + var threadMock = new Mock(); + var adapter = new AIAgentThreadAdapter(threadMock.Object, (t, o) => default); + var serviceKey = new object(); + + // Act + var result = adapter.GetService(typeof(AgentThread), serviceKey); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetService_WithNonAgentThreadType_ReturnsNull() + { + // Arrange + var threadMock = new Mock(); + var adapter = new AIAgentThreadAdapter(threadMock.Object, (t, o) => default); + + // Act + var result = adapter.GetService(typeof(string)); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetService_WithNullType_ThrowsArgumentNullException() + { + // Arrange + var threadMock = new Mock(); + var adapter = new AIAgentThreadAdapter(threadMock.Object, (t, o) => default); + + // Act & Assert + Assert.Throws(() => adapter.GetService(null!)); + } + + [Fact] + public void Serialize_WithNullOptions_CallsSerializerWithNull() + { + // Arrange + var threadMock = new Mock(); + JsonSerializerOptions? capturedOptions = new(); + + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) + { + capturedOptions = o; + return default; + } + + var adapter = new AIAgentThreadAdapter(threadMock.Object, ThreadSerializer); + + // Act + adapter.Serialize(null); + + // Assert + Assert.Null(capturedOptions); + } + + [Fact] + public void Constructor_WithNullThread_ThrowsArgumentNullException() + { + // Arrange & Act + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + Assert.Throws(() => new AIAgentThreadAdapter(null!, ThreadSerializer)); + } + + [Fact] + public void Constructor_WithNullSerializer_ThrowsArgumentNullException() + { + // Arrange & Act + var threadMock = new Mock(); + Assert.Throws(() => new AIAgentThreadAdapter(threadMock.Object, null!)); + } +} diff --git a/dotnet/src/Agents/UnitTests/AgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/AgentExtensionsTests.cs new file mode 100644 index 000000000000..1263de16ea27 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/AgentExtensionsTests.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using Microsoft.SemanticKernel.Agents; +using Moq; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests; + +public sealed class AgentExtensionsTests +{ + [Fact] + public void AsAIAgent_WithValidParameters_ReturnsAIAgentAdapter() + { + // Arrange + var agentMock = new Mock(); + AgentThread ThreadFactory() => Mock.Of(); + AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of(); + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + + // Act + var result = agentMock.Object.AsAIAgent(ThreadFactory, ThreadDeserializationFactory, ThreadSerializer); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void AsAIAgent_WithNullSemanticKernelAgent_ThrowsArgumentNullException() + { + // Arrange + Agent nullAgent = null!; + AgentThread ThreadFactory() => Mock.Of(); + AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of(); + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + + // Act & Assert + Assert.Throws(() => nullAgent.AsAIAgent(ThreadFactory, ThreadDeserializationFactory, ThreadSerializer)); + } + + [Fact] + public void AsAIAgent_WithNullThreadFactory_ThrowsArgumentNullException() + { + // Arrange + var agentMock = new Mock(); + AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of(); + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + + // Act & Assert + Assert.Throws(() => agentMock.Object.AsAIAgent(null!, ThreadDeserializationFactory, ThreadSerializer)); + } + + [Fact] + public void AsAIAgent_WithNullThreadDeserializationFactory_ThrowsArgumentNullException() + { + // Arrange + var agentMock = new Mock(); + AgentThread ThreadFactory() => Mock.Of(); + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + + // Act & Assert + Assert.Throws(() => agentMock.Object.AsAIAgent(ThreadFactory, null!, ThreadSerializer)); + } + + [Fact] + public void AsAIAgent_WithNullThreadSerializer_ThrowsArgumentNullException() + { + // Arrange + var agentMock = new Mock(); + AgentThread ThreadFactory() => Mock.Of(); + AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of(); + + // Act & Assert + Assert.Throws(() => agentMock.Object.AsAIAgent(ThreadFactory, ThreadDeserializationFactory, null!)); + } + + [Fact] + public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent() + { + // Arrange + var agentMock = new Mock(); + AgentThread ThreadFactory() => Mock.Of(); + AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => Mock.Of(); + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + + // Act + var result = agentMock.Object.AsAIAgent(ThreadFactory, ThreadDeserializationFactory, ThreadSerializer); + + // Assert + var adapter = Assert.IsType(result); + Assert.Same(agentMock.Object, adapter.InnerAgent); + } + + [Fact] + public void AsAIAgent_WithValidFactories_CreatesWorkingAdapter() + { + // Arrange + var agentMock = new Mock(); + var expectedThread = Mock.Of(); + var factoryCallCount = 0; + + AgentThread ThreadFactory() + { + factoryCallCount++; + return expectedThread; + } + + AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) => expectedThread; + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + + // Act + var result = agentMock.Object.AsAIAgent(ThreadFactory, ThreadDeserializationFactory, ThreadSerializer); + var thread = result.GetNewThread(); + + // Assert + Assert.NotNull(thread); + Assert.Equal(1, factoryCallCount); + Assert.IsType(thread); + Assert.Same(expectedThread, ((AIAgentThreadAdapter)thread).InnerThread); + } + + [Fact] + public void AsAIAgent_WithDeserializationFactory_CreatesWorkingAdapter() + { + // Arrange + var agentMock = new Mock(); + var expectedThread = Mock.Of(); + var deserializationCallCount = 0; + + AgentThread ThreadFactory() => Mock.Of(); + + AgentThread ThreadDeserializationFactory(JsonElement e, JsonSerializerOptions? o) + { + deserializationCallCount++; + return expectedThread; + } + + JsonElement ThreadSerializer(AgentThread t, JsonSerializerOptions? o) => default; + + // Act + var result = agentMock.Object.AsAIAgent(ThreadFactory, ThreadDeserializationFactory, ThreadSerializer); + var json = JsonDocument.Parse("{}").RootElement; + var thread = result.DeserializeThread(json); + + // Assert + Assert.NotNull(thread); + Assert.Equal(1, deserializationCallCount); + Assert.IsType(thread); + Assert.Same(expectedThread, ((AIAgentThreadAdapter)thread).InnerThread); + } +} diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj index f4a98b16a9cc..da3c9326460c 100644 --- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj +++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj @@ -14,16 +14,16 @@ - - + + - - - - + + + + - - + + diff --git a/dotnet/src/Agents/UnitTests/AzureAI/AzureAIAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/AzureAI/AzureAIAgentExtensionsTests.cs new file mode 100644 index 000000000000..13f36012f9fa --- /dev/null +++ b/dotnet/src/Agents/UnitTests/AzureAI/AzureAIAgentExtensionsTests.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel.Primitives; +using System.Text.Json; +using Azure.AI.Agents.Persistent; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.AzureAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.AzureAI; + +public sealed class AzureAIAgentExtensionsTests +{ + private static readonly JsonSerializerOptions s_jsonModelConvererOptions = new() { Converters = { new JsonModelConverter() } }; + private static readonly PersistentAgent s_agentMetadata = JsonSerializer.Deserialize( + """ + { + "id": "1", + "description": "A test agent", + "name": "TestAgent" + } + """, s_jsonModelConvererOptions)!; + + [Fact] + public void AsAIAgent_WithValidAzureAIAgent_ReturnsAIAgentAdapter() + { + // Arrange + var clientMock = new Mock(); + var azureAIAgent = new AzureAIAgent(s_agentMetadata, clientMock.Object); + + // Act + var result = azureAIAgent.AsAIAgent(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void AsAIAgent_WithNullAzureAIAgent_ThrowsArgumentNullException() + { + // Arrange + AzureAIAgent nullAgent = null!; + + // Act & Assert + Assert.Throws(() => nullAgent.AsAIAgent()); + } + + [Fact] + public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent() + { + // Arrange + var clientMock = new Mock(); + var azureAIAgent = new AzureAIAgent(s_agentMetadata, clientMock.Object); + + // Act + var result = azureAIAgent.AsAIAgent(); + + // Assert + var adapter = Assert.IsType(result); + Assert.Same(azureAIAgent, adapter.InnerAgent); + } + + [Fact] + public void AsAIAgent_CreatesWorkingThreadFactory() + { + var clientMock = new Mock(); + var azureAIAgent = new AzureAIAgent(s_agentMetadata, clientMock.Object); + + // Act + var result = azureAIAgent.AsAIAgent(); + var thread = result.GetNewThread(); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithNullAgentId_CreatesNewThread() + { + // Arrange + var clientMock = new Mock(); + var azureAIAgent = new AzureAIAgent(s_agentMetadata, clientMock.Object); + var jsonElement = JsonSerializer.SerializeToElement((string?)null); + + // Act + var result = azureAIAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithValidAgentId_CreatesThreadWithId() + { + // Arrange + var clientMock = new Mock(); + var azureAIAgent = new AzureAIAgent(s_agentMetadata, clientMock.Object); + + var threadId = "test-thread-id"; + var jsonElement = JsonSerializer.SerializeToElement(threadId); + + // Act + var result = azureAIAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + Assert.Equal(threadId, threadAdapter.InnerThread.Id); + } + + [Fact] + public void AsAIAgent_ThreadSerializer_SerializesThreadId() + { + // Arrange + var clientMock = new Mock(); + var azureAIAgent = new AzureAIAgent(s_agentMetadata, clientMock.Object); + + var expectedThreadId = "test-thread-id"; + var azureAIThread = new AzureAIAgentThread(clientMock.Object, expectedThreadId); + var jsonElement = JsonSerializer.SerializeToElement(expectedThreadId); + + var result = azureAIAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Act + var serializedElement = thread.Serialize(); + + // Assert + Assert.Equal(JsonValueKind.String, serializedElement.ValueKind); + Assert.Equal(expectedThreadId, serializedElement.GetString()); + } +} diff --git a/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs index e48faaa2b797..ecd26d58e84f 100644 --- a/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/Bedrock/Extensions.cs/BedrockAgentExtensionsTests.cs @@ -1,9 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. +using System; +using System.Text.Json; using System.Threading.Tasks; using Amazon.BedrockAgent; using Amazon.BedrockAgent.Model; using Amazon.BedrockAgentRuntime; +using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Bedrock; using Moq; using Xunit; @@ -30,6 +33,124 @@ public class BedrockAgentExtensionsTests Instruction = "Instruction must have at least 40 characters", }; + [Fact] + public void AsAIAgent_WithValidBedrockAgent_ReturnsAIAgentAdapter() + { + // Arrange + var (mockClient, mockRuntimeClient) = this.CreateMockClients(); + var bedrockAgent = new BedrockAgent(this._agentModel, mockClient.Object, mockRuntimeClient.Object); + + // Act + var result = bedrockAgent.AsAIAgent(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void AsAIAgent_WithNullBedrockAgent_ThrowsArgumentNullException() + { + // Arrange + BedrockAgent nullAgent = null!; + + // Act & Assert + Assert.Throws(() => nullAgent.AsAIAgent()); + } + + [Fact] + public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent() + { + // Arrange + var (mockClient, mockRuntimeClient) = this.CreateMockClients(); + var bedrockAgent = new BedrockAgent(this._agentModel, mockClient.Object, mockRuntimeClient.Object); + + // Act + var result = bedrockAgent.AsAIAgent(); + + // Assert + var adapter = Assert.IsType(result); + Assert.Same(bedrockAgent, adapter.InnerAgent); + } + + [Fact] + public void AsAIAgent_CreatesWorkingThreadFactory() + { + // Arrange + var (mockClient, mockRuntimeClient) = this.CreateMockClients(); + var bedrockAgent = new BedrockAgent(this._agentModel, mockClient.Object, mockRuntimeClient.Object); + + // Act + var result = bedrockAgent.AsAIAgent(); + var thread = result.GetNewThread(); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithNullAgentId_CreatesNewThread() + { + // Arrange + var (mockClient, mockRuntimeClient) = this.CreateMockClients(); + var bedrockAgent = new BedrockAgent(this._agentModel, mockClient.Object, mockRuntimeClient.Object); + var jsonElement = JsonSerializer.SerializeToElement((string?)null); + + // Act + var result = bedrockAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithValidAgentId_CreatesThreadWithId() + { + // Arrange + var (mockClient, mockRuntimeClient) = this.CreateMockClients(); + var bedrockAgent = new BedrockAgent(this._agentModel, mockClient.Object, mockRuntimeClient.Object); + var agentId = "test-agent-id"; + var jsonElement = JsonSerializer.SerializeToElement(agentId); + + // Act + var result = bedrockAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadSerializer_SerializesThreadId() + { + // Arrange + var (mockClient, mockRuntimeClient) = this.CreateMockClients(); + var bedrockAgent = new BedrockAgent(this._agentModel, mockClient.Object, mockRuntimeClient.Object); + var expectedThreadId = "test-thread-id"; + var bedrockThread = new BedrockAgentThread(mockRuntimeClient.Object, expectedThreadId); + var jsonElement = JsonSerializer.SerializeToElement(expectedThreadId); + + var result = bedrockAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Act + var serializedElement = thread.Serialize(); + + // Assert + Assert.Equal(JsonValueKind.String, serializedElement.ValueKind); + Assert.Equal(expectedThreadId, serializedElement.GetString()); + } + /// /// Verify the creation of the agent and the preparation of the agent. /// The status of the agent should be checked 3 times based on the setup. @@ -240,6 +361,11 @@ public async Task VerifyEnableUserInputActionGroupAsync() } }); + mockClient.Setup(x => x.PrepareAgentAsync( + It.IsAny(), + default) + ).ReturnsAsync(new PrepareAgentResponse { AgentId = this._agentModel.AgentId, AgentStatus = AgentStatus.PREPARING }); + return (mockClient, mockRuntimeClient); } diff --git a/dotnet/src/Agents/UnitTests/Copilot/CopilotStudioAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Copilot/CopilotStudioAgentExtensionsTests.cs new file mode 100644 index 000000000000..e831d362dab3 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Copilot/CopilotStudioAgentExtensionsTests.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using Microsoft.Agents.CopilotStudio.Client; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Copilot; +using Moq; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Copilot; + +public sealed class CopilotStudioAgentExtensionsTests +{ + [Fact] + public void AsAIAgent_WithValidCopilotStudioAgent_ReturnsAIAgentAdapter() + { + // Arrange + var clientMock = new Mock(null, null, null, null); + var copilotStudioAgent = new CopilotStudioAgent(clientMock.Object); + + // Act + var result = copilotStudioAgent.AsAIAgent(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void AsAIAgent_WithNullCopilotStudioAgent_ThrowsArgumentNullException() + { + // Arrange + CopilotStudioAgent nullAgent = null!; + + // Act & Assert + Assert.Throws(() => nullAgent.AsAIAgent()); + } + + [Fact] + public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent() + { + // Arrange + var clientMock = new Mock(null, null, null, null); + var copilotStudioAgent = new CopilotStudioAgent(clientMock.Object); + + // Act + var result = copilotStudioAgent.AsAIAgent(); + + // Assert + var adapter = Assert.IsType(result); + Assert.Same(copilotStudioAgent, adapter.InnerAgent); + } + + [Fact] + public void AsAIAgent_CreatesWorkingThreadFactory() + { + // Arrange + var clientMock = new Mock(null, null, null, null); + var copilotStudioAgent = new CopilotStudioAgent(clientMock.Object); + + // Act + var result = copilotStudioAgent.AsAIAgent(); + var thread = result.GetNewThread(); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithNullAgentId_CreatesNewThread() + { + // Arrange + var clientMock = new Mock(null, null, null, null); + var copilotStudioAgent = new CopilotStudioAgent(clientMock.Object); + var jsonElement = JsonSerializer.SerializeToElement((string?)null); + + // Act + var result = copilotStudioAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithValidAgentId_CreatesThreadWithId() + { + // Arrange + var clientMock = new Mock(null, null, null, null); + var copilotStudioAgent = new CopilotStudioAgent(clientMock.Object); + var agentId = "test-agent-id"; + var jsonElement = JsonSerializer.SerializeToElement(agentId); + + // Act + var result = copilotStudioAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadSerializer_SerializesThreadId() + { + // Arrange + var clientMock = new Mock(null, null, null, null); + var copilotStudioAgent = new CopilotStudioAgent(clientMock.Object); + var expectedThreadId = "test-thread-id"; + var copilotStudioThread = new CopilotStudioAgentThread(clientMock.Object, expectedThreadId); + var jsonElement = JsonSerializer.SerializeToElement(expectedThreadId); + + var result = copilotStudioAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Act + var serializedElement = thread.Serialize(); + + // Assert + Assert.Equal(JsonValueKind.String, serializedElement.ValueKind); + Assert.Equal(expectedThreadId, serializedElement.GetString()); + } +} diff --git a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentExtensionsTests.cs new file mode 100644 index 000000000000..70eaf1c536be --- /dev/null +++ b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentExtensionsTests.cs @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.Core; + +public sealed class ChatCompletionAgentExtensionsTests +{ + [Fact] + public void AsAIAgent_WithValidChatCompletionAgent_ReturnsAIAgentAdapter() + { + // Arrange + var chatCompletionAgent = new ChatCompletionAgent() + { + Name = "TestAgent", + Instructions = "Test instructions" + }; + + // Act + var result = chatCompletionAgent.AsAIAgent(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void AsAIAgent_WithNullChatCompletionAgent_ThrowsArgumentNullException() + { + // Arrange + ChatCompletionAgent nullAgent = null!; + + // Act & Assert + Assert.Throws(() => nullAgent.AsAIAgent()); + } + + [Fact] + public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent() + { + // Arrange + var chatCompletionAgent = new ChatCompletionAgent() + { + Name = "TestAgent", + Instructions = "Test instructions" + }; + + // Act + var result = chatCompletionAgent.AsAIAgent(); + + // Assert + var adapter = Assert.IsType(result); + Assert.Same(chatCompletionAgent, adapter.InnerAgent); + } + + [Fact] + public void AsAIAgent_CreatesWorkingThreadFactory() + { + // Arrange + var chatCompletionAgent = new ChatCompletionAgent() + { + Name = "TestAgent", + Instructions = "Test instructions" + }; + + // Act + var result = chatCompletionAgent.AsAIAgent(); + var thread = result.GetNewThread(); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithNullChatHistory_CreatesNewThread() + { + // Arrange + var chatCompletionAgent = new ChatCompletionAgent() + { + Name = "TestAgent", + Instructions = "Test instructions" + }; + var jsonElement = JsonSerializer.SerializeToElement((ChatHistory?)null); + + // Act + var result = chatCompletionAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithValidChatHistory_CreatesThreadWithHistory() + { + // Arrange + var chatCompletionAgent = new ChatCompletionAgent() + { + Name = "TestAgent", + Instructions = "Test instructions" + }; + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("System message"); + chatHistory.AddUserMessage("User message"); + var jsonElement = JsonSerializer.SerializeToElement(chatHistory); + + // Act + var result = chatCompletionAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + var chatHistoryThread = (ChatHistoryAgentThread)threadAdapter.InnerThread; + Assert.Equal(2, chatHistoryThread.ChatHistory.Count); + } + + [Fact] + public void AsAIAgent_ThreadSerializer_SerializesChatHistory() + { + // Arrange + var chatCompletionAgent = new ChatCompletionAgent() + { + Name = "TestAgent", + Instructions = "Test instructions" + }; + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("System message"); + chatHistory.AddUserMessage("User message"); + var chatHistoryThread = new ChatHistoryAgentThread(chatHistory); + var jsonElement = JsonSerializer.SerializeToElement(chatHistory); + + var result = chatCompletionAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Act + var serializedElement = thread.Serialize(); + + // Assert + Assert.Equal(JsonValueKind.Array, serializedElement.ValueKind); + var deserializedChatHistory = JsonSerializer.Deserialize(serializedElement.GetRawText()); + Assert.NotNull(deserializedChatHistory); + Assert.Equal(2, deserializedChatHistory.Count); + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentExtensionsTests.cs new file mode 100644 index 000000000000..74ff9523346c --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentExtensionsTests.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel.Primitives; +using System.Text.Json; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Moq; +using OpenAI.Assistants; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +public sealed class OpenAIAssistantAgentExtensionsTests +{ + private static readonly Assistant s_assistantDefinition = ModelReaderWriter.Read(BinaryData.FromString( + """ + { + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698984975, + "name": "TestAssistant", + "description": "A test assistant", + "model": "gpt-4", + "instructions": "Test instructions", + "tools": [], + "metadata": {} + } + """))!; + + [Fact] + public void AsAIAgent_WithValidOpenAIAssistantAgent_ReturnsAIAgentAdapter() + { + // Arrange + var clientMock = new Mock(); + var assistantAgent = new OpenAIAssistantAgent(s_assistantDefinition, clientMock.Object); + + // Act + var result = assistantAgent.AsAIAgent(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void AsAIAgent_WithNullOpenAIAssistantAgent_ThrowsArgumentNullException() + { + // Arrange + OpenAIAssistantAgent nullAgent = null!; + + // Act & Assert + Assert.Throws(() => nullAgent.AsAIAgent()); + } + + [Fact] + public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent() + { + // Arrange + var clientMock = new Mock(); + var assistantAgent = new OpenAIAssistantAgent(s_assistantDefinition, clientMock.Object); + + // Act + var result = assistantAgent.AsAIAgent(); + + // Assert + var adapter = Assert.IsType(result); + Assert.Same(assistantAgent, adapter.InnerAgent); + } + + [Fact] + public void AsAIAgent_CreatesWorkingThreadFactory() + { + // Arrange + var clientMock = new Mock(); + var assistantAgent = new OpenAIAssistantAgent(s_assistantDefinition, clientMock.Object); + + // Act + var result = assistantAgent.AsAIAgent(); + var thread = result.GetNewThread(); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithNullAgentId_CreatesNewThread() + { + // Arrange + var clientMock = new Mock(); + var assistantAgent = new OpenAIAssistantAgent(s_assistantDefinition, clientMock.Object); + var jsonElement = JsonSerializer.SerializeToElement((string?)null); + + // Act + var result = assistantAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithValidAgentId_CreatesThreadWithId() + { + // Arrange + var clientMock = new Mock(); + var assistantAgent = new OpenAIAssistantAgent(s_assistantDefinition, clientMock.Object); + var threadId = "test-thread-id"; + var jsonElement = JsonSerializer.SerializeToElement(threadId); + + // Act + var result = assistantAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + Assert.Equal(threadId, threadAdapter.InnerThread.Id); + } + + [Fact] + public void AsAIAgent_ThreadSerializer_SerializesThreadId() + { + // Arrange + var clientMock = new Mock(); + var assistantAgent = new OpenAIAssistantAgent(s_assistantDefinition, clientMock.Object); + var expectedThreadId = "test-thread-id"; + var assistantThread = new OpenAIAssistantAgentThread(clientMock.Object, expectedThreadId); + var jsonElement = JsonSerializer.SerializeToElement(expectedThreadId); + + var result = assistantAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Act + var serializedElement = thread.Serialize(); + + // Assert + Assert.Equal(JsonValueKind.String, serializedElement.ValueKind); + Assert.Equal(expectedThreadId, serializedElement.GetString()); + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIResponseAgentExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIResponseAgentExtensionsTests.cs new file mode 100644 index 000000000000..878b07c8b8b1 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIResponseAgentExtensionsTests.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using OpenAI.Responses; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +public sealed class OpenAIResponseAgentExtensionsTests +{ + [Fact] + public void AsAIAgent_WithValidOpenAIResponseAgent_ReturnsAIAgentAdapter() + { + // Arrange + var responseClient = new OpenAIResponseClient("model", "apikey"); + var responseAgent = new OpenAIResponseAgent(responseClient); + + // Act + var result = responseAgent.AsAIAgent(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void AsAIAgent_WithNullOpenAIResponseAgent_ThrowsArgumentNullException() + { + // Arrange + OpenAIResponseAgent nullAgent = null!; + + // Act & Assert + Assert.Throws(() => nullAgent.AsAIAgent()); + } + + [Fact] + public void AsAIAgent_ReturnsAdapterWithCorrectInnerAgent() + { + // Arrange + var responseClient = new OpenAIResponseClient("model", "apikey"); + var responseAgent = new OpenAIResponseAgent(responseClient); + + // Act + var result = responseAgent.AsAIAgent(); + + // Assert + var adapter = Assert.IsType(result); + Assert.Same(responseAgent, adapter.InnerAgent); + } + + [Fact] + public void AsAIAgent_CreatesWorkingThreadFactory() + { + // Arrange + var responseClient = new OpenAIResponseClient("model", "apikey"); + var responseAgent = new OpenAIResponseAgent(responseClient); + + // Act + var result = responseAgent.AsAIAgent(); + var thread = result.GetNewThread(); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithNullAgentId_CreatesNewThread() + { + // Arrange + var responseClient = new OpenAIResponseClient("model", "apikey"); + var responseAgent = new OpenAIResponseAgent(responseClient); + var jsonElement = JsonSerializer.SerializeToElement((string?)null); + + // Act + var result = responseAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + } + + [Fact] + public void AsAIAgent_ThreadDeserializationFactory_WithValidAgentId_CreatesThreadWithId() + { + // Arrange + var responseClient = new OpenAIResponseClient("model", "apikey"); + var responseAgent = new OpenAIResponseAgent(responseClient); + var threadId = "test-agent-id"; + var jsonElement = JsonSerializer.SerializeToElement(threadId); + + // Act + var result = responseAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Assert + Assert.NotNull(thread); + Assert.IsType(thread); + var threadAdapter = (AIAgentThreadAdapter)thread; + Assert.IsType(threadAdapter.InnerThread); + Assert.Equal(threadId, threadAdapter.InnerThread.Id); + } + + [Fact] + public void AsAIAgent_ThreadSerializer_SerializesThreadId() + { + // Arrange + var responseClient = new OpenAIResponseClient("model", "apikey"); + var responseAgent = new OpenAIResponseAgent(responseClient); + var expectedThreadId = "test-thread-id"; + var responseThread = new OpenAIResponseAgentThread(responseClient, expectedThreadId); + var jsonElement = JsonSerializer.SerializeToElement(expectedThreadId); + + var result = responseAgent.AsAIAgent(); + var thread = result.DeserializeThread(jsonElement); + + // Act + var serializedElement = thread.Serialize(); + + // Assert + Assert.Equal(JsonValueKind.String, serializedElement.ValueKind); + Assert.Equal(expectedThreadId, serializedElement.GetString()); + } +} diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AIAgentAdapterTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AIAgentAdapterTests.cs new file mode 100644 index 000000000000..8d510d800e26 --- /dev/null +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AIAgentAdapterTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AIAgentAdapterConformance; + +public abstract class AIAgentAdapterTests(Func createAgentFixture) : IAsyncLifetime +{ +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + private AgentFixture _agentFixture; +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + + protected AgentFixture Fixture => this._agentFixture; + + [Fact] + public virtual async Task ConvertAndRunAgentAsync() + { + var aiagent = this.Fixture.AIAgent; + var thread = aiagent.GetNewThread(); + + var result = await aiagent.RunAsync("What is the capital of France?", thread); + Assert.Contains("Paris", result.Text, StringComparison.OrdinalIgnoreCase); + + var serialisedTreadJsonElement = thread.Serialize(); + + var deserializedThread = aiagent.DeserializeThread(serialisedTreadJsonElement); + + var secondResult = await aiagent.RunAsync("And Austria?", deserializedThread); + Assert.Contains("Vienna", secondResult.Text, StringComparison.OrdinalIgnoreCase); + } + + public Task InitializeAsync() + { + this._agentFixture = createAgentFixture(); + return this._agentFixture.InitializeAsync(); + } + + public Task DisposeAsync() + { + return this._agentFixture.DisposeAsync(); + } +} diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AzureAIAgentAdapterTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AzureAIAgentAdapterTests.cs new file mode 100644 index 000000000000..e78df12447b2 --- /dev/null +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/AzureAIAgentAdapterTests.cs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AIAgentAdapterConformance; + +public class AzureAIAgentAdapterTests() : AIAgentAdapterTests(() => new AzureAIAgentFixture()); diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/BedrockAgentAdapterTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/BedrockAgentAdapterTests.cs new file mode 100644 index 000000000000..7a32afad292e --- /dev/null +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/BedrockAgentAdapterTests.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AIAgentAdapterConformance; + +public class BedrockAgentAdapterTests() : AIAgentAdapterTests(() => new BedrockAgentFixture()) +{ + private const string ManualVerificationSkipReason = "This test is for manual verification."; + + [Fact(Skip = ManualVerificationSkipReason)] + public override Task ConvertAndRunAgentAsync() + { + return base.ConvertAndRunAgentAsync(); + } +} diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/ChatCompletionAgentAdapterTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/ChatCompletionAgentAdapterTests.cs new file mode 100644 index 000000000000..cb8cf145500d --- /dev/null +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/ChatCompletionAgentAdapterTests.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AIAgentAdapterConformance; + +public class ChatCompletionAgentAdapterTests() : AIAgentAdapterTests(() => new ChatCompletionAgentFixture()) +{ +} diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIAssistantAgentAdapterTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIAssistantAgentAdapterTests.cs new file mode 100644 index 000000000000..a393d773ab8f --- /dev/null +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIAssistantAgentAdapterTests.cs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AIAgentAdapterConformance; + +public class OpenAIAssistantAgentAdapterTests() : AIAgentAdapterTests(() => new OpenAIAssistantAgentFixture()); diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIResponseAgentAdapterTests.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIResponseAgentAdapterTests.cs new file mode 100644 index 000000000000..d3ed57b47587 --- /dev/null +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AIAgentAdapterConformance/OpenAIResponseAgentAdapterTests.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance.AIAgentAdapterConformance; + +public class OpenAIResponseAgentAdapterTests() : AIAgentAdapterTests(() => new OpenAIResponseAgentFixture()) +{ +} diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentFixture.cs index 89ea56fa4e27..fea7e5859e7c 100644 --- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentFixture.cs +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AgentFixture.cs @@ -4,6 +4,7 @@ using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; using Xunit; +using MAAI = Microsoft.Agents.AI; namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance; @@ -15,6 +16,8 @@ public abstract class AgentFixture : IAsyncLifetime { public abstract Agent Agent { get; } + public abstract MAAI.AIAgent AIAgent { get; } + public abstract AgentThread AgentThread { get; } public abstract AgentThread CreatedAgentThread { get; } diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs index 29c96bc5e371..958858bcd2cb 100644 --- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/AzureAIAgentFixture.cs @@ -10,6 +10,7 @@ using Microsoft.SemanticKernel.Agents.AzureAI; using Microsoft.SemanticKernel.ChatCompletion; using SemanticKernel.IntegrationTests.TestSettings; +using MAAI = Microsoft.Agents.AI; namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance; @@ -34,6 +35,8 @@ public class AzureAIAgentFixture : AgentFixture public override Agent Agent => this._agent!; + public override MAAI.AIAgent AIAgent => this._agent!.AsAIAgent(); + public override AgentThread AgentThread => this._thread!; public override AgentThread CreatedAgentThread => this._createdThread!; diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/BedrockAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/BedrockAgentFixture.cs index d9ba2a333003..e3e738856f22 100644 --- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/BedrockAgentFixture.cs +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/BedrockAgentFixture.cs @@ -15,6 +15,7 @@ using Microsoft.SemanticKernel.ChatCompletion; using SemanticKernel.IntegrationTests.TestSettings; using Xunit; +using MAAI = Microsoft.Agents.AI; namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance; @@ -39,6 +40,8 @@ public sealed class BedrockAgentFixture : AgentFixture, IAsyncDisposable public override Microsoft.SemanticKernel.Agents.Agent Agent => this._agent!; + public override MAAI.AIAgent AIAgent => this._agent!.AsAIAgent(); + public override AgentThread AgentThread => this._thread!; public override AgentThread CreatedAgentThread => this._createdThread!; diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentFixture.cs index f866de16fbf6..fbb0766b86ff 100644 --- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentFixture.cs +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/ChatCompletionAgentFixture.cs @@ -7,6 +7,7 @@ using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; using SemanticKernel.IntegrationTests.TestSettings; +using MAAI = Microsoft.Agents.AI; namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance; @@ -28,6 +29,8 @@ public class ChatCompletionAgentFixture : AgentFixture public override Agent Agent => this._agent!; + public override MAAI.AIAgent AIAgent => this._agent!.AsAIAgent(); + public override AgentThread AgentThread => this._thread!; public override AgentThread CreatedAgentThread => this._createdThread!; diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs index fbcbda581a9c..053743ab3f78 100644 --- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIAssistantAgentFixture.cs @@ -11,6 +11,7 @@ using Microsoft.SemanticKernel.ChatCompletion; using OpenAI.Assistants; using SemanticKernel.IntegrationTests.TestSettings; +using MAAI = Microsoft.Agents.AI; namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance; @@ -38,6 +39,8 @@ public class OpenAIAssistantAgentFixture : AgentFixture public override Agent Agent => this._agent!; + public override MAAI.AIAgent AIAgent => this._agent!.AsAIAgent(); + public override AgentThread AgentThread => this._thread!; public override AgentThread CreatedAgentThread => this._createdThread!; diff --git a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIResponseAgentFixture.cs b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIResponseAgentFixture.cs index 5a14cd765374..fcc799315508 100644 --- a/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIResponseAgentFixture.cs +++ b/dotnet/src/IntegrationTests/Agents/CommonInterfaceConformance/OpenAIResponseAgentFixture.cs @@ -9,6 +9,7 @@ using OpenAI; using OpenAI.Responses; using SemanticKernel.IntegrationTests.TestSettings; +using MAAI = Microsoft.Agents.AI; namespace SemanticKernel.IntegrationTests.Agents.CommonInterfaceConformance; @@ -35,6 +36,8 @@ public class OpenAIResponseAgentFixture : AgentFixture public override Agent Agent => this._agent!; + public override MAAI.AIAgent AIAgent => this._agent!.AsAIAgent(); + public override AgentThread AgentThread => this._thread!; public override AgentThread CreatedAgentThread => this._createdThread!; diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/ChatMessageExtensions.cs b/dotnet/src/InternalUtilities/meai/Extensions/ChatMessageExtensions.cs similarity index 87% rename from dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/ChatMessageExtensions.cs rename to dotnet/src/InternalUtilities/meai/Extensions/ChatMessageExtensions.cs index f9198c46c4c4..b82bfb61f577 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/ChatMessageExtensions.cs +++ b/dotnet/src/InternalUtilities/meai/Extensions/ChatMessageExtensions.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; -using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; -namespace Microsoft.SemanticKernel.ChatCompletion; +namespace Microsoft.Extensions.AI; +[ExcludeFromCodeCoverage] internal static class ChatMessageExtensions { /// Converts a to a . @@ -22,6 +25,7 @@ internal static ChatMessageContent ToChatMessageContent(this ChatMessage message foreach (AIContent content in message.Contents) { +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. KernelContent? resultContent = content switch { Microsoft.Extensions.AI.TextContent tc => new Microsoft.SemanticKernel.TextContent(tc.Text), @@ -41,6 +45,7 @@ internal static ChatMessageContent ToChatMessageContent(this ChatMessage message result: frc.Result), _ => null }; +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. if (resultContent is not null) { diff --git a/dotnet/src/InternalUtilities/meai/MEAIUtilities.props b/dotnet/src/InternalUtilities/meai/MEAIUtilities.props new file mode 100644 index 000000000000..57f8ec064371 --- /dev/null +++ b/dotnet/src/InternalUtilities/meai/MEAIUtilities.props @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs index 74a8c24fbe11..5757ad9fb966 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/ChatClient/KernelFunctionInvokingChatClient.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.Extensions.AI; diff --git a/dotnet/src/SemanticKernel.Abstractions/Functions/FunctionResult.cs b/dotnet/src/SemanticKernel.Abstractions/Functions/FunctionResult.cs index 4454c2c0b493..4bc58f6c7156 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Functions/FunctionResult.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Functions/FunctionResult.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.Linq; using Microsoft.Extensions.AI; -using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel; diff --git a/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj b/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj index a048b020e8c4..d69f2d0cc9b5 100644 --- a/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj +++ b/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj @@ -15,6 +15,7 @@ + diff --git a/dotnet/src/SemanticKernel.UnitTests/Extensions/ChatMessageExtensionsTests.cs b/dotnet/src/SemanticKernel.UnitTests/Extensions/ChatMessageExtensionsTests.cs new file mode 100644 index 000000000000..982b4122d06e --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Extensions/ChatMessageExtensionsTests.cs @@ -0,0 +1,516 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.AI; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; + +namespace SemanticKernel.UnitTests.Extensions; + +/// +/// Unit tests for class. +/// +public sealed class ChatMessageExtensionsTests +{ + [Fact] + public void ToChatMessageContentWithTextContentReturnsCorrectChatMessageContent() + { + // Arrange + var chatMessage = new ChatMessage(ChatRole.User, "Hello, world!"); + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Equal(AuthorRole.User, result.Role); + Assert.Single(result.Items); + var textContent = Assert.IsType(result.Items[0]); + Assert.Equal("Hello, world!", textContent.Text); + } + + [Fact] + public void ToChatMessageContentWithAuthorNameSetsAuthorName() + { + // Arrange + var chatMessage = new ChatMessage(ChatRole.Assistant, "Response") + { + AuthorName = "TestAssistant" + }; + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Equal("TestAssistant", result.AuthorName); + } + + [Fact] + public void ToChatMessageContentWithResponseSetsModelIdFromResponse() + { + // Arrange + var chatMessage = new ChatMessage(ChatRole.Assistant, "Response"); + var response = new ChatResponse(new[] { chatMessage }) + { + ModelId = "gpt-4" + }; + + // Act + var result = chatMessage.ToChatMessageContent(response); + + // Assert + Assert.NotNull(result); + Assert.Equal("gpt-4", result.ModelId); + } + + [Fact] + public void ToChatMessageContentWithImageDataContentCreatesImageContent() + { + // Arrange + var imageUri = new Uri("data:image/png;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA"); + var chatMessage = new ChatMessage(ChatRole.User, [ + new DataContent(imageUri, "image/png") + ]); + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Items); + var imageContent = Assert.IsType(result.Items[0]); + Assert.Equal(imageUri.OriginalString, imageContent.DataUri); + } + + [Fact] + public void ToChatMessageContentWithImageUriContentCreatesImageContent() + { + // Arrange + var imageUri = new Uri("https://example.com/image.jpg"); + var chatMessage = new ChatMessage(ChatRole.User, [ + new UriContent(imageUri, "image/jpeg") + ]); + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Items); + var imageContent = Assert.IsType(result.Items[0]); + Assert.Equal(imageUri, imageContent.Uri); + } + + [Fact] + public void ToChatMessageContentWithAudioDataContentCreatesAudioContent() + { + // Arrange + var audioUri = new Uri("data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA"); + var chatMessage = new ChatMessage(ChatRole.User, [ + new DataContent(audioUri, "audio/mpeg") + ]); + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Items); + var audioContent = Assert.IsType(result.Items[0]); + Assert.Equal(audioUri.OriginalString, audioContent.DataUri); + } + + [Fact] + public void ToChatMessageContentWithAudioUriContentCreatesAudioContent() + { + // Arrange + var audioUri = new Uri("http://example.com/audio.wav"); + var chatMessage = new ChatMessage(ChatRole.User, [ + new UriContent(audioUri, "audio/wav") + ]); + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Items); + var audioContent = Assert.IsType(result.Items[0]); + Assert.Equal(audioUri, audioContent.Uri); + } + + [Fact] + public void ToChatMessageContentWithBinaryDataContentCreatesBinaryContent() + { + // Arrange + var dataUri = new Uri("data:application/octet-stream;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQAAAAA="); + var chatMessage = new ChatMessage(ChatRole.User, [ + new DataContent(dataUri, "application/octet-stream") + ]); + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Items); + var binaryContent = Assert.IsType(result.Items[0]); + Assert.Equal(dataUri.OriginalString, binaryContent.DataUri); + } + + [Fact] + public void ToChatMessageContentWithBinaryUriContentCreatesBinaryContent() + { + // Arrange + var dataUri = new Uri("https://example.com/data.pdf"); + var chatMessage = new ChatMessage(ChatRole.User, [ + new UriContent(dataUri, "application/pdf") + ]); + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Items); + var binaryContent = Assert.IsType(result.Items[0]); + Assert.Equal(dataUri, binaryContent.Uri); + } + + [Fact] + public void ToChatMessageContentWithFunctionCallContentCreatesFunctionCallContent() + { + // Arrange + var arguments = new Dictionary { { "param1", "value1" } }; + var chatMessage = new ChatMessage(ChatRole.Assistant, [ + new Microsoft.Extensions.AI.FunctionCallContent("call-123", "MyFunction", arguments) + ]); + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Items); + var functionCall = Assert.IsType(result.Items[0]); + Assert.Equal("call-123", functionCall.Id); + Assert.Equal("MyFunction", functionCall.FunctionName); + Assert.NotNull(functionCall.Arguments); + } + + [Fact] + public void ToChatMessageContentWithFunctionResultContentCreatesFunctionResultContent() + { + // Arrange + var functionCallMessage = new ChatMessage(ChatRole.Assistant, [ + new Microsoft.Extensions.AI.FunctionCallContent("call-123", "MyFunction") + ]); + var resultMessage = new ChatMessage(ChatRole.Tool, [ + new Microsoft.Extensions.AI.FunctionResultContent("call-123", "result value") + ]); + var response = new ChatResponse(new[] { functionCallMessage, resultMessage }); + + // Act + var result = resultMessage.ToChatMessageContent(response); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Items); + var functionResult = Assert.IsType(result.Items[0]); + Assert.Equal("call-123", functionResult.CallId); + Assert.Equal("MyFunction", functionResult.FunctionName); + Assert.Equal("result value", functionResult.Result); + } + + [Fact] + public void ToChatMessageContentWithMultipleContentItemsCreatesMultipleItems() + { + // Arrange + var chatMessage = new ChatMessage(ChatRole.User, [ + new Microsoft.Extensions.AI.TextContent("Hello"), + new Microsoft.Extensions.AI.TextContent("World") + ]); + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Items.Count); + Assert.All(result.Items, item => Assert.IsType(item)); + } + + [Fact] + public void ToChatMessageContentWithAdditionalPropertiesSetsMetadata() + { + // Arrange + var additionalProps = new AdditionalPropertiesDictionary + { + { "customKey", "customValue" } + }; + var chatMessage = new ChatMessage(ChatRole.User, "Test") + { + AdditionalProperties = additionalProps + }; + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Metadata); + Assert.True(result.Metadata.ContainsKey("customKey")); + Assert.Equal("customValue", result.Metadata["customKey"]); + } + + [Fact] + public void ToChatMessageContentWithUsageInResponseSetsUsageInMetadata() + { + // Arrange + var chatMessage = new ChatMessage(ChatRole.Assistant, "Response"); + var usage = new UsageDetails + { + InputTokenCount = 10, + OutputTokenCount = 20 + }; + var response = new ChatResponse(new[] { chatMessage }) + { + Usage = usage + }; + + // Act + var result = chatMessage.ToChatMessageContent(response); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Metadata); + Assert.True(result.Metadata.ContainsKey("Usage")); + Assert.Same(usage, result.Metadata["Usage"]); + } + + [Fact] + public void ToChatMessageContentWithRawRepresentationSetsInnerContent() + { + // Arrange + var rawObject = new { test = "value" }; + var chatMessage = new ChatMessage(ChatRole.User, "Test") + { + RawRepresentation = rawObject + }; + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Same(rawObject, result.InnerContent); + } + + [Fact] + public void ToChatMessageContentWithResponseRawRepresentationUsesResponseRawRepresentation() + { + // Arrange + var messageRaw = new { message = "value" }; + var responseRaw = new { response = "value" }; + var chatMessage = new ChatMessage(ChatRole.User, "Test") + { + RawRepresentation = messageRaw + }; + var response = new ChatResponse(new[] { chatMessage }) + { + RawRepresentation = responseRaw + }; + + // Act + var result = chatMessage.ToChatMessageContent(response); + + // Assert + Assert.NotNull(result); + Assert.Same(responseRaw, result.InnerContent); + } + + [Fact] + public void ToChatMessageContentSetsContentMetadataFromAIContent() + { + // Arrange + var contentProps = new AdditionalPropertiesDictionary { { "contentKey", "contentValue" } }; + var contentRaw = new { content = "raw" }; + var textContent = new Microsoft.Extensions.AI.TextContent("Hello") + { + AdditionalProperties = contentProps, + RawRepresentation = contentRaw + }; + var chatMessage = new ChatMessage(ChatRole.User, [textContent]); + var response = new ChatResponse(new[] { chatMessage }) + { + ModelId = "gpt-4" + }; + + // Act + var result = chatMessage.ToChatMessageContent(response); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Items); + var resultContent = result.Items[0]; + Assert.NotNull(resultContent.Metadata); + Assert.Equal("contentValue", resultContent.Metadata["contentKey"]); + Assert.Same(contentRaw, resultContent.InnerContent); + Assert.Equal("gpt-4", resultContent.ModelId); + } + + [Fact] + public void ToChatHistoryWithEmptyCollectionReturnsEmptyChatHistory() + { + // Arrange + var messages = Array.Empty(); + + // Act + var result = messages.ToChatHistory(); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void ToChatHistoryWithMultipleMessagesReturnsCorrectChatHistory() + { + // Arrange + var messages = new[] + { + new ChatMessage(ChatRole.System, "You are a helpful assistant."), + new ChatMessage(ChatRole.User, "Hello!"), + new ChatMessage(ChatRole.Assistant, "Hi there! How can I help you?") + }; + + // Act + var result = messages.ToChatHistory(); + + // Assert + Assert.NotNull(result); + Assert.Equal(3, result.Count); + Assert.Equal(AuthorRole.System, result[0].Role); + Assert.Equal(AuthorRole.User, result[1].Role); + Assert.Equal(AuthorRole.Assistant, result[2].Role); + } + + [Fact] + public void ToChatHistoryPreservesMessageOrder() + { + // Arrange + var messages = new[] + { + new ChatMessage(ChatRole.User, "First"), + new ChatMessage(ChatRole.Assistant, "Second"), + new ChatMessage(ChatRole.User, "Third") + }; + + // Act + var result = messages.ToChatHistory(); + + // Assert + Assert.NotNull(result); + Assert.Equal(3, result.Count); + Assert.Equal("First", ((Microsoft.SemanticKernel.TextContent)result[0].Items[0]).Text); + Assert.Equal("Second", ((Microsoft.SemanticKernel.TextContent)result[1].Items[0]).Text); + Assert.Equal("Third", ((Microsoft.SemanticKernel.TextContent)result[2].Items[0]).Text); + } + + [Fact] + public void ToChatHistoryWithComplexMessagesConvertsAllContent() + { + // Arrange + var imageUri = new Uri("https://example.com/image.png"); + var messages = new[] + { + new ChatMessage(ChatRole.User, [ + new Microsoft.Extensions.AI.TextContent("Look at this image:"), + new UriContent(imageUri, "image/png") + ]) + }; + + // Act + var result = messages.ToChatHistory(); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal(2, result[0].Items.Count); + Assert.IsType(result[0].Items[0]); + Assert.IsType(result[0].Items[1]); + } + + [Fact] + public void ToChatMessageContentWithNullFunctionCallIdDoesNotThrow() + { + // Arrange + var chatMessage = new ChatMessage(ChatRole.Tool, [ + new Microsoft.Extensions.AI.FunctionResultContent("call-456", "result") + ]); + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Items); + var functionResult = Assert.IsType(result.Items[0]); + Assert.Null(functionResult.FunctionName); + } + + [Fact] + public void ToChatMessageContentWithSystemRoleMapsToSystemRole() + { + // Arrange + var chatMessage = new ChatMessage(ChatRole.System, "You are helpful."); + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Equal(AuthorRole.System, result.Role); + } + + [Fact] + public void ToChatMessageContentWithToolRoleMapsToToolRole() + { + // Arrange + var chatMessage = new ChatMessage(ChatRole.Tool, "Tool result"); + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Equal(AuthorRole.Tool, result.Role); + } + + [Fact] + public void ToChatMessageContentWithUnknownContentTypeSkipsContent() + { + // Arrange + var chatMessage = new ChatMessage(ChatRole.User, [ + new Microsoft.Extensions.AI.TextContent("Valid text"), + new CustomAIContent() // Unknown content type + ]); + + // Act + var result = chatMessage.ToChatMessageContent(); + + // Assert + Assert.NotNull(result); + Assert.Single(result.Items); // Only the text content should be included + Assert.IsType(result.Items[0]); + } + + /// + /// Custom AI content type for testing unknown content handling + /// + private sealed class CustomAIContent : AIContent + { + } +}