diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 09e82c027b..05f73bd947 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -4,6 +4,14 @@ + + + + + + + + @@ -37,6 +45,13 @@ + + + + + + + diff --git a/dotnet/samples/GettingStarted/GettingStarted.csproj b/dotnet/samples/GettingStarted/GettingStarted.csproj index 8a07b3eec0..2d13bb0bc0 100644 --- a/dotnet/samples/GettingStarted/GettingStarted.csproj +++ b/dotnet/samples/GettingStarted/GettingStarted.csproj @@ -2,10 +2,7 @@ $(ProjectsTargetFrameworks) - - - - $(ProjectsDebugTargetFrameworks) + $(ProjectsDebugTargetFrameworks) diff --git a/dotnet/src/Microsoft.Agents.Abstractions/Microsoft.Agents.Abstractions.csproj b/dotnet/src/Microsoft.Agents.Abstractions/Microsoft.Agents.Abstractions.csproj index ea7e91804b..9f8d8c231f 100644 --- a/dotnet/src/Microsoft.Agents.Abstractions/Microsoft.Agents.Abstractions.csproj +++ b/dotnet/src/Microsoft.Agents.Abstractions/Microsoft.Agents.Abstractions.csproj @@ -2,6 +2,7 @@ $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) Microsoft.Agents alpha @@ -10,10 +11,6 @@ true - - $(ProjectsDebugTargetFrameworks) - - diff --git a/dotnet/src/Microsoft.Agents/Microsoft.Agents.csproj b/dotnet/src/Microsoft.Agents/Microsoft.Agents.csproj index 258357a0f6..28dce96e44 100644 --- a/dotnet/src/Microsoft.Agents/Microsoft.Agents.csproj +++ b/dotnet/src/Microsoft.Agents/Microsoft.Agents.csproj @@ -2,6 +2,7 @@ $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) alpha @@ -9,10 +10,6 @@ true true - - - $(ProjectsDebugTargetFrameworks) - diff --git a/dotnet/tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj b/dotnet/tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj new file mode 100644 index 0000000000..2e03f8eaf9 --- /dev/null +++ b/dotnet/tests/AgentConformance.IntegrationTests/AgentConformance.IntegrationTests.csproj @@ -0,0 +1,21 @@ + + + + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) + false + + + + + + + + + + + + + + + diff --git a/dotnet/tests/AgentConformance.IntegrationTests/AgentFixture.cs b/dotnet/tests/AgentConformance.IntegrationTests/AgentFixture.cs new file mode 100644 index 0000000000..fb6b04e803 --- /dev/null +++ b/dotnet/tests/AgentConformance.IntegrationTests/AgentFixture.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Agents; +using Microsoft.Extensions.AI; + +namespace AgentConformanceTests; + +/// +/// Base class for setting up and tearing down agents, to be used in tests. +/// Each agent type should have its own derived class. +/// +public abstract class AgentFixture : IAsyncLifetime +{ + public abstract Agent Agent { get; } + + public abstract AgentThread AgentThread { get; } + + public abstract Task> GetChatHistory(); + + public abstract Task DisposeAsync(); + + public abstract Task InitializeAsync(); +} diff --git a/dotnet/tests/AgentConformance.IntegrationTests/AgentTests.cs b/dotnet/tests/AgentConformance.IntegrationTests/AgentTests.cs new file mode 100644 index 0000000000..1d04b1a5c3 --- /dev/null +++ b/dotnet/tests/AgentConformance.IntegrationTests/AgentTests.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using AgentConformanceTests; + +namespace AgentConformance.IntegrationTests; + +/// +/// Base class for all test classes used for testing agents. +/// +/// The type of the agent fixture used in these tests. +/// Used to create a new fixture for this test suite. +public abstract class AgentTests(Func createAgentFixture) : IAsyncLifetime + where TAgentFixture : AgentFixture +{ +#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. + protected TAgentFixture Fixture { get; private set; } +#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. + + public Task InitializeAsync() + { + this.Fixture = createAgentFixture(); + return this.Fixture.InitializeAsync(); + } + + public Task DisposeAsync() + { + return this.Fixture.DisposeAsync(); + } +} diff --git a/dotnet/tests/AgentConformance.IntegrationTests/RunAsyncTests.cs b/dotnet/tests/AgentConformance.IntegrationTests/RunAsyncTests.cs new file mode 100644 index 0000000000..9e72401dbb --- /dev/null +++ b/dotnet/tests/AgentConformance.IntegrationTests/RunAsyncTests.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Threading.Tasks; +using AgentConformanceTests; +using Microsoft.Extensions.AI; + +namespace AgentConformance.IntegrationTests; + +/// +/// Conformance tests for run methods on agents. +/// +/// The type of test fixture used by the concrete test implementation. +/// Function to create the test fixture with. +public abstract class RunAsyncTests(Func createAgentFixture) : AgentTests(createAgentFixture) + where TAgentFixture : AgentFixture +{ + [RetryFact(3, 5000)] + public virtual async Task RunReturnsResultAsync() + { + // Arrange + var agent = this.Fixture.Agent; + var thread = agent.GetNewThread(); + + // Act + var chatResponse = await agent.RunAsync(new ChatMessage(ChatRole.User, "What is the capital of France."), thread); + + // Assert + Assert.NotNull(chatResponse); + Assert.Single(chatResponse.Messages); + Assert.Contains("Paris", chatResponse.Text); + } +} diff --git a/dotnet/tests/AgentConformance.IntegrationTests/TestConfiguration.cs b/dotnet/tests/AgentConformance.IntegrationTests/TestConfiguration.cs new file mode 100644 index 0000000000..9c4bf61cda --- /dev/null +++ b/dotnet/tests/AgentConformance.IntegrationTests/TestConfiguration.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Extensions.Configuration; + +namespace AgentConformance.IntegrationTests; + +/// +/// Helper for loading test configuration settings. +/// +public sealed class TestConfiguration +{ + private static readonly IConfiguration s_configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true) + .AddJsonFile(path: "testsettings.development.json", optional: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + /// + /// Loads the type of configuration using a section name based on the type name. + /// + /// The type of config to load. + /// The loaded configuration section of the specified type. + /// Thrown if the configuration section cannot be loaded. + public static T LoadSection() + { + var configType = typeof(T); + var configTypeName = configType.Name; + + var trimText = "Configuration"; + if (configTypeName.EndsWith(trimText, StringComparison.OrdinalIgnoreCase)) + { + configTypeName = configTypeName.Substring(0, configTypeName.Length - trimText.Length); + } + + return s_configuration.GetRequiredSection(configTypeName).Get() ?? + throw new InvalidOperationException($"Could not load config for {configTypeName}."); + } +} diff --git a/dotnet/tests/Directory.Build.props b/dotnet/tests/Directory.Build.props index c54e3c39e5..e327ae25e0 100644 --- a/dotnet/tests/Directory.Build.props +++ b/dotnet/tests/Directory.Build.props @@ -8,18 +8,21 @@ false net472;net9.0 net9.0 + b7762d10-e29b-4bb1-8b74-b6d69a667dd4 + + diff --git a/dotnet/tests/Microsoft.Agents.Abstractions.UnitTests/Microsoft.Agents.Abstractions.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.Abstractions.UnitTests/Microsoft.Agents.Abstractions.UnitTests.csproj index 02023b3bce..3d7a73f48f 100644 --- a/dotnet/tests/Microsoft.Agents.Abstractions.UnitTests/Microsoft.Agents.Abstractions.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.Abstractions.UnitTests/Microsoft.Agents.Abstractions.UnitTests.csproj @@ -2,10 +2,7 @@ $(ProjectsTargetFrameworks) - - - - $(ProjectsDebugTargetFrameworks) + $(ProjectsDebugTargetFrameworks) diff --git a/dotnet/tests/Microsoft.Agents.UnitTests/Microsoft.Agents.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.UnitTests/Microsoft.Agents.UnitTests.csproj index cdfe20b4d3..5077e67861 100644 --- a/dotnet/tests/Microsoft.Agents.UnitTests/Microsoft.Agents.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.UnitTests/Microsoft.Agents.UnitTests.csproj @@ -2,10 +2,7 @@ $(ProjectsTargetFrameworks) - - - - $(ProjectsDebugTargetFrameworks) + $(ProjectsDebugTargetFrameworks) diff --git a/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletion.IntegrationTests.csproj b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletion.IntegrationTests.csproj new file mode 100644 index 0000000000..bc3b7f2d89 --- /dev/null +++ b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletion.IntegrationTests.csproj @@ -0,0 +1,16 @@ + + + + $(ProjectsTargetFrameworks) + $(ProjectsDebugTargetFrameworks) + + + + + + + + + + + diff --git a/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs new file mode 100644 index 0000000000..5a2014f820 --- /dev/null +++ b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionFixture.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; +using AgentConformance.IntegrationTests; +using AgentConformanceTests; +using Microsoft.Agents; +using Microsoft.Extensions.AI; +using OpenAI; + +namespace OpenAIChatCompletion.IntegrationTests; + +public class OpenAIChatCompletionFixture : AgentFixture +{ +#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 IChatClient _chatClient; + private Agent _agent; + private AgentThread _agentThread; +#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. + + public override Agent Agent => this._agent; + + public override AgentThread AgentThread => this._agentThread; + + public override Task> GetChatHistory() + { + throw new System.NotImplementedException(); + } + + public override Task InitializeAsync() + { + var config = TestConfiguration.LoadSection(); + + this._chatClient = new OpenAIClient(config.ApiKey) + .GetChatClient(config.ChatModelId) + .AsIChatClient(); + + this._agentThread = new ChatClientAgentThread(); + + this._agent = + new ChatClientAgent(this._chatClient, new() + { + Name = "HelpfulAssistant", + Instructions = "You are a helpful assistant.", + }); + + return Task.CompletedTask; + } + + public override Task DisposeAsync() + { + this._chatClient.Dispose(); + return Task.CompletedTask; + } +} diff --git a/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionInvokeTests.cs b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionInvokeTests.cs new file mode 100644 index 0000000000..45e7993350 --- /dev/null +++ b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIChatCompletionInvokeTests.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. + +using AgentConformance.IntegrationTests; + +namespace OpenAIChatCompletion.IntegrationTests; + +public class OpenAIChatCompletionInvokeTests() : RunAsyncTests(() => new()) +{ +} diff --git a/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIConfiguration.cs b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIConfiguration.cs new file mode 100644 index 0000000000..e9a01a2ced --- /dev/null +++ b/dotnet/tests/OpenAIChatCompletion.IntegrationTests/OpenAIConfiguration.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace OpenAIChatCompletion.IntegrationTests; + +#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. +#pragma warning disable CA1812 // Internal class that is apparently never instantiated. + +internal sealed class OpenAIConfiguration +{ + public string? ServiceId { get; set; } + + public string ChatModelId { get; set; } + + public string ApiKey { get; set; } +}