diff --git a/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor b/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor index c83b6d26..12f8b054 100644 --- a/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor +++ b/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor @@ -7,6 +7,13 @@ New chat - -

OpenChat Playground

+

+ OpenChat Playground + + + @Settings.ConnectorType + | + @Settings.Model + +

diff --git a/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor.cs b/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor.cs index 3f755644..d090b341 100644 --- a/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor.cs +++ b/src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Components; +using OpenChat.PlaygroundApp.Configurations; namespace OpenChat.PlaygroundApp.Components.Pages.Chat; @@ -6,4 +7,7 @@ public partial class ChatHeader : ComponentBase { [Parameter] public EventCallback OnNewChat { get; set; } + + [Inject] + public required AppSettings Settings { get; set; } } diff --git a/src/OpenChat.PlaygroundApp/Configurations/AppSettings.cs b/src/OpenChat.PlaygroundApp/Configurations/AppSettings.cs index a0dfaa81..efc7f507 100644 --- a/src/OpenChat.PlaygroundApp/Configurations/AppSettings.cs +++ b/src/OpenChat.PlaygroundApp/Configurations/AppSettings.cs @@ -12,6 +12,11 @@ public partial class AppSettings /// public ConnectorType ConnectorType { get; set; } + /// + /// Gets or sets the model name to use. + /// + public string? Model { get; set; } + /// /// Gets or sets the value indicating whether to display help information or not. /// diff --git a/src/OpenChat.PlaygroundApp/Extensions/AppSettingsExtensions.cs b/src/OpenChat.PlaygroundApp/Extensions/AppSettingsExtensions.cs new file mode 100644 index 00000000..e5b4828a --- /dev/null +++ b/src/OpenChat.PlaygroundApp/Extensions/AppSettingsExtensions.cs @@ -0,0 +1,58 @@ +using OpenChat.PlaygroundApp.Configurations; +using OpenChat.PlaygroundApp.Connectors; + +namespace OpenChat.PlaygroundApp.Extensions; + +/// +/// This represents the extension entity for handling AppSettings configuration. +/// +public static class AppSettingsExtensions +{ + /// + /// Configures and adds AppSettings to the service collection with model name populated. + /// + /// The instance. + /// The instance. + /// The instance. + /// Returns the modified instance. + public static IServiceCollection AddAppSettings(this IServiceCollection services, IConfiguration configuration, AppSettings settings) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(settings); + + ConfigureModelName(configuration, settings); + services.AddSingleton(settings); + + return services; + } + + /// + /// Configures the model name in AppSettings based on the connector type. + /// + /// The instance. + /// The instance. + private static void ConfigureModelName(IConfiguration configuration, AppSettings settings) + { + string? modelFromSettings = settings.ConnectorType switch + { + ConnectorType.AzureAIFoundry => settings.AzureAIFoundry?.DeploymentName, + ConnectorType.FoundryLocal => settings.FoundryLocal?.Alias, + ConnectorType.GitHubModels => settings.GitHubModels?.Model, + ConnectorType.OpenAI => settings.OpenAI?.Model, + ConnectorType.HuggingFace => settings.HuggingFace?.Model, + ConnectorType.Anthropic => settings.Anthropic?.Model, + ConnectorType.LG => settings.LG?.Model, + _ => throw new ArgumentException($"Unsupported ConnectorType: {settings.ConnectorType}") + }; + + var section = configuration.GetSection(settings.ConnectorType.ToString()); + + settings.Model = modelFromSettings ?? settings.ConnectorType switch + { + ConnectorType.AzureAIFoundry => section.GetValue("DeploymentName"), + ConnectorType.FoundryLocal => section.GetValue("Alias"), + _ => section.GetValue("Model") + }; + } +} \ No newline at end of file diff --git a/src/OpenChat.PlaygroundApp/Program.cs b/src/OpenChat.PlaygroundApp/Program.cs index 24a588ca..92ee206b 100644 --- a/src/OpenChat.PlaygroundApp/Program.cs +++ b/src/OpenChat.PlaygroundApp/Program.cs @@ -3,6 +3,7 @@ using OpenChat.PlaygroundApp.Abstractions; using OpenChat.PlaygroundApp.Components; using OpenChat.PlaygroundApp.Endpoints; +using OpenChat.PlaygroundApp.Extensions; using OpenChat.PlaygroundApp.OpenApi; using OpenChat.PlaygroundApp.Services; @@ -16,6 +17,8 @@ return; } +builder.Services.AddAppSettings(config, settings); + builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); diff --git a/test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatHeaderUITests.cs b/test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatHeaderUITests.cs index a55b4898..18d749d9 100644 --- a/test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatHeaderUITests.cs +++ b/test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatHeaderUITests.cs @@ -1,6 +1,8 @@ using Microsoft.Playwright; using Microsoft.Playwright.Xunit; +using OpenChat.PlaygroundApp.Connectors; + namespace OpenChat.PlaygroundApp.Tests.Components.Pages.Chat; public class ChatHeaderUITests : PageTest @@ -18,12 +20,26 @@ public override async Task InitializeAsync() public async Task Given_Root_Page_When_Loaded_Then_Header_Should_Be_Visible(string expected) { // Act - var title = await Page.Locator("h1").InnerTextAsync(); + var title = await Page.Locator("span.app-title-text").InnerTextAsync(); // Assert title.ShouldBe(expected); } + [Trait("Category", "IntegrationTest")] + [Fact] + public async Task Given_Root_Page_When_Loaded_Then_Header_Should_Display_ConnectorType_And_Model() + { + // Act + var connector = await Page.Locator("span.app-connector").InnerTextAsync(); + var model = await Page.Locator("span.app-model").InnerTextAsync(); + + // Assert + connector.ShouldNotBeNullOrEmpty(); + Enum.IsDefined(typeof(ConnectorType), connector).ShouldBeTrue(); + model.ShouldNotBeNullOrEmpty(); + } + [Trait("Category", "IntegrationTest")] [Fact] public async Task Given_Root_Page_When_Loaded_Then_NewChat_Button_Should_Be_Visible() diff --git a/test/OpenChat.PlaygroundApp.Tests/Extensions/AppSettingsExtensionsTests.cs b/test/OpenChat.PlaygroundApp.Tests/Extensions/AppSettingsExtensionsTests.cs new file mode 100644 index 00000000..7d0ca15a --- /dev/null +++ b/test/OpenChat.PlaygroundApp.Tests/Extensions/AppSettingsExtensionsTests.cs @@ -0,0 +1,340 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +using OpenChat.PlaygroundApp.Configurations; +using OpenChat.PlaygroundApp.Connectors; +using OpenChat.PlaygroundApp.Extensions; + +namespace OpenChat.PlaygroundApp.Tests.Extensions; + +public class AppSettingsExtensionsTests +{ + private const string openaiDefaultModel = "openai-default-model"; + private const string azureDefaultModel = "azure-default-model"; + private const string foundryDefaultModel = "foundry-default-model"; + private const string openaiCommandLineModel = "openai-commandline-model"; + private const string azureCommandLineModel = "azure-commandline-model"; + private const string foundryCommandLineModel = "foundry-commandline-model"; + + private static IConfiguration CreateConfiguration(Dictionary? configData = null) + { + var data = configData ?? new Dictionary(); + return new ConfigurationBuilder() + .AddInMemoryCollection(data) + .Build(); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Null_Services_When_AddAppSettings_Invoked_Then_It_Should_Throw_ArgumentNullException() + { + // Arrange + IServiceCollection services = null!; + var configuration = CreateConfiguration(); + var settings = new AppSettings { ConnectorType = ConnectorType.OpenAI }; + + // Act + Action action = () => services.AddAppSettings(configuration, settings); + + // Assert + action.ShouldThrow() + .Message.ShouldContain("Value cannot be null"); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Null_Configuration_When_AddAppSettings_Invoked_Then_It_Should_Throw_ArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + IConfiguration configuration = null!; + var settings = new AppSettings { ConnectorType = ConnectorType.OpenAI }; + + // Act + Action action = () => services.AddAppSettings(configuration, settings); + + // Assert + action.ShouldThrow() + .Message.ShouldContain("Value cannot be null"); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Null_Settings_When_AddAppSettings_Invoked_Then_It_Should_Throw_ArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + var configuration = CreateConfiguration(); + AppSettings settings = null!; + + // Act + Action action = () => services.AddAppSettings(configuration, settings); + + // Assert + action.ShouldThrow() + .Message.ShouldContain("Value cannot be null"); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Unsupported_ConnectorType_When_AddAppSettings_Invoked_Then_It_Should_Throw_ArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var configuration = CreateConfiguration(); + var settings = new AppSettings { ConnectorType = ConnectorType.Unknown }; + + // Act + Action action = () => services.AddAppSettings(configuration, settings); + + // Assert + action.ShouldThrow() + .Message.ShouldContain("Unsupported ConnectorType"); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Valid_Parameters_When_AddAppSettings_Invoked_Then_It_Should_Return_Services() + { + // Arrange + var services = new ServiceCollection(); + var configuration = CreateConfiguration(); + var settings = new AppSettings { ConnectorType = ConnectorType.OpenAI }; + + // Act + var result = services.AddAppSettings(configuration, settings); + + // Assert + result.ShouldNotBeNull(); + result.ShouldBeSameAs(services); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_Valid_Parameters_When_AddAppSettings_Invoked_Then_It_Should_Register_AppSettings_As_Singleton() + { + // Arrange + var services = new ServiceCollection(); + var configuration = CreateConfiguration(); + var settings = new AppSettings { ConnectorType = ConnectorType.OpenAI }; + + // Act + services.AddAppSettings(configuration, settings); + var descriptor = services.FirstOrDefault(sd => sd.ServiceType == typeof(AppSettings)); + + // Assert + descriptor.ShouldNotBeNull(); + descriptor.Lifetime.ShouldBe(ServiceLifetime.Singleton); + descriptor.ImplementationInstance.ShouldBeSameAs(settings); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_OpenAI_CommandLine_Model_When_AddAppSettings_Invoked_Then_It_Should_Use_CommandLine_Value() + { + // Arrange + var services = new ServiceCollection(); + var configuration = CreateConfiguration(); + var settings = new AppSettings + { + ConnectorType = ConnectorType.OpenAI, + OpenAI = new OpenAISettings { Model = openaiCommandLineModel } + }; + + // Act + services.AddAppSettings(configuration, settings); + + // Assert + settings.Model.ShouldBe(openaiCommandLineModel); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_AzureAIFoundry_CommandLine_Model_When_AddAppSettings_Invoked_Then_It_Should_Use_CommandLine_Value() + { + // Arrange + var services = new ServiceCollection(); + var configuration = CreateConfiguration(); + var settings = new AppSettings + { + ConnectorType = ConnectorType.AzureAIFoundry, + AzureAIFoundry = new AzureAIFoundrySettings { DeploymentName = azureCommandLineModel } + }; + + // Act + services.AddAppSettings(configuration, settings); + + // Assert + settings.Model.ShouldBe(azureCommandLineModel); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_FoundryLocal_CommandLine_Model_When_AddAppSettings_Invoked_Then_It_Should_Use_CommandLine_Value() + { + // Arrange + var services = new ServiceCollection(); + var configuration = CreateConfiguration(); + var settings = new AppSettings + { + ConnectorType = ConnectorType.FoundryLocal, + FoundryLocal = new FoundryLocalSettings { Alias = foundryCommandLineModel } + }; + + // Act + services.AddAppSettings(configuration, settings); + + // Assert + settings.Model.ShouldBe(foundryCommandLineModel); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_OpenAI_No_CommandLine_Model_When_AddAppSettings_Invoked_Then_It_Should_Use_Configuration_Default() + { + // Arrange + var services = new ServiceCollection(); + var configData = new Dictionary + { + ["OpenAI:Model"] = openaiDefaultModel + }; + var configuration = CreateConfiguration(configData); + + var settings = new AppSettings + { + ConnectorType = ConnectorType.OpenAI, + OpenAI = new OpenAISettings() + }; + + // Act + services.AddAppSettings(configuration, settings); + + // Assert + settings.Model.ShouldBe(openaiDefaultModel); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_AzureAIFoundry_No_CommandLine_Model_When_AddAppSettings_Invoked_Then_It_Should_Use_Configuration_Default() + { + // Arrange + var services = new ServiceCollection(); + var configData = new Dictionary + { + ["AzureAIFoundry:DeploymentName"] = azureDefaultModel + }; + var configuration = CreateConfiguration(configData); + + var settings = new AppSettings + { + ConnectorType = ConnectorType.AzureAIFoundry, + AzureAIFoundry = new AzureAIFoundrySettings() + }; + + // Act + services.AddAppSettings(configuration, settings); + + // Assert + settings.Model.ShouldBe(azureDefaultModel); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_FoundryLocal_No_CommandLine_Model_When_AddAppSettings_Invoked_Then_It_Should_Use_Configuration_Default() + { + // Arrange + var services = new ServiceCollection(); + var configData = new Dictionary + { + ["FoundryLocal:Alias"] = foundryDefaultModel + }; + var configuration = CreateConfiguration(configData); + + var settings = new AppSettings + { + ConnectorType = ConnectorType.FoundryLocal, + FoundryLocal = new FoundryLocalSettings() + }; + + // Act + services.AddAppSettings(configuration, settings); + + // Assert + settings.Model.ShouldBe(foundryDefaultModel); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_OpenAI_CommandLine_Priority_When_AddAppSettings_Invoked_Then_It_Should_Override_Configuration() + { + // Arrange + var services = new ServiceCollection(); + var configData = new Dictionary + { + ["OpenAI:Model"] = openaiDefaultModel + }; + var configuration = CreateConfiguration(configData); + + var settings = new AppSettings + { + ConnectorType = ConnectorType.OpenAI, + OpenAI = new OpenAISettings { Model = openaiCommandLineModel } + }; + + // Act + services.AddAppSettings(configuration, settings); + + // Assert + settings.Model.ShouldBe(openaiCommandLineModel); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_AzureAIFoundry_CommandLine_Priority_When_AddAppSettings_Invoked_Then_It_Should_Override_Configuration() + { + // Arrange + var services = new ServiceCollection(); + var configData = new Dictionary + { + ["AzureAIFoundry:DeploymentName"] = azureDefaultModel + }; + var configuration = CreateConfiguration(configData); + + var settings = new AppSettings + { + ConnectorType = ConnectorType.AzureAIFoundry, + AzureAIFoundry = new AzureAIFoundrySettings { DeploymentName = azureCommandLineModel } + }; + + // Act + services.AddAppSettings(configuration, settings); + + // Assert + settings.Model.ShouldBe(azureCommandLineModel); + } + + [Trait("Category", "UnitTest")] + [Fact] + public void Given_FoundryLocal_CommandLine_Priority_When_AddAppSettings_Invoked_Then_It_Should_Override_Configuration() + { + // Arrange + var services = new ServiceCollection(); + var configData = new Dictionary + { + ["FoundryLocal:Alias"] = foundryDefaultModel + }; + var configuration = CreateConfiguration(configData); + + var settings = new AppSettings + { + ConnectorType = ConnectorType.FoundryLocal, + FoundryLocal = new FoundryLocalSettings { Alias = foundryCommandLineModel } + }; + + // Act + services.AddAppSettings(configuration, settings); + + // Assert + settings.Model.ShouldBe(foundryCommandLineModel); + } +} \ No newline at end of file